Programming/Dot.NET

Visual Basic.NET으로 다중 스레드 프로그래밍

bcheul 2013. 7. 31. 09:22

Visual Basic.NET으로 다중 스레드 프로그래밍

NONSCROLLING BANNER END

Robert Burns
Visual Studio 팀
Microsoft Corporation

요약: .NET Framework에서는 다중 스레드 응용 프로그램을 쉽게 만들 수 있는 클래스를 제공합니다. 이 기사에서는 Visual Basic.NET과 함께 다중 스레드 프로그래밍 기술을 사용하여 더욱 효율적이고 응답 기능이 우수한 응용 프로그램을 개발하는 방법에 대해 설명합니다(18페이지/인쇄 페이지 기준).

목차

소개
다중 스레드 처리의 장점
새 스레드 만들기
스레드 동기화
스레드 타이머
작업 취소
결론

소개

종래의 Visual Basic 개발자들은 프로그램 작업이 순서대로 실행되는 동기식 응용 프로그램을 개발했습니다. 다중 스레드 응용 프로그램은 여러 작업을 거의 동시에 실행하기 때문에 효율적이지만, 이전 버전의 Visual Basic을 사용하여 만들기는 어려웠습니다.

여러 응용 프로그램을 동시에 실행할 수 있는 기능을 시뮬레이트하는 멀티태스킹이라는 운영 체제 기능 덕분에 다중 스레드 프로그램이 가능해집니다. PC에는 대부분 프로세서가 하나만 있지만, 오늘날의 운영 체제에서는 스레드라고 하는 실행 코드의 여러 부분들 간에 프로세서 시간을 나눔으로써 멀티태스킹을 제공합니다. 스레드란 하나의 응용 프로그램 전체를 나타낼 수 있지만, 별도로 실행할 수 있는 응용 프로그램의 한 부분만을 나타내는 경우가 더 많습니다. 운영 체제에서는 스레드의 우선 순위와 스레드가 마지막으로 실행된 이후의 경과 시간을 기반으로 각 스레드에 대한 처리 시간을 할당합니다. 파일 입력 및 출력과 같이 시간이 많이 소요되는 작업을 실행할 때는 여러 스레드가 성능을 크게 향상시킬 수 있습니다.

여기에는 한 가지 주의할 사항이 있습니다. 스레드가 여러 개이면 성능이 향상될 수 있지만, 스레드마다 그 스레드를 만드는 데 추가 메모리가 필요하고 스레드를 실행 상태로 유지하는 데 프로세서 시간이 필요하다는 점에서 비용이 수반됩니다. 실제로 스레드를 너무 많이 만들면 응용 프로그램의 성능이 저하될 수 있습니다. 다중 스레드 응용 프로그램을 디자인할 때는 스레드 추가로 인한 이점과 비용을 잘 따져 봐야 합니다.

멀티태스킹은 한동안 운영 체제의 일부였습니다. 그러나 최근까지도 Visual Basic 프로그래머는 문서화되지 않은 기능을 사용하거나 운영 체제의 비동기 부분 또는 COM 구성 요소를 사용하여 간접적으로만 다중 스레드 작업을 실행할 수 있었습니다. .NET Framework에서는 다중 스레드 응용 프로그램 개발을 위한 System.Threading 네임스페이스를 포괄적으로 지원합니다.

이 기사에서는 다중 스레딩의 장점에 대해 설명하고 Visual Basic .NET을 사용하여 다중 스레드 응용 프로그램을 개발하는 방법을 설명합니다. Visual Basic .NET과 .NET Framework를 사용하면 다중 스레드 응용 프로그램을 쉽게 만들 수 있지만, 이 백서에서는 이전 버전의 Visual Basic에서 Visual Basic .NET으로 전환하고 있는 개발자와 함께 중급 및 고급 개발자들도 대상으로 합니다. Visual Basic .NET을 사용한 지 얼마 되지 않는 경우에는 Visual Basic Language Tour 의 항목들을 먼저 읽어 보아야 합니다.

이 기사의 끝부분에 나오는 추가 리소스에는 여기서 다루는 다중 스레드 프로그래밍에 대한 포괄적인 설명의 범위를 벗어나는 내용이기는 하지만 자세한 내용에 대한 링크가 있습니다.

다중 스레드 처리의 장점

동기식 응용 프로그램은 다중 스레드 응용 프로그램에 비해 개발이 쉽기는 하지만, 이전 작업이 끝나기 전에는 새로운 작업을 시작할 수 없기 때문에 다중 스레드 응용 프로그램보다 효율성이 떨어지는 경우가 많습니다. 동기 작업이 완료되는 데 예상보다 시간이 오래 걸리는 경우에는 응용 프로그램이 응답을 안 하는 것처럼 보일 수도 있습니다. 다중 스레드 처리는 여러 개의 프로시저를 동시에 실행할 수 있습니다. 예를 들어, 응용 프로그램에서 문서 작업을 계속하면서 별도의 작업으로 맞춤법 검사를 실행할 수 있습니다. 다중 스레드 응용 프로그램은 프로그램을 독립된 여러 작업으로 나누기 때문에 다음과 같은 방식으로 성능을 크게 향상시킬 수 있습니다.

  • 다중 스레드 기술을 사용하면 다른 작업이 계속되는 동안에도 사용자 인터페이스가 활성 상태로 유지되므로 프로그램의 응답이 더욱 빨라질 수 있습니다.
  • 현재 사용 중이 아닌 작업은 다른 작업에 프로세서 시간을 양보할 수 있습니다.
  • 프로세서 시간을 많이 사용하는 작업은 주기적으로 다른 작업에 양보할 수 있습니다.
  • 언제든지 작업을 중지할 수 있습니다.
  • 개별 작업의 우선 순위를 더 높게 또는 더 낮게 설정하여 성능을 최적화할 수 있습니다.

