업데이트: 2007년 11월
다음 단원에서는 다중 스레드 응용 프로그램에서 리소스에 대한 액세스를 동기화하는 데 사용할 수 있는 기능과 클래스에 대해 설명합니다.
응용 프로그램에서 다중 스레드를 사용할 때의 이점 중 하나는 각 스레드가 비동기적으로 실행된다는 점입니다. Windows 응용 프로그램의 경우 이렇게 하면 응용 프로그램 창과 컨트롤의 응답 가능 상태를 유지한 채 시간이 오래 걸리는 작업을 백그라운드에서 수행할 수 있습니다. 서버 응용 프로그램의 경우 다중 스레딩을 사용하면 들어오는 각 요청을 서로 다른 스레드로 처리할 수 있습니다. 그렇지 않으면 이전 요청이 완전히 처리될 때까지 새로운 각 요청의 처리를 시작할 수 없습니다.
그러나 스레드의 비동기적 특성으로 인해 파일 핸들, 네트워크 연결, 메모리 등과 같은 리소스에 대한 액세스를 조정해야 한다는 문제가 있습니다. 그렇지 않으면 두 개 이상의 스레드에서 각각 다른 스레드의 작업을 인식하지 못한 채 동시에 동일한 리소스에 액세스할 수 있습니다. 그 결과로 예기치 않은 데이터 손상이 발생할 수 있습니다.
정수 숫자 데이터 형식에 대한 간단한 연산의 경우 Interlocked 클래스의 멤버를 통해 스레드를 동기화할 수 있습니다. 다른 모든 데이터 형식과 스레드로부터 안전하게 보호되지 않는 리소스의 경우 다중 스레딩을 안전하게 수행하려면 이 항목에서 설명하는 구문을 사용해야만 합니다.
다중 스레드 프로그래밍에 대한 배경 지식은 다음을 참조하십시오.
lock 키워드를 사용하면 다른 스레드의 방해를 받지 않은 채 코드 블록의 실행을 완료할 수 있습니다. 이를 위해서는 코드 블록을 진행하는 동안 지정된 개체에 대한 상호 배타적 잠금을 유지해야 합니다.
lock 문은 lock 키워드로 시작합니다. 여기에 개체가 인수로 제공되고 한 번에 스레드 하나에서만 실행할 코드 블록이 그 뒤에 나옵니다. 예를 들면 다음과 같습니다.
lock 키워드에 제공되는 인수는 참조 형식을 기반으로 한 개체여야 하고 이 개체는 잠금 범위를 정의하는 데 사용됩니다. 위 예제에서 함수 외부에 lockThis 개체에 대한 참조가 없으므로 잠금 범위는 이 함수로 제한됩니다. 이와 같은 참조가 있는 경우에는 잠금 범위가 해당 개체에 맞게 확대됩니다. 엄밀하게 말해서 lock에 제공되는 개체는 여러 스레드 간에 공유되는 리소스를 고유하게 식별하는 데만 사용되므로 이는 임의의 클래스 인스턴스가 될 수 있습니다. 그러나 실제로 코드를 작성하는 경우 이 개체는 일반적으로 스레드 동기화가 필요한 리소스를 나타냅니다. 예를 들어, 컨테이너 개체를 여러 스레드에서 사용해야 하는 경우 이 컨테이너를 lock 키워드에 전달하고 동기화된 코드 블록을 그 뒤에 추가하여 컨테이너에 액세스할 수 있습니다. 다른 스레드는 동일한 컨테이너에 대해 잠긴 상태이므로 이 개���에 액세스할 수 없고 개체에 대한 액세스가 안전하게 동기화됩니다.
일반적으로 public 형식이나 사용자 응용 프로그램의 제어 범위 밖에 있는 개체 인스턴스에 대해서는 잠금을 사용하지 않는 것이 좋습니다. 예를 들어, 인스턴스에 공용으로 액세스할 수 있는 경우 lock(this)을 사용하면 문제가 발생할 수 있습니다. 제어 범위 밖에 있는 코드마저 개체에 대해 잠길 수 있기 때문입니다. 이 경우 동일한 개체가 해제되기를 두 개 이상의 스레드가 기다리는 교착 상태가 발생할 수 있습니다. 개체와 달리 공용 데이터 형식에 대해 잠금을 수행하는 경우에도 동일한 이유로 인해 문제가 발생할 수 있습니다. 리터럴 문자열에 대해 잠금을 수행하는 경우는 특히 위험합니다. 리터럴 문자열은 CLR(공용 언어 런타임)에서 사용하도록 의도되어 있기 때문입니다. 즉, 전체 프로그램에서 임의의 지정된 문자열에 대한 인스턴스가 하나 있으며 정확하게 동일한 개체는 실행 중인 모든 응용 프로그램 도메인에서 모든 스레드에 대해 이 리터럴을 나타냅니다. 그 결과, 응용 프로그램 프로세스에서 내용이 동일한 문자열을 잠그면 응용 프로그램에서 해당 문자열의 인스턴스가 모두 잠깁니다. 따라서 잠금은 의도되지 않은 전용 또는 보호된 멤버에 대해 수행하는 것이 좋습니다. 일부 클래스는 잠금을 위한 특별한 멤버를 제공합니다. 예를 들어, Array 형식은 SyncRoot를 제공합니다. 대부분의 컬렉션 형식은 SyncRoot 멤버도 제공합니다.
lock 키워드에 대한 자세한 내용은 다음을 참조하십시오.
lock 키워드와 마찬가지로 monitor를 사용하면 코드 블록이 여러 스레드에서 동시에 실행되지 않도록 방지할 수 있습니다. Enter 메서드를 사용하면 스레드 하나만 다음 문으로 진행하도록 허용할 수 있습니다. 다른 모든 스레드는 현재 실행 중인 스레드가 Exit를 호출할 때까지 차단됩니다. 이는 lock 키워드를 사용할 때와 동일합니다. 사실 lock 키워드는Monitor 클래스를 통해 구현됩니다. 예를 들면 다음과 같습니다.
위 코드는 다음과 같습니다.
일반적으로 Monitor 클래스를 직접 사용하는 것보다 lock 키워드를 사용하는 것이 더 좋습니다. lock 키워드를 사용하면 코드를 더 간결하게 작성할 수 있고 lock 키워드의 경우 보호된 코드에서 예외를 throw하더라도 내부 모니터를 해제할 수 있기 때문입니다. 이를 수행하는 데는 finally 키워드가 사용됩니다. 이 키워드는 예외가 throw되었는지 여부와 상관없이 관련 코드 블록을 실행합니다.
monitor에 대한 자세한 내용은 Monitor Synchronization 기술 샘플을 참조하십시오.
lock 또는 monitor를 사용하면 스레드가 중요한 부분을 차지하는 코드 블록이 동시에 실행되지 않도록 방지할 수 있지만 이러한 구문을 사용하면 한 스레드가 다른 스레드에 이벤트를 전달할 수 없습니다. 이 문제를 해결하기 위해서는 스레드를 활성화하거나 일시 중단하는 데 사용할 수 있고 신호를 받은 상태 및 신호를 받지 않은 상태 중 한 가지 상태가 지정되는 개체인 동기화 이벤트가 필요합니다. 스레드를 일시 중단하려면 신호를 받지 않은 상태의 동기화 이벤트에서 스레드를 대기시키고, 스레드를 활성화하려면 신호를 받은 상태로 이벤트 상태를 변경합니다. 이미 신호를 받은 상태의 이벤트에서 스레드를 대기시키려고 하면 스레드가 지연 시간 없이 계속 실행됩니다.
동기화 이벤트에는 AutoResetEvent와 ManualResetEvent라는 두 가지 종류가 있습니다. 이 둘 사이의 유일한 차이는 AutoResetEvent의 경우 스레드를 활성화할 때마다 신호를 받은 상태에서 신호를 받지 않은 상태로 자동으로 변경된다는 점입니다. 반대로, ManualResetEvent를 사용하면 신호를 받은 상태를 통해 스레드를 그 수에 상관없이 활성화할 수 있고 해당 Reset 메서드를 호출한 경우에만 신호를 받지 않은 상태로 되돌릴 수 있습니다.
WaitOne, WaitAny 또는 WaitAll 같은 대기 메서드 중 하나를 호출하여 이벤트에 대해 스레드가 대기하도록 할 수 있습니다. WaitHandle.WaitOne()은 단일 이벤트가 신호를 받을 때까지 스레드를 대기시키고, WaitHandle.WaitAny()는 하나 이상의 지정된 이벤트가 신호를 받을 때까지 스레드를 차단하며, WaitHandle.WaitAll()은 지정된 모든 이벤트가 신호를 받을 때까지 스레드를 차단합니다. 이벤트는 해당 Set 메서드가 호출되면 신호를 받은 상태로 변경됩니다.
다음 예제에서는 Main 함수를 사용하여 스레드를 만들고 시작합니다. 새 스레드는 WaitOne 메서드를 사용하여 이벤트에서 대기합니다. Main 함수를 실행하는 기본 스레드를 통해 이벤트가 신호를 받은 상태가 될 때까지 이 스레드는 일시 중단됩니다. 이벤트가 신호를 받은 상태가 되면 보조 스레드가 반환됩니다. 이 경우 이벤트는 스레드 하나의 활성화에만 사용되므로 AutoResetEvent 또는 ManualResetEvent 클래스를 사용할 수 있습니다.
using System; using System.Threading; class ThreadingExample { static AutoResetEvent autoEvent; static void DoWork() { Console.WriteLine(" worker thread started, now waiting on event..."); autoEvent.WaitOne(); Console.WriteLine(" worker thread reactivated, now exiting..."); } static void Main() { autoEvent = new AutoResetEvent(false); Console.WriteLine("main thread starting worker thread..."); Thread t = new Thread(DoWork); t.Start(); Console.WriteLine("main thread sleeping for 1 second..."); Thread.Sleep(1000); Console.WriteLine("main thread signaling worker thread..."); autoEvent.Set(); } }
스레드 동기화 이벤트를 사용하는 방법을 보여 주는 다른 예제는 다음을 참조하십시오.
뮤텍스는 monitor와 비슷합니다. 이는 한 번에 여러 스레드에서 코드 블록이 동시에 실행되는 것을 방지합니다. 사실 "뮤텍스(mutex)"라는 용어는 "상호 배타적(mutually exclusive)"이라는 표현의 줄임말입니다. 그러나 monitor와 달리 뮤텍스를 사용하면 프로세스 간에 스레드를 동기화할 수 있습니다. 뮤텍스는 Mutex 클래스로 표현됩니다.
프로세스간 동기화에 사용되는 뮤텍스를 명명된 뮤텍스라고 합니다. 이는 다른 응용 프로그램에 사용하기 위한 것이며 전역 또는 정적 변수를 통해 공유할 수 없기 때문입니다. 두 응용 프로그램에서 모두 동일한 뮤텍스 개체에 액세스할 수 있도록 이 뮤텍스에 이름을 지정해야 합니다.
프로세스 내의 스레드를 동기화하는 데 뮤텍스를 사용할 수도 있지만 일반적으로 Monitor를 사용하는 것이 더 좋습니다. monitor는 .NET Framework용으로 특별히 디자인되었으며 리소스를 더 효율적으로 활용하기 때문입니다. 반면, Mutex 클래스는 Win32 구문에 대한 래퍼입니다. 이는 monitor보다 더 강력하지만 뮤텍스를 사용하려면 Monitor 클래스에 필요한 것보다 더 처리가 복잡한 interop 전환이 필요합니다. 뮤텍스를 사용하는 방법의 예제는 뮤텍스를 참조하십시오.
업데이트: 2007년 11월
.NET Framework는 스레드의 상호 작용을 처리하고 경합 상태를 피하기 위한 다양한 동기화 기본 형식을 제공합니다. 이러한 기본 형식은 크게 잠금, 신호 및 연동 작업으로 분류할 수 있습니다.
범주가 명확히 정의되어 있지 않기 때문에 일부 동기화 메커니즘에 여러 범주의 특성이 있고, 한 번에 하나의 스레드를 해제하는 이벤트가 잠금과 비슷한 기능을 수행하고, 잠금 해제가 신호로 간주될 수 있고, 연동 작업이 잠금을 생성하는 데 사용될 수 있습니다. 그러나 범주는 여전히 유용합니다.
스레드 동기화는 공동 작업입니다. 즉, 스레드가 하나라도 동기화 메커니즘을 사용하지 않고 보호된 리소스에 직접 액세스하는 경우 이 동기화 메커니즘은 유효하지 않습니다.
잠금은 한 번에 하나의 스레드만 리소스를 제어할 수 있도록 하거나 지정한 수의 스레드가 리소스를 제어할 수 있도록 합니다. 잠금이 사용 중일 때 단독 잠금을 요청하는 스레드는 잠금을 사용할 수 있게 될 때까지 차단됩니다.
단독 잠금
가장 간단한 형식의 잠금은 코드 블록에 대한 액세스를 제어하는 C# lock 문(Visual Basic의 경우SyncLock)입니다. 이러한 블록을 임계 영역이라고도 합니다. lock 문은 Monitor클래스의 Enter 및 Exit 메서드를 사용하여 구현되며 try¢¢c|catch¢¢c|finally를 사용하여 잠금이 해제되도록 합니다.
일반적으로 lock 문을 사용하여 여러 메서드에 걸쳐 있지 않은 작은 코드 블록을 보호하는 것이 Monitor 클래스를 가장 효과적으로 사용하는 방법입니다. Monitor 클래스는 강력하기는 하지만 고아 잠금 상태와 교착 상태가 발생하기 쉽습니다.
Monitor 클래스
Monitor 클래스는 다음과 같이 lock 문과 함께 사용할 수 있는 추가 기능을 제공합니다.
TryEnter 메서드를 사용하면 지정한 간격 후에 차단된 스레드의 리소스 대기 상태를 해제할 수 있습니다. 이 메서드는 성공 또는 실패를 나타내는 부울 값을 반환합니다. 이 값은 잠재적인 교착 상태를 감지하여 막는 데 사용될 수 있습니다.
Wait 메서드는 임계 영역의 스레드에 의해 호출됩니다. 이 메서드는 리소스를 다시 사용할 수 있게 될 때까지 리소스와 블록을 제어하지 않습니다.
Pulse 및 PulseAll 메서드를 사용하면 잠금을 해제하거나 Wait를 호출할 스레드가 하나 이상의 스레드를 준비된 큐로 이동하여 잠금을 가져오게 할 수 있습니다.
Wait 메서드 오버로드에 대한 제한 시간은 대기 스레드가 준비된 큐로 이스케이프되게 할 수 있습니다.
잠금에 사용되는 개체가 MarshalByRefObject에서 파생되는 경우 Monitor 클래스는 여러 응용 프로그램 도메인에서 잠금을 제공할 수 있습니다.
Monitor에는 스레드 선호도가 있습니다. 즉, 모니터에 들어간 스레드는 Exit 또는 Wait를 호출하여 빠져 나와야 합니다.
Monitor 클래스는 인스턴스화할 수 없습니다. 해당 메서드는 정적(Visual Basic의 경우 Shared)이며 인스턴스화할 수 있는 잠금 개체에 대해 작동합니다.
개념적 개요를 보려면 Monitor를 참조하십시오.
Mutex 클래스
스레드는 해당 WaitOne 메서드의 오버로드를 호출하여 Mutex를 요청합니다. 시간 제한이 있는 오버로드는 스레드가 대기 상태를 해제할 수 있도록 하기 위해 제공됩니다.Monitor 클래스와 달리 뮤텍스는 지역적일 수도 있고 전역적일 수도 있습니다. 전역 뮤텍스는 명명된 뮤텍스라고도 하며 전체 운영 체제에서 표시되고 여러 응용 프로그램 도메인 또는 프로세스에서 스레드를 동기화하는 데 사용할 수 있습니다. 지역 뮤텍스는 MarshalByRefObject에서 파생되며 응용 프로그램 도메인 경계에 걸쳐 사용될 수 있습니다.
또한 Mutex는 WaitHandle에서 파생되므로 WaitAll, WaitAny 및 SignalAndWait 메서드와 같은 WaitHandle에서 제공하는 신호 메커니즘에 사용될 수 있습니다.
Monitor와 마찬가지로 Mutex에도 스레드 선호도가 있습니다. 그러나 Monitor와 달리 Mutex는 인스턴스화할 수 있는 개체입니다.
개념적 개요를 보려면 뮤텍스를 참조하십시오.
기타 잠금
잠금은 반드시 단독 잠금이 아니어도 됩니다. 단독 잠금은 일반적으로 제한된 수의 스레드가 리소스에 동시 액세스할 수 있도록 허용하는 데 유용합니다. 이러한 종류의 풀링 리소스 액세스를 제어하기 위해 세마포와 판독기 및 작성기 잠금이 설계되었습니다.
ReaderWriterLock 클래스
ReaderWriterLockSlim 클래스는 데이터를 변경하는 스레드인 작성기가 리소스에 독점적으로 액세스해야 하는 경우에 사용됩니다. 작성기가 활성화되어 있지 않으면 임의의 수의 판독기가 EnterReadLock 메서드를 호출하는 등의 방법으로 리소스에 액세스할 수 있습니다. 스레드에서 EnterWriteLock 메서드를 호출하는 등의 방법으로 단독 액세스를 요청하면 모든 기존 판독기가 잠금을 종료하고 작성기가 잠금을 시작 및 종료할 때까지 이후 판독기 요청이 차단됩니다.
ReaderWriterLockSlim에는 스레드 선호도가 있습니다.
개념적 개요를 보려면 판독기 및 작성기 잠금을 참조하십시오.
Semaphore 클래스
Semaphore 클래스를 사용하면 지정한 수의 스레드가 리소스에 액세스할 수 있습니다. 리소스를 요청하는 추가 스레드는 스레드가 세마포를 해제할 때까지 차단됩니다.
Mutex 클래스와 마찬가지로 Semaphore는 WaitHandle에서 파생됩니다. 또한 Mutex와 마찬가지로 Semaphore는 지역적일 수도 있고 전역적일 수도 있습니다. 이 클래스는 응용 프로그램 도메인 경계에 걸쳐 사용될 수 있습니다.
Monitor, Mutex 및 ReaderWriterLock과 달리 Semaphore에는 스레드 선호도가 없습니다. 즉, 이 클래스는 하나의 스레드가 세마포를 가져오고 다른 스레드가 이 세마포를 해제하는 시나리오에서 사용할 수 있습니다.
개념적 개요를 보려면 세마포를 참조하십시오.
다른 스레드로부터의 신호를 대기하는 가장 간단한 방법은 Join 메서드를 호출하는 것입니다. 이 메서드는 다른 스레드가 완료될 때까지 차단됩니다. Join에는 지정한 간격이 경과되면 차단된 스레드의 대기 상태를 해제하는 두 가지 오버로드가 있습니다.
대기 핸들은 보다 다양한 대기 및 신호 기능을 제공합니다.
대기 핸들
대기 핸들은 WaitHandle 클래스에서 파생되고 이 클래스는 MarshalByRefObject에서 파생됩니다. 따라서 대기 핸들은 응용 프로그램 도메인 경계에서 스레드 활동을 동기화하는 데 사용할 수 있습니다.
스레드는 WaitOne 인스턴스 메서드나 WaitAll, WaitAny 또는 SignalAndWait 정적 메서드 중 하나를 호출하여 대기 핸들을 차단합니다. 해제 방법은 호출된 메서드와 해당 대기 핸들의 유형에 따라 다릅니다.
개념적 개요를 보려면 대기 핸들을 참조하십시오.
이벤트 대기 핸들
이벤트 대기 핸들에는 EventWaitHandle 클래스와 해당 파생 클래스인 AutoResetEvent 및 ManualResetEvent가 포함됩니다. 스레드는 해당 Set 메서드를 호출하거나SignalAndWait 메서드를 사용하여 이벤트 대기 핸들에 신호를 보낼 때 이벤트 대기 핸들에서 해제됩니다.
이벤트 대기 핸들은 신호를 보낼 때마다 하나의 스레드만 허용되는 회전문과 같이 자동으로 다시 설정되거나, 신호를 보내면 열리고 다른 사용자가 닫으면 다음 신호까지 그 상태가 유지되는 게이트와 같이 수동으로 다시 설정해야 합니다. 이름에서 알 수 있듯이 AutoResetEvent와 ManualResetEvent는 각각 전자와 후자를 나타냅니다.
EventWaitHandle은 이벤트 형식을 나타내므로 지역 또는 전역입니다. 파생 클래스인 AutoResetEvent와 ManualResetEvent의 형식은 항상 지역입니다.
이벤트 대기 핸들에는 스레드 선호도가 없습니다. 모든 스레드는 이벤트 대기 핸들에 신호를 보낼 수 있습니다.
개념적 개요를 보려면 EventWaitHandle, AutoResetEvent 및 ManualResetEvent를 참조하십시오.
Mutex 및 Semaphore 클래스
Mutex 및 Semaphore 클래스는 WaitHandle에서 파생되므로 WaitHandle의 정적 메서드에서 사용할 수 있습니다. 예를 들어, 스레드는 WaitAll 메서드를 사용하여 세 가지 해당 조건이 충족될 때까지 대기할 수 있습니다. 즉, EventWaitHandle이 신호를 받아야 하고 Mutex가 해제되어야 하며 Semaphore도 해제되어야 합니다. 마찬가지로 스레드는WaitAny 메서드를 사용하여 이 세 가지 조건 중 하나가 충족될 때까지 대기할 수 있습니다.
Mutex 또는 Semaphore의 경우 신호를 받는다는 것은 해제됨을 의미합니다. 둘 중 하나의 형식을 SignalAndWait 메서드의 첫 번째 인수로 사용하면 메서드가 해제됩니다. 스레드 선호도가 있는 Mutex의 경우 호출 스레드에 뮤텍스가 없으면 예외가 throw됩니다. 앞에서 설명한 대로 세마포에는 스레드 선호도가 없습니다.
연동 작업은 Interlocked 클래스의 정적 메서드에 의해 메모리 위치에서 수행되는 간단한 원자 작업입니다. 이러한 원자 작업에는 비교 내용에 따라 추가, 증가 및 감소, 교환, 조건부 교환과, 32비트 플랫폼의 64비트 값에 대한 읽기 작업이 포함됩니다.
원자성에 대한 보장은 개발 작업으로 제한됩니다. 즉, 여러 작업을 하나의 단위로 수행해야 하는 경우 덜 정교한 동기화 메커니즘을 사용해야 합니다. |
이 작업은 잠금 또는 신호는 아니지만 잠금과 신호를 생성하는 데 사용할 수 있습니다. 또한 연동 작업은 Windows 운영 체제의 기본 작업이므로 속도가 매우 빠릅니다.
연동 작업은 강력한 비차단 동시성을 수행하는 응용 프로그램을 작성하기 위한 휘발성 메모리에 사용할 수 있지만 정교하면서도 낮은 수준의 프로그램이 필요합니다. 대부분의 경우 간단한 잠금을 사용하는 것이 좋습니다.
개념적 개요를 보려면 연동 작업을 참조하십시오.
'C#, .NET' 카테고리의 다른 글
다차원 배열. jagged array (0) | 2011.06.17 |
---|---|
C++, C#, Java의 문자열 비교 (0) | 2011.06.17 |
combobox 색선택, readonly 로 만들기 (0) | 2011.06.17 |
Brushes 에 정의된 색 (0) | 2011.06.17 |
DateTime 출력형식 출력모양 메서드에 따른 나오는모양 (0) | 2011.06.17 |