다음을 통해 공유


스레드 동기화(C# 및 Visual Basic)

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

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

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

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

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

잠금 및 SyncLock 키워드

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

lock 또는 SyncLock 문은 개체를 인수로 지정하며 뒤에 한 번에 하나의 스레드에서만 실행할 코드 블록이 나옵니다. 예를 들면 다음과 같습니다.

Public Class TestThreading
    Dim lockThis As New Object 

    Public Sub Process()
        SyncLock lockThis
            ' Access thread-sensitive resources. 
        End SyncLock 
    End Sub 
End Class
public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Process()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}

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

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

lock 및 SyncLock 문에 대한 자세한 내용은 다음 항목을 참조하십시오.

Monitor

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

SyncLock x
    DoSomething()
End SyncLock
lock (x)
{
    DoSomething();
}

다음과 일치합니다.

Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
    DoSomething()
Finally
    System.Threading.Monitor.Exit(obj)
End Try
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

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

동기화 이벤트 및 대기 핸들

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

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

스레드는 WaitOne, WaitAny 또는 WaitAll 등의 대기 메서드 중 하나를 호출하여 이벤트에 대기할 수 있도록 합니다. WaitHandle.WaitOne은 단일 이벤트가 신호를 받을 때까지 스레드를 대기시키고, WaitHandle.WaitAny는 하나 이상의 지정된 이벤트가 신호를 받을 때까지 스레드를 차단하며, WaitHandle.WaitAll은 지정된 모든 이벤트가 신호를 받을 때까지 스레드를 차단합니다. 이벤트는 해당 Set 메서드가 호출되면 신호를 받은 상태로 변경됩니다.

다음 예제에서는 Main 함수를 사용하여 스레드를 만들고 시작합니다. 새 스레드는 WaitOne 메서드를 사용하여 이벤트에서 대기합니다. Main 함수를 실행하는 기본 스레드를 통해 이벤트가 신호를 받은 상태가 될 때까지 이 스레드는 일시 중단됩니다. 이벤트가 신호를 받은 상태가 되면 보조 스레드가 반환됩니다. 이 경우 이벤트는 스레드 하나의 활성화에만 사용되므로 AutoResetEvent 또는 ManualResetEvent 클래스를 사용할 수 있습니다.

Imports System.Threading

Module Module1
    Dim autoEvent As AutoResetEvent

    Sub DoWork()
        Console.WriteLine("   worker thread started, now waiting on event...")
        autoEvent.WaitOne()
        Console.WriteLine("   worker thread reactivated, now exiting...")
    End Sub 

    Sub Main()
        autoEvent = New AutoResetEvent(False)

        Console.WriteLine("main thread starting worker thread...")
        Dim t As New Thread(AddressOf DoWork)
        t.Start()

        Console.WriteLine("main thread sleeping for 1 second...")
        Thread.Sleep(1000)

        Console.WriteLine("main thread signaling worker thread...")
        autoEvent.Set()
    End Sub 
End Module
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 전환이 필요합니다. 뮤텍스를 사용하는 방법의 예제는 뮤텍스를 참조하십시오.

Interlocked 클래스

Interlocked 클래스의 메서드를 사용하면 여러 스레드에서 같은 값을 동시에 업데이트하거나 비교하려고 할 때 발생할 수 있는 문제를 방지할 수 있습니다. 이 클래스의 메서드를 사용하면 모든 스레드에서 값을 안전하게 늘리거나, 줄이거나, 교환하거나, 비교할 수 있습니다.

ReaderWriter 잠금

일부 경우에는 데이터를 쓰고 있을 때만 리소스를 잠그고 데이터를 업데이트하지 않을 때는 여러 클라이언트에서 동시에 데이터를 읽을 수 있도록 할 수 있습니다. ReaderWriterLock 클래스를 사용하면 스레드에서 리소스를 수정하는 동안은 리소스를 단독으로 사용하고 리소스를 읽을 때는 여러 스레드에서 동시에 사용하도록 할 수 있습니다. ReaderWriter 잠금은 해당 스레드에서 데이터를 업데이트할 필요가 없는 경우에도 다른 스레드를 대기하도록 만드는 단독 잠금 대신 사용할 수 있는 유용한 기능입니다.

교착 상태

스레드 동기화는 다중 스레드 응용 프로그램에서 매우 중요하지만 여러 스레드가 서로를 대기하여 응용 프로그램이 중지되는 deadlock이 발생할 위험이 항상 존재합니다. 교착 상태는 교차로에서 자동차들이 서로 다른 자동차가 가기를 기다리며 모두 멈춰있는 상황과 비슷합니다. 따라서 교착 상태를 방지하는 것이 중요하며 이를 위해서는 철저한 계획을 세워야 합니다. 종종 코딩을 시작하기 전에 다중 스레드 응용 프로그램의 다이어그램을 작성하면 교착 상태를 예측할 수 있습니다.

관련 단원

방법: 스레드 풀 사용(C# 및 Visual Basic)

HOW TO: Visual C# .NET을 사용하여 다중 스레딩 환경에서 공유 리소스에 대한 액세스 동기화

HOW TO: Visual C# .NET을 사용하여 스레드 만들기

Visual C#를 사용하여 작업 항목을 스레드 풀에 제출하는 방법

HOW TO: Visual C# .NET을 사용하여 다중 스레딩 환경에서 공유 리소스에 대한 액세스 동기화

참고 항목

참조

SyncLock 문

lock 문(C# 참조)

Thread

WaitOne

WaitAny

WaitAll

Join

Start

Sleep

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

EventWaitHandle

System.Threading

Set

개념

다중 스레드 응용 프로그램(C# 및 Visual Basic)

뮤텍스

Monitor

연동 작업

AutoResetEvent

다중 스레딩을 위한 데이터 동기화

기타 리소스

CLR 비동기 프로그래밍 모델 구현

C#의 간소화된 APM

교착 상태 모니터

구성 요소에서 다중 스레딩

HOW TO: Visual C# .NET을 사용하여 다중 스레딩 환경에서 공유 리소스에 대한 액세스 동기화