다중 스레드 응용 프로그램을 명시적으로 만들지 여부에 대한 판단은 몇 가지 요소에 따라 좌우됩니다. 다중 스레드는 다음과 같은 경우에 가장 적합합니다.

  • 많은 시간이 소요되거나 프로세서를 많이 사용하는 작업으로 인해 사용자 인터페이스가 차단되는 경우
  • 개별 작업이 원격 파일이나 인터넷 연결과 같은 외부 리소스를 기다려야 하는 경우

예를 들어, 웹 페이지의 링크를 따라 이동하여 특정 조건에 맞는 파일을 다운로드하는 응용 프로그램인 인터넷 "로봇"을 생각해 보십시오. 그러한 응용 프로그램은 각 파일을 하나씩 동기식으로 다운로드하거나 다중 스레딩을 사용하여 여러 파일을 동시에 다운로드할 수 있습니다. 다중 스레드 방법은 일부 스레드가 원격 웹 서버로부터 느린 응답을 받더라도 파일을 다운로드할 수 있기 때문에 동기 방식보다 훨씬 효율적일 수 있습니다.

새 스레드 만들기

스레드를 만드는 가장 간단한 방법은 스레드 클래스의 새로운 인스턴스를 만들고 AddressOf 문을 사용하여 실행할 프로시저에 대한 대리자를 전달하는 것입니다. 예를 들어, 다음은 SomeTask라는 Sub 프로시저를 별도의 스레드로 실행하는 코드입니다.

Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
Thread1.Start
' 실행할 코드가 바로 옵니다.

위 코드에는 스레드를 만들고 시작하는 데 필요한 모든 내용이 있습니다. 스레드의 Start 메서드 호출 다음에 오는 코드는 이전 스레드가 끝날 때까지 기다리지 않고 바로 실행됩니다.

다음 표는 개별 스레드를 제어하는 데 사용할 수 있는 일부 메서드를 나타냅니다.

메서드 작업
Start 스레드가 실행되기 시작합니다.
Sleep 지정한 시간 동안 스레드가 일시 중지됩니다.
Suspend 안전한 시점에 도달하면 스레드가 일시 중지됩니다.
Abort 안전한 시점에 도달하면 스레드가 중지됩니다.
Resume 일시 중단된 스레드가 다시 시작됩니다
Join 다른 스레드가 끝날 때까지 현재 스레드가 대기됩니다. 시간 초과 값과 함께 사용하면, 할당된 시간에 스레드가 끝나는 경우 True가 반환됩니다.

이러한 메서드들은 대부분 이름만 보아도 그 기능을 쉽게 알 수 있지만, "안전한 시점"이라는 개념은 생소할 수도 있습니다. 안전한 시점이란 코드에서 공용 언어 런타임(CLR)이 사용하지 않는 변수를 해제하고 메모리를 재사용하는 과정인 자동 "가비지 수집"을 실행해도 좋은 지점을 의미합니다. 스레드의 Abort 또는 Suspend 메서드를 호출하면 공용 언어 런타임(CLR)이 코드를 분석하여 스레드의 실행을 중지하기에 적합한 지점을 결정합니다.

또한 스레드에는 다음과 같이 유용한 속성들이 많이 있습니다.

속성
IsAlive 스레드가 활성 상태이면 True 값이 포함됩니다.
IsBackground 스레드가 백그라운드 스레드인지 여부를 확인하거나 백그라운드 스레드여야 하는지 여부를 설정합니다. 백그라운드 스레드는 포그라운드 스레드와 비슷하지만, 프로세스가 종료되는 것을 막지는 않습니다. 한 프로세스에 속하는 포그라운드 스레드가 모두 종료된 경우, 공용 언어 런타임(CLR)은 아직 활성 상태인 백그라운드 스레드에 대해 Abort 메서드를 호출하여 해당 프로세스를 종료시킵니다.
Name 스레드의 이름을 확인하거나 설정합니다. 디버깅시 개별 스레드를 찾는 데 가장 많이 사용됩니다.
Priority 운영 체제에서 스레드 일정의 우선 순위를 매기는 데 사용하는 값을 확인하거나 설정합니다.
ApartmentState 특정 스레드에 사용된 스레딩 모델을 확인하거나 설정합니다. 스레딩 모델은 관리되지 않는 코드를 스레드가 호출할 때 중요합니다.
ThreadState 스레드의 상태를 설명하는 값이 포함됩니다.

스레드 속성과 메서드는 스레드를 만들고 관리할 때 유용합니다. 이 기사의 스레드 동기화 절에서는 이러한 속성과 메서드를 사용하여 스레드를 제어하고 조정하는 방법에 대해 설명합니다.

스레드 인수 및 반환 값

앞에 나온 예제의 메서드 호출에는 매개 변수나 반환 값을 포함할 수 없습니다. 바로 이 점이 이 방식으로 스레드를 만들고 실행하는 데 있어 주요 단점 중 하나입니다. 그러나 클래스나 구조에 래핑하는 방식으로 별도의 스레드에 대해 실행되는 프로시저에 인수를 제공하거나 반환할 수 있습니다.

Class TasksClass
   Friend StrArg As String
   Friend RetVal As Boolean
   Sub SomeTask()
      ' StrArg 필드를 인수로 사용합니다.       
      MsgBox("StrArg에 포함된 문자열은 " & StrArg & "입니다.")
      RetVal = True ' 반환 인수에 반환 값을 설정합니다.
   End Sub
