스레드 동기화(C# 프로그래밍 가이드)

다음 단원에서는 다중 스레드 응용 프로그램에서 리소스에 대한 액세스를 동기화하는 데 사용할 수 있는 기능과 클래스에 대해 설명합니다.

응용 프로그램에서 다중 스레드를 사용할 때의 이점 중 하나는 각 스레드가 비동기적으로 실행된다는 점입니다. Windows 응용 프로그램의 경우 이렇게 하면 응용 프로그램 창과 컨트롤의 응답 가능 상태를 유지한 채 시간이 오래 걸리는 작업을 백그라운드에서 수행할 수 있습니다. 서버 응용 프로그램의 경우 다중 스레딩을 사용하면 들어오는 각 요청을 서로 다른 스레드로 처리할 수 있습니다. 그렇지 않으면 이전 요청이 완전히 처리될 때까지 새로운 각 요청의 처리를 시작할 수 없습니다.

그러나 스레드의 비동기적 특성으로 인해 파일 핸들, 네트워크 연결, 메모리 등과 같은 리소스에 대한 액세스를 조정해야 한다는 문제가 있습니다. 그렇지 않으면 두 개 이상의 스레드에서 각각 다른 스레드의 작업을 인식하지 못한 채 동시에 동일한 리소스에 액세스할 수 있습니다. 그 결과로 예기치 않은 데이터 손상이 발생할 수 있습니다.

정수 숫자 데이터 형식에 대한 간단한 연산의 경우 Interlocked 클래스의 멤버를 통해 스레드를 동기화할 수 있습니다. 다른 모든 데이터 형식과 스레드로부터 안전하게 보호되지 않는 리소스의 경우 다중 스레딩을 안전하게 수행하려면 이 항목에서 설명하는 구문을 사용해야만 합니다.

다중 스레드 프로그래밍에 대한 배경 지식은 다음을 참조하십시오.

lock 키워드

lock 키워드를 사용하면 다른 스레드의 방해를 받지 않은 채 코드 블록의 실행을 완료할 수 있습니다. 이를 위해서는 코드 블록을 진행하는 동안 지정된 개체에 대한 상호 배타적 잠금을 유지해야 합니다.

lock 문은 lock 키워드로 시작합니다. 여기에 개체가 인수로 제공되고 한 번에 스레드 하나에서만 실행할 코드 블록이 그 뒤에 나옵니다. 예를 들면 다음과 같습니다.

public void Function()
{
    System.Object lockThis = new System.Object();
    lock(lockThis)
    {
        // Access thread-sensitive resources.
    }
}

lock 키워드에 제공되는 인수는 참조 형식을 기반으로 한 개체여야 하고 이 개체는 잠금 범위를 정의하는 데 사용됩니다. 위 예제에서 잠금 범위는 이 함수로 제한되어 있습니다. 함수 바깥에 개체에 대한 참조가 없기 때문입니다. 엄밀하게 말해서 lock에 제공되는 개체는 여러 스레드 간에 공유되는 리소스를 고유하게 식별하는 데만 사용되므로 이는 임의의 클래스 인스턴스가 될 수 있습니다. 그러나 실제로 코드를 작성하는 경우 이 개체는 일반적으로 스레드 동기화가 필요한 리소스를 나타냅니다. 예를 들어, 컨테이너 개체를 여러 스레드에서 사용해야 하는 경우 이 컨테이너를 lock 키워드에 전달하고 동기화된 코드 블록을 그 뒤에 추가하여 컨테이너에 액세스할 수 있습니다. 다른 스레드는 동일한 컨테이너에 대해 잠긴 상태이므로 이 개체에 액세스할 수 없고 개체에 대한 액세스가 안전하게 동기화됩니다.

일반적으로 public 형식이나 사용자 응용 프로그램의 제어 범위 밖에 있는 개체 인스턴스에 대해서는 잠금을 사용하지 않는 것이 좋습니다. 예를 들어, 인스턴스에 공용으로 액세스할 수 있는 경우 lock(this)을 사용하면 문제가 발생할 수 있습니다. 제어 범위 밖에 있는 코드마저 개체에 대해 잠길 수 있기 때문입니다. 이 경우 동일한 개체가 해제되기를 두 개 이상의 스레드가 기다리는 교착 상태가 발생할 수 있습니다. 개체와 달리 공용 데이터 형식에 대해 잠금을 수행하는 경우에도 동일한 이유로 인해 문제가 발생할 수 있습니다. 리터럴 문자열에 대해 잠금을 수행하는 경우는 특히 위험합니다. 리터럴 문자열은 CLR(공용 언어 런타임)에서 사용하도록 "의도"되어 있기 때문입니다. 즉, 전체 프로그램에서 임의의 지정된 문자열에 대한 인스턴스가 하나 있으며 정확하게 동일한 개체는 실행 중인 모든 응용 프로그램 도메인에서 모든 스레드에 대해 이 리터럴을 나타냅니다. 그 결과, 응용 프로그램 프로세스에서 내용이 동일한 문자열을 잠그면 응용 프로그램에서 해당 문자열의 인스턴스가 모두 잠깁니다. 따라서 잠금은 의도되지 않은 전용 또는 보호된 멤버에 대해 수행하는 것이 좋습니다. 일부 클래스는 잠금을 위한 특별한 멤버를 제공합니다. 예를 들어, Array 형식은 SyncRoot를 제공합니다. 대부분의 컬렉션 형식은 SyncRoot 멤버도 제공합니다.

lock 키워드에 대한 자세한 내용은 다음을 참조하십시오.

Monitor

lock 키워드와 마찬가지로 monitor를 사용하면 코드 블록이 여러 스레드에서 동시에 실행되지 않도록 방지할 수 있습니다. Enter 메서드를 사용하면 스레드 하나만 다음 문으로 진행하도록 허용할 수 있습니다. 다른 모든 스레드는 현재 실행 중인 스레드가 Exit를 호출할 때까지 차단됩니다. 이는 lock 키워드를 사용할 때와 동일합니다. 사실 lock 키워드는 Monitor 클래스를 통해 구현됩니다. 예를 들면 다음과 같습니다.

lock(x)
{
    DoSomething();
}

위 코드는 다음과 같습니다.

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

일반적으로 Monitor 클래스를 직접 사용하는 것보다 lock 키워드를 사용하는 것이 더 좋습니다. lock 키워드를 사용하면 코드를 더 간결하게 작성할 수 있고 lock 키워드의 경우 보호된 코드에서 예외를 throw하더라도 내부 모니터를 해제할 수 있기 때문입니다. 이를 수행하는 데는 finally 키워드가 사용됩니다. 이 키워드는 예외가 throw되었는지 여부와 상관없이 관련 코드 블록을 실행합니다.

monitor에 대한 자세한 내용은 Monitor Synchronization 기술 샘플을 참조하십시오.

동기화 이벤트 및 대기 핸들

lock 또는 monitor를 사용하면 스레드가 중요한 부분을 차지하는 코드 블록이 동시에 실행되지 않도록 방지할 수 있지만 이러한 구문을 사용하면 한 스레드가 다른 스레드에 이벤트를 전달할 수 없습니다. 이 문제를 해결하기 위해서는 스레드를 활성화하거나 일시 중단하는 데 사용할 수 있고 신호를 받은 상태 및 신호를 받지 않은 상태 중 한 가지 상태가 지정되는 개체인 "동기화 이벤트"가 필요합니다. 스레드를 일시 중단하려면 신호를 받지 않은 상태의 동기화 이벤트에서 스레드를 대기시키고, 스레드를 활성화하려면 신호를 받은 상태로 이벤트 상태를 변경합니다. 이미 신호를 받은 상태의 이벤트에서 스레드를 대기시키려고 하면 스레드가 지연 시간 없이 계속 실행됩니다.

동기화 이벤트에는 AutoResetEventManualResetEvent라는 두 가지 종류가 있습니다. 이 둘 사이의 유일한 차이는 AutoResetEvent의 경우 스레드를 활성화할 때마다 신호를 받은 상태에서 신호를 받지 않은 상태로 자동으로 변경된다는 점입니다. 반대로, ManualResetEvent를 사용하면 신호를 받은 상태를 통해 스레드를 그 수에 상관없이 활성화할 수 있고 해당 Reset 메서드를 호출한 경우에만 신호를 받지 않은 상태로 되돌릴 수 있습니다.

WaitOne, WaitAny 또는 WaitAll 같은 대기 메서드 중 하나를 호출하여 이벤트에 대해 스레드가 대기하도록 할 수 있습니다. System.Threading.WaitHandle.WaitOne은 단일 이벤트가 신호를 받은 상태가 될 때까지 스레드를 대기시키고, System.Threading.WaitHandle.WaitAny는 하나 이상의 지정된 이벤트가 신호를 받은 상태가 될 때까지 스레드를 차단하고, System.Threading.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 thrad 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 전환이 필요합니다. 뮤텍스를 사용하는 방법의 예제는 뮤텍스를 참조하십시오.

관련 단원

참고 항목

참조

Thread
WaitOne
WaitAny
WaitAll
Monitor
Mutex
AutoResetEvent
ManualResetEvent
Interlocked
WaitHandle

개념

C# 프로그래밍 가이드