End Class
' 클래스를 사용하려면 매개 변수가 저장된 필드나 속성을 설정한 다음 
' 필요에 따라 메서드를 비동기로 호출합니다.
Sub DoWork()
   Dim Tasks As New TasksClass()
   Dim Thread1 As New System.Threading.Thread( _
       AddressOf Tasks.SomeTask)
   Tasks.StrArg = "Some Arg" ' 인수로 사용되는 필드를 설정합니다.
   Thread1.Start() ' 새 스레드를 시작합니다.
   Thread1.Join() ' 스레드 1이 끝날 때까지 기다립니다. 
   ' 반환 값을 표시합니다.    
   MsgBox("스레드 1이 " & Tasks.RetVal & "값을 반환했습니다.")
End Sub

응용 프로그램에서 스레드 우선 순위와 스레딩 모델과 같은 세부적인 사항을 미세 조정하려는 경우에는 스레드를 수동으로 만들고 관리하는 방법이 가장 적합합니다. 스레드가 많은 경우에는 이런 식으로 관리하기가 어려울 수 있습니다. 스레드가 많이 필요한 경우에는 스레드 풀링을 사용하여 복잡한 정도를 줄여 보십시오.

스레드 풀링

스레드 풀링은 스레드가 만들어질 때 작업이 대기열에 추가되고 자동으로 시작되는 다중 스레딩의 한 형태입니다. 스레드 풀링에서는 실행할 프로시저에 대한 대리자와 함께 Threadpool.QueueUserWorkItem 메서드를 사용자가 호출하고, Visual Basic.NET에서 스레드를 만들고 사용자의 프로시저를 실행합니다. 다음은 스레드 풀링을 사용하여 몇 가지 작업을 시작하는 방법을 보여 주는 예제입니다.

Sub DoWork()
   Dim TPool As System.Threading.ThreadPool
   ' 작업을 대기열에 보관합니다.
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf SomeLongTask))
   ' 또 다른 작업을 대기열에 보관합니다.
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf AnotherLongTask))
End Sub

스레드 풀링은 각 스레드의 속성을 개별적으로 설정하지 않은 채로 별도의 여러 작업을 시작하려는 경우에 유용합니다. 각 스레드는 기본 스택 크기와 우선 순위로 시작됩니다. 기본적으로 시스템 프로세서 당 최대 25개의 스레드 풀 스레드가 실행될 수 있습니다. 이 한도를 넘는 추가 스레드는 대기열에 보관될 수 있지만, 다른 스레드가 끝날 때까지는 시작되지 않습니다.

스레드 풀링의 한 가지 장점은 상태 개체의 인수를 작업 프로시저에 전달할 수 있다는 점입니다. 호출할 프로시저에 두 개 이상의 인수가 필요한 경우에는 구조나 클래스 인스턴스를 Object 데이터 형식으로 캐스팅할 수 있습니다.

매개 변수 및 반환 값

스레드 풀 스레드에서 값을 반환하는 것은 약간 까다롭습니다. 스레드 풀의 대기열에 보관될 수 있는 유일한 프로시저 유형이 Sub 프로시저이기 때문에 함수 호출에서 값을 반환하는 표준적인 방법은 사용할 수 없습니다. 매개 변수와 반환 값을 제공할 수 있는 한 가지 방법은 스레드 인수 및 반환 값에 나오는 것처럼 매개 변수, 반환 값, 메서드를 래퍼 클래스로 래핑하는 것입니다. 더 쉬운 방법은 QueueUserWorkItem 메서드의 선택적인 ByVal 상태 개체 변수를 사용하는 것입니다. 이 변수를 사용하여 클래스 인스턴스에 대한 참조를 전달하는 경우, 해당 인스턴스의 구성원은 스레드 풀 스레드에 의해 수정되고 반환 값으로 사용될 수 있습니다. 처음에는 값으로 전달되는 변수에 의해 참조된 개체를 수정할 수 있다는 것이 쉽게 이해되지 않을 수도 있습니다. 이것은 개체 참조만이 값으로 전달되기 때문에 가능한 것입니다. 개체 참조로 참조되는 개체의 구성원을 변경하면 변경 내용이 실제 클래스 인스턴스에 적용됩니다.

상태 개체 안에서 값을 반환하는 데는 구조를 사용할 수 없습니다. 구조는 값 형식이므로 비동기 프로세스의 변경으로 인해 원래 구조의 구성원이 변경되지는 않습니다. 반환 값이 필요 없을 경우에는 구조를 사용하여 매개 변수를 제공합니다.

Friend Class StateObj
   Friend StrArg As String
   Friend IntArg As Integer
   Friend RetVal As String
End Class

Sub ThreadPoolTest()
   Dim TPool As System.Threading.ThreadPool
   Dim StObj1 As New StateObj()
   Dim StObj2 As New StateObj()
   ' 상태 개체에서 매개 변수와 같은 기능을 하는 일부 필드를 설정합니다.
   StObj1.IntArg = 10
   StObj1.StrArg = "약간의 문자열"
   StObj2.IntArg = 100
   StObj2.StrArg = "약간의 다른 문자열"
   ' 작업을 대기열에 보관합니다.
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf SomeOtherTask), StObj1)
   ' 또 다른 작업을 대기열에 보관합니다.
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf AnotherTask), StObj2)
End Sub

Sub SomeOtherTask(ByVal StateObj As Object)
   ' 상태 개체 필드를 인수로 사용합니다.
   Dim StObj As StateObj
   StObj = CType(StateObj, StateObj)   ' 정확한 형식으로 캐스팅합니다.
   MsgBox("StrArg에 포함된 문자열은 " & StObj.StrArg & "입니다.")
   MsgBox("IntArg에 포함된 숫자는 " & CStr(StObj.IntArg) & "입니다.")
   ' 필드를 반환 값으로 사용합니다.
   StObj.RetVal = "SomeOtherTask의 반환 값"
End Sub

Sub AnotherTask(ByVal StateObj As Object)
   ' 상태 개체 필드를 인수로 사용합니다.
   ' 상태 개체는 Object로 전달됩니다.
   ' 구체적인 해당 형식으로 캐스팅하면 사용하기가 더욱 쉽습니다.
   Dim StObj As StateObj
   StObj = CType(StateObj, StateObj)
   MsgBox("StrArg에 포함된 문자열은 " & StObj.StrArg & "입니다.")
   MsgBox("IntArg에 포함된 숫자는 " & CStr(StObj.IntArg) & "입니다.")
   ' 필드를 반환 값으로 사용합니다.
   StObj.RetVal = "AnotherTask의 반환 값"
End Sub

공용 언어 런타임(CLR)은 대기 중인 스레드 풀 작업에 대한 스레드를 자동으로 만든 다음 해당 작업이 완료되면 이러한 리소스를 해제합니다. 작업이 대기열에 보관된 후에는 그 작업을 쉽게 취소할 수 있는 방법이 없습니다. ThreadPool 스레드는 항상 MTA(다중 스레드 아파트) 스레딩 모델을 사용하여 실행됩니다. STA(단일 스레드 아파트) 모델을 사용하는 스레드를 원하는 경우에는 스레드를 수동으로 만들어야 합니다.

스레드 동기화

동기화는 다중 스레드 프로그래밍의 비구조적 특징과 동기식 처리의 구조적 순서 간 절충안을 제공합니다.

다음과 같은 작업에 동기화 기술을 사용합니다.

  • 작업이 특정 순서로 실행되어야 할 때마다 코드가 실행되는 순서를 명시적으로 제어합니다.

    또는

  • 두 개의 스레드가 동시에 동일한 리소스를 공유하는 경우에 발생할 수 있는 문제를 막습니다.

예를 들어, 동기화를 사용하면 또 다른 스레드에서 실행되고 있는 데이터 검색 프로시저가 끝날 때까지 표시 프로시저는 기다리도록 할 수 있습니다.

동기화에 대한 방법은 폴링과 동기화 개체 사용 등 두 가지가 있습니다. 폴링은 루프 안에서 이루어지는 비동기 호출의 상태를 반복적으로 확인합니다. 폴링은 여러 가지 스레드 속성의 상태를 반복적으로 확인하면서 리소스를 낭비하므로 스레드 관리에는 효율성이 가장 낮은 방법입니다.

예를 들어, 스레드가 끝났는지 여부를 확인하기 위해 폴링할 때는 IsAlive 속성을 사용할 수 있습니다. 활성화되어 있는 스레드가 반드시 실행 중인 것은 아니므로 이 속성을 사용할 때는 주의해서 사용해야 합니다. 스레드의 ThreadState 속성을 사용하면 스레드의 상태에 대한 자세한 내용을 볼 수 있습니다. 스레드는 주어진 시점에 두 가지 이상의 상태가 될 수 있으므로, ThreadState에 저장된 값은 System.Threading.Threadstate 열거의 값을 조합한 것일 수 있습니다. 따라서 폴링할 때는 모든 관련 스레드 상태를 주의해서 확인해야 합니다. 예를 들어, 스레드의 상태가 실행 중으로 표시되지 않는다면 완료된 것일 수 있습니다. 반면에, 일시 중단되거나 대기 중일 수도 있습니다.

폴링을 사용하면 스레드 실행 순서를 제어할 수 있게 되는 대신 다중 스레딩의 일부 장점을 잃게 됩니다. 더욱 효율적인 방법은 Join 메서드를 사용하여 스레드를 제어하는 것입니다. Join을 사용하면 호출하는 프로시저가 스레드가 완료될 때까지 또는 시간 초과가 지정된 경우 호출 시간이 초과될 때까지 기다리게 됩니다. Join이라는 이름은 새 스레드를 만드는 것이 실행 경로의 포크라는 개념을 기반으로 정해진 것입니다. Join을 사용하여 별도의 실행 경로들을 단일 스레드로 다시 병합합니다.

그림 1 스레딩

Join이 동기 또는 "차단" 호출이라는 점을 확실히 해야 합니다. Join 또는 대기 핸들의 대기 메서드를 호출하면, 호출하는 프로시저가 중지되고 스레드가 완료되었다는 신호를 보낼 때까지 기다리게 됩니다.

Sub JoinThreads()
   Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
   Thread1.Start()
   Thread1.Join()      ' 스레드가 끝날 때까지 기다립니다.   
   MsgBox("스레드가 완료되었습니다.")
End Sub

이렇게 간단하게 스레드를 제어하는 방법은 적은 수의 스레드를 관리하는 경우에는 유용하지만, 큰 프로젝트에서 사용하기는 어렵습니다. 다음 절에서는 스레드 동기화에 사용할 수 있는 몇 가지 고급 기술에 대해 설명합니다.

고급 동기화 기술

다중 스레드 응용 프로그램에서는 여러 스레드를 동기화하기 위해 대기 핸들과 모니터 개체를 사용하는 경우가 많습니다. 다음 표에서는 스레드를 동기화할 때 사용할 수 있는 .NET Framework 클래스 일부에 대해 설명합니다.

클래스 목적
AutoResetEvent 한 개 이상의 대기 스레드에 이벤트가 발생했음을 알려 주는 대기 핸들입니다. AutoResetEvent는 대기 스레드가 해제되면 상태를 신호 받음으로 자동 변경합니다.
Interlocked 여러 스레드에 의해 공유되는 변수에 대한 단일 연산 작업을 제공합니다.
ManualResetEvent 한 개 이상의 대기 스레드에 이벤트가 발생했음을 알려 주는 대기 핸들입니다. 수동으로 다시 설정된 이벤트의 상태는 Reset 메서드로 신호 받지 않음 상태로 설정할 때까지 신호 받음으로 유지됩니다. 마찬가지로, Set 메서드로 신호 받음 상태로 설정할 때까지는 신호 받지 않음 상태가 유지됩니다. 대기 스레드, 즉 대기 함수 중 하나를 호출하여 지정한 이벤트 개체에 대한 대기 작업을 차후에 시작하는 스레드의 경우, 그 수에 관계 없이 개체의 상태가 신호 받음인 동안 해제될 수 있습니다.
Monitor 개체에 대한 액세스를 동기화하는 메커니즘을 제공합니다. Visual Basic.NET 응용 프로그램에서는 SyncLock을 호출하여 모니터 개체를 사용합니다.
Mutex 프로세스 간 동기화에 사용할 수 있는 대기 핸들입니다.
ReaderWriterLock 단일 기록기와 다중 판독기 기능을 구현하는 잠금을 정의합니다.
Timer 지정한 간격으로 작업을 실행할 수 있는 메커니즘을 제공합니다.
WaitHandle 공유 리소스에 대한 단독 사용 권한을 기다리는 운영 체제별 개체를 캡슐화합니다.

대기 핸들

대기 핸들은 한 스레드의 상태를 다른 스레드에 신호로 보내는 개체입니다. 스레드는 대기 핸들을 사용하여 리소스에 대한 단독 사용 권한이 필요하다는 것을 다른 스레드에 알릴 수 있습니다. 다른 스레드가 이 리소스를 사용하려면 해당 대기 핸들이 더 이상 사용되지 않을 때까지 기다려야 합니다. 대기 핸들은 "신호 받음"과 "신호 받지 않음" 등 두 가지 상태를 갖습니다. 어떤 스레드에 의해서도 소유되지 않은 대기 핸들은 "신호 받음" 상태이며, 스레드에 의해 소유된 대기 핸들은 "신호 받지 않음" 상태입니다.

스레드는 WaitOne, WaitAny, WaitAll과 같은 대기 메서드 중 하나를 호출하여 대기 핸들의 소유권을 요청합니다. 대기 메서드는 개별 스레드에 대한 Join 메서드와 비슷한 차단 호출입니다.

  • 대기 핸들을 소유하는 다른 스레드가 없으면 해당 호출에서 바로 True가 반환되고, 대기 핸들의 상태는 신호 받지 않음으로 변경되며, 해당 대기 핸들을 소유하는 스레드는 계속해서 실행됩니다.
  • 스레드가 대기 핸들의 대기 메서드 중 하나를 호출하는 경우, 해당 대기 핸들이 다른 스레드에 의해 소유된 것이면 호출하는 스레드는 지정한 시간 동안(시간 초과가 지정된 경우) 기다리거나 다른 스레드가 대기 핸들을 해제할 때까지 무한정(시간 초과가 지정되지 않은 경우) 기다립니다. 시간 초과가 지정되어 있고 시간이 초과되기 전에 대기 핸들이 해제되면 해당 호출에서 True가 반환됩니다. 그렇지 않은 경우에는 False를 반환하고 대기하며, 호출하는 스레드는 계속해서 실행됩니다.

대기 핸들을 소유하는 스레드는 완료되는 경우 또는 해당 대기 핸들이 필요 없게 되는 경우 Set 메서드를 호출합니다. 다른 스레드들은 Reset 메서드를 호출하거나 WaitOne, WaitAll 또는 WaitAny를 호출하고 스레드가 Set을 호출할 때까지 기다림으로써 대기 핸들의 상태를 신호 받지 않음으로 다시 설정할 수 있습니다. AutoResetEvent 핸들은 대기 중인 단일 스레드가 해제된 후 시스템에 의해 자동으로 신호 받지 않음으로 다시 설정됩니다. 대기 중인 스레드가 없으면 이벤트 개체의 상태는 신호 받음으로 유지됩니다.

메서드 목적
WaitOne 대기 핸들을 인수로 받아들이고, 호출하는 스레드는 Set을 호출하는 다른 스레드에 의해 현재 대기 핸들이 신호 받음이 될 때까지 기다리게 합니다.
WaitAny 대기 핸들의 배열을 인수로 받아들이고, 호출하는 스레드는 지정한 대기 핸들 중 하나가 Set을 호출하여 신호를 보낼 때까지 기다리게 합니다.
WaitAll 대기 핸들의 배열을 인수로 받아들이고, 호출하는 스레드는 지정한 모든 대기 핸들이 Set을 호출하여 신호를 보낼 때까지 기다리게 합니다.
Set 지정한 대기 핸들의 상태를 신호 받음으로 설정하고 대기 중인 모든 스레드가 다시 시작되게 합니다.
Reset 지정한 이벤트의 상태를 신호 받지 않음으로 설정합니다.

Visual Basic.NET에서 일반적으로 사용하는 대기 핸들에는 뮤텍스 개체, ManualResetEvent, AutoResetEvent 등 세 종류가 있습니다. 이 중 마지막 두 가지는 주로 동기화 이벤트라고 합니다.

뮤텍스 개체

뮤텍스 개체란 한 번에 한 스레드에 의해서만 소유될 수 있는 동기화 개체입니다. 사실상 뮤텍스(mutex)라는 이름은 뮤텍스 개체의 소유권이 동시에 적용될 수 없다(MUTually EXclusive)는 점에 따라 만들어진 것입니다. 스레드는 리소스에 대한 단독 사용 권한이 필요할 때 뮤텍스 개체에 대한 소유권을 요청합니다. 한 번에 오직 한 스레드만이 뮤텍스 개체를 소유할 수 있으므로 다른 스레드가 리소스를 사용하려면 뮤텍스 개체의 소유권을 받을 때까지 기다려야 합니다.

WaitOne 메서드는 호출하는 스레드가 뮤텍스 개체의 소유권을 기다리게 합니다. 뮤텍스 개체를 소유하는 동안 스레드가 정상적으로 종료되면, 해당 뮤텍스 개체의 상태가 신호 받음으로 설정되고 대기 중인 다음 스레드가 소유권을 받게 됩니다.

동기화 이벤트

동기화 이벤트는 무언가 발생했거나 리소스가 사용 가능하게 되었음을 다른 스레드에 알릴 때 사용됩니다. 이러한 항목에 "이벤트"라는 용어를 사용하는 것 때문에 혼동되기 쉽지만, 동기화 이벤트는 다른 Visual Basic 이벤트와는 달리 실제로는 대기 핸들입니다. 다른 대기 핸들과 마찬가지로, 동기화 이벤트에는 신호 받음과 신호 받지 않음 등 두 가지 상태가 있습니다. 동기화 이벤트의 대기 메서드 중 하나를 호출하는 스레드는 다른 스레드가 Set 메서드를 호출하여 이벤트에 신호를 보낼 때까지 기다려야 합니다. 동기화 이벤트 클래스는 두 가지가 있습니다. 즉, 스레드는 Set 메서드를 사용하여 ManualResetEvent 인스턴스의 상태를 신호 받음으로 설정하고, ManualResetEvent 인스턴스의 상태는 Reset 메서드를 사용하여 또는 대기 중인 WaitOne 호출로 제어가 반환되는 경우 신호 받지 않음으로 설정합니다. AutoResetEvent 클래스의 인스턴스도 Set을 사용하여 신호 받음으로 설정할 수 있지만, 이벤트가 신호를 받은 것으로 대기 중인 스레드에 알려지는 즉시 신호 받지 않음으로 자동 복귀됩니다.

다음은 AutoResetEvent 클래스를 사용하여 스레드 풀 작업을 동기화하는 예제입니다.

Sub StartTest()
   Dim AT As New AsyncTest()
   AT.StartTask()
End Sub

Class AsyncTest
   Private Shared AsyncOpDone As New _
      System.Threading.AutoResetEvent(False)

   Sub StartTask()
      Dim Tpool As System.Threading.ThreadPool
      Dim arg As String = "SomeArg"
      Tpool.QueueUserWorkItem(New System.Threading.WaitCallback( _
         AddressOf Task), arg)  ' 작업을 대기열에 보관합니다.
      AsyncOpDone.WaitOne() ' 스레드가 Set을 호출할 때까지 기다립니다.
      MsgBox("스레드가 완료되었습니다.")
   End Sub

   Sub Task(ByVal Arg As Object)
      MsgBox("스레드가 시작됩니다.")
      System.Threading.Thread.Sleep(4000) ' 4초 동안 기다립니다.
      MsgBox("상태 개체에 포함된 문자열은 " & CStr(Arg) & "입니다.")
      AsyncOpDone.Set()   ' 스레드가 완료되었다는 신호를 보냅니다.
   End Sub
End Class

모니터 개체 및 SyncLock

모니터 개체는 코드 블록이 다른 스레드에서 실행되는 코드로 인해 중단되는 일 없이 실행되도록 하는 데 사용됩니다. 즉, 동기화된 코드 블록의 코드가 끝날 때까지는 다른 스레드의 코드가 실행될 수 없습니다. SyncLock 키워드는 Visual Basic .NET의 개체 모니터링에 대한 액세스를 단순화하는 데 사용됩니다. Visual C#.NET에서는 똑같은 방식으로 Lock 키워드를 사용합니다.

예를 들어, 데이터를 비동기식으로 반복해서 읽고 그 결과를 표시하는 프로그램을 사용하는 경우, 선점형 멀티태스킹을 사용하는 운영 체제에서 다른 스레드가 실행될 수 있는 시간을 허용하기 위해 실행 중인 스레드가 운영 체제에 의해 중단될 수 있습니다. 동기화가 이루어지지 않으면, 데이터가 표시되는 동안 그 데이터를 나타내는 개체가 다른 스레드에 의해 수정되는 경우 데이터의 부분적으로 업데이트된 상태를 볼 수 있습니다. SyncLock 문은 코드 섹션이 중단되지 않고 실행되는 것을 보장합니다. 다음은 SyncLock을 사용하여 표시 프로시저에 데이터 개체에 대한 단독 사용 권한을 제공하는 방법을 보여 주는 예제입니다.

Class DataObject
   Public ObjText As String
   Public ObjTimeStamp As Date
End Class

Sub RunTasks()
   Dim MyDataObject As New DataObject()
   ReadDataAsync(MyDataObject)
   SyncLock MyDataObject
      DisplayResults(MyDataObject)
   End SyncLock
End Sub

Sub ReadDataAsync(ByRef MyDataObject As DataObject)
   ' 데이터를 비동기로 읽고 처리하는 코드를 추가합니다.
End Sub

Sub DisplayResults(ByVal MyDataObject As DataObject)
   ' 결과를 표시하는 코드를 추가합니다.
End Sub

별도의 스레드에서 실행되고 있는 코드로 인해 중단되어서는 안 될 코드 섹션이 있을 때 SyncLock을 사용합니다.

Interlocked 클래스

Interlocked 클래스의 메서드를 사용하면 여러 메서드가 동일한 값을 업데이트하거나 비교하려고 할 때 발생할 수 있는 문제를 막을 수 있습니다. 이 클래스의 메서드를 통해 어떤 스레드의 값이든 안전하게 늘리거나 줄이거나 교환하고 비교할 수 있습니다. 다음은 Increment 메서드를 사용하여 별도의 스레드에서 실행되고 있는 프로시저들이 공유하는 변수를 증가시키는 방법을 보여 주는 예제입니다.

Sub ThreadA(ByRef IntA As Integer)
   System.Threading.Interlocked.Increment(IntA)
End Sub

Sub ThreadB(ByRef IntA As Integer)
   System.Threading.Interlocked.Increment(IntA)
End Sub

ReaderWriter 잠금

데이터가 기록되는 동안에만 리소스를 잠그고 데이터가 업데이트되는 경우가 아니면 여러 클라이언트가 데이터를 동시에 읽을 수 있게 해야 할 경우도 있습니다. ReaderWriterLock 클래스는 스레드가 리소스를 수정하는 동안 그 리소스에 대해 단독 사용 권한을 적용하지만, 리소스를 읽을 때는 단독이 아닌 사용 권한을 허용합니다. ReaderWriter 잠금은 다른 스레드들이 데이터를 업데이트할 필요가 없을 때도 기다려야 하는 단독 사용 모드의 잠금 대신 사용할 수 있는 유용한 방법입니다. 다음은 ReaderWriter를 사용하여 여러 스레드의 읽기 및 쓰기 작업을 조정하는 방법을 보여 주는 예제입니다.

Class ReadWrite
' ReadData 및 WriteData 메서드를 여러 스레드에서 안전하게 호출할 수  
' 있습니다.   
   Public ReadWriteLock As New System.Threading.ReaderWriterLock()
   Sub ReadData()
      ' 이 프로시저는 일부 소스에서 정보를 읽어옵니다.
      ' 읽기 잠금은 스레드가 읽기를 완료할 때까지 데이터가 기록되지 못하게 하면서,  
      ' 다른 스레드가 ReadData를 호출하는 것은 허용합니다.
      ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite)
      Try
         ' 여기서 읽기 작업을 실행합니다.
      Finally
         ReadWriteLock.ReleaseReaderLock() ' 읽기 잠금을 해제합니다.
      End Try
   End Sub

   Sub WriteData()
      ' 이 프로시저는 일부 소스에 정보를 기록합니다.
      ' 쓰기 잠금은 스레드가 쓰기를 완료할 때까지 데이터를 읽거나 쓰지 
      ' 못하게 합니다.
      ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite)
      Try
         ' 여기서 쓰기 작업을 실행합니다.
      Finally
         ReadWriteLock.ReleaseWriterLock() ' 쓰기 잠금을 해제합니다.
      End Try
   End Sub
End Class

교착 상태

스레드 동기화는 다중 스레드 응용 프로그램에서 매우 중요하지만, 여러 스레드가 서로를 기다리게 되는 "교착 상태"의 위험이 항상 있습니다. 사거리 정지선에서 정지된 자동차들이 각자 서로 다른 진행 방향을 기다리고 있는 상황과 마찬가지로, 교착 상태가 발생하면 모든 것이 중단됩니다. 두 말할 필요도 없이 이러한 교착 상태를 피하는 것이 중요합니다. 교착 상태에 빠지게 되는 경우가 많은 것처럼 교착 상태를 피할 수 있는 방법도 많습니다. 이 기사에서 교착 상태에 대한 모든 문제를 다룰 만한 여유는 없지만, 중요한 점은 신중한 계획이 교착 상태를 피할 수 있는 비결이라는 것입니다. 코딩을 시작하기 전에 다중 스레드 응용 프로그램을 다이어그램으로 나타내면 교착 상태를 예측할 수 있는 경우가 많습니다.

스레드 타이머

Threading.Timer 클래스는 별도의 스레드에서 작업을 주기적으로 실행하는 데 유용합니다. 예를 들어, 스레드 타이머를 사용하여 데이터베이스의 상태와 무결성을 확인하거나 중요한 파일을 백업할 수 있습니다. 다음은 2초마다 작업을 시작하고 플래그를 사용하여 타이머를 중지시키는 Dispose 메서드를 시작하는 예제입니다. 이 예제에서는 상태를 출력 창에 표시하므로, 코드를 테스트하기 전에 Control+Alt+O를 눌러 이 창이 표시되게 해야 합니다.

Class StateObjClass
' TimerTask에 대한 호출을 위한 매개 변수를 보관하는 데 사용됩니다.
      Public SomeValue As Integer
      Public TimerReference As System.Threading.Timer
      Public TimerCanceled As Boolean
End Class

Sub RunTimer()
   Dim StateObj As New StateObjClass()
   StateObj.TimerCanceled = False
   StateObj.SomeValue = 1
   Dim TimerDelegate As New Threading.TimerCallback(AddressOf TimerTask)
   ' 2초마다 프로시저를 호출하는 타이머를 만듭니다.
   ' 참고: Start 메서드는 없고, 인스턴스가 만들어지는 대로 바로 타이머 
   ' 실행이 시작됩니다.
   Dim TimerItem As New System.Threading.Timer(TimerDelegate, StateObj, _
                                               2000, 2000)
   StateObj.TimerReference = TimerItem  ' Dispose에 대한 참조를 저장합니다.
   
   While StateObj.SomeValue < 10 ' 루프를 열 번 실행합니다.
      System.Threading.Thread.Sleep(1000)  ' 1초 동안 기다립니다.
   End While

   StateObj.TimerCanceled = True  ' 타이머 개체의 Dispose를 요청합니다.
End Sub

Sub TimerTask(ByVal StateObj As Object)
   Dim State As StateObjClass = CType(StateObj, StateObjClass)
   Dim x As Integer
   ' Interlocked 클래스를 사용하여 카운터 변수를 증가시킵니다.
   System.Threading.Interlocked.Increment(State.SomeValue)
   Debug.WriteLine("새 스레드가 시작된 시간은 " & Now & "입니다.")
   If State.TimerCanceled Then    ' Dispose가 요청되었습니다.
      State.TimerReference.Dispose()
      Debug.WriteLine("완료된 시간은 " & Now & "입니다.")
   End If
End Sub

스레드 타이머는 콘솔 응용 프로그램을 개발하는 경우와 같이 System.Windows.Forms.Timer 클래스를 사용할 수 없을 때 특히 유용합니다.

작업 취소

다중 스레딩의 한 가지 장점은 작업이 다른 스레드에서 실행되는 중이라도 응용 프로그램의 사용자 인터페이스 부분이 응답을 한다는 것입니다. 플래그 기능을 하는 필드와 동기화 이벤트는 다른 스레드를 중지시키려고 한다는 것을 알리는 데 주로 사용됩니다. 다음은 동기화 이벤트를 사용하여 작업을 취소하는 예제입니다. 이 예제를 사용하려면 다음 모듈을 프로젝트에 추가합니다. 스레드를 시작하려면 StartCancel.StartTask() 메서드를 호출합니다. 실행 중인 스레드를 한 개 이상 취소하려면 StartCancel.CancelTask() 메서드를 호출합니다.

Module StartCancel
   Public CancelThread As New System.Threading.ManualResetEvent(False)
   Public ThreadisCanceled As New System.Threading.ManualResetEvent(False)
   Private Sub SomeLongTask()
      Dim LoopCount As Integer
      Dim Loops As Integer = 10
      ' 10초가 경과할 때까지 또는 CancelThread가 설정될 때까지 
      ' While 루프의 코드를 실행합니다.      
      While Not CancelThread.WaitOne(0, False) And LoopCount < Loops
         ' 여기서 어떤 종류의 작업을 실행합니다.
         System.Threading.Thread.Sleep(1000) ' 1초 동안 기다립니다.
         LoopCount += 1
      End While
      If CancelThread.WaitOne(0, False) Then
         'ManualResetEvent CancelThread가 설정된 것을 인정합니다.
         ThreadisCanceled.Set()
         MsgBox("스레드를 취소합니다.")
      Else
         MsgBox("스레드가 완료되었습니다.")
      End If
   End Sub

   Public Sub StartTask()
      ' 새 스레드를 시작합니다.
      Dim th As New System.Threading.Thread(AddressOf SomeLongTask)
      CancelThread.Reset()
      ThreadisCanceled.Reset()
      th.Start()
      MsgBox("스레드가 시작되었습니다.")
   End Sub

   Public Sub CancelTask()
      ' StartTask 프로시저로 시작된 스레드를 모두 중지합니다.
      ' 이 스레드는 스레드를 조정하기 위해 동기화 이벤트를  
      ' 보내고 받습니다.
      CancelThread.Set()  ' CancelThread를 설정하여 스레드 중지를 요구합니다.
      If ThreadisCanceled.WaitOne(4000, False) Then
         ' 스레드가 중지된 것을 인정할 때까지  
         ' 최대 4초까지 기다립니다.
         MsgBox("스레드가 중지되었습니다.")
      Else
         MsgBox("스레드를 중지하지 못했습니다.")
      End If
   End Sub
End Module

결론

다중 스레드 처리는 응답 기능이 우수한 확장 가능 응용 프로그램의 핵심입니다. Visual Basic .NET에서는 개발자들이 다중 스레드 응용 프로그램의 기능을 신속하게 이용할 수 있는 견고한 다중 스레딩 개발 모델을 지원합니다.

  • Visual Basic .NET에서는 다중 스레드 응용 프로그램을 쉽게 만들 수 있는 새로운 .NET Framework 클래스를 사용합니다.
  • 스레드가 여러 개이면 성능이 향상될 수 있지만, 스레드마다 그 스레드를 만드는 데 추가 메모리가 필요하고 스레드를 실행 상태로 유지하는 데 프로세서 시간이 필요하다는 점에서 비용이 수반되는 것을 기억하십시오.
  • 스레드 속성과 메서드는 스레드 간의 상호 작용을 제어하고 실행 중인 스레드가 리소스를 사용할 수 있게 되는 시점을 결정합니다.
  • 다중 스레딩으로 인해 매우 혼란스러워지는 것처럼 보이지만, 동기화 기술을 사용하면 실행 중인 스레드를 제어할 수 있습니다.
  • 다중 스레딩은 응용 프로그램이 복잡해지는 경우에도 사용 가능한 리소스를 효율적으로 할당함으로써 확장 가능한 응용 프로그램을 만듭니다.

이 기사에서 설명한 기술을 사용하면 프로세서를 가장 많이 사용하는 작업도 처리할 수 있는 전문적인 응용 프로그램을 개발할 수 있습니다.

추가 리소스

Guidelines for Asynchronous Programming 
비동기 프로그래밍의 개념과 .NET Framework에서 지원되는 방식을 설명합니다.
Delegates, Events, and Remoting 
대리자 및 원격의 개념과 .NET 원격에서 비동기 프로그램을 지원하는 방법에 대해 설명합니다.
Asynchronous Programming Design Pattern 
예제를 통한 비동기 프로그래밍 실습을 보여 줍니다.
Remoting Example: Asynchronous Remoting 
비동기 원격을 보여 주는 샘플이 들어 있습니다.