AddressOf 연산자의 등장으로 Visual Basic을 사용하여 이전에는 불가능했던 작업을 수행하는 방법을 설명하는 전체 업계가 저자들 사이에서 발전했습니다.
Microsoft Systems Journal 및 Visual Basic Programmer 's Journal의 최근 기사에서는 Visual Basic 프로그래머에게 Visual Basic에서 멀티 스레딩을 직접 지원하기 위해 CreateThread API 함수를 사용할 수있는 가능성을 소개했습니다.
이미 멀티 스레딩 기술에 정통한 분이라면이 섹션을 건너 뛰고 "The Threading Contract"또는 "Service Pack 2의 새로운 기능"절에서 계속하십시오.
단일 CPU가 여러 작업을 수행하는 방법은 무엇입니까?
프로그램간에 전환 할 때마다 명령 포인터 및 스택 포인터를 포함하여 내부 레지스터 값을 서로 바꿉니다. 이러한 각각의 "작업"을 실행 스레드라고합니다.
응용 프로그램에서 새로운 실행 추적을 시작하면 실행 대기열이 실행 대기 상태가됩니다.
왜 이것이 문제입니까?
MTDemo 프로젝트를 생각해보십시오.
(샘플 코드는 ftp.desaware.com/SampleCode/Articles/Thread.zip 에서 다운로드 할 수 있습니다. )
프로젝트에는 다음과 같이 두 개의 전역 변수가 포함 된 단일 코드 모듈이 들어 있습니다.
'MTDemo - 멀티 스레딩 데모 프로그램
Option Explicit
Public GenericGlobalCounter As Long
Public TotalIncrements As Long
여기에는 다음 코드가 포함 된 frmMTDemo1이라는 단일 폼이 포함되어 있습니다.
Option Explicit
Dim State As Integer
' State = 0 - Idle
' State = 1 - Loading existing value
' State = 2 - Adding 1 to existing value
' State = 3 - Storing existing value
' State = 4 - Extra delay
Dim Accumlator as loang
Const OtherCodeDelay = 10
Private Sub Command1_Click ()
Dim f as new frmMTDemo1
f.Show
End Sub
Private Sub Form_Load()
Timer1.Interval = 750 + Rnd * 500
End Sub
Private Sub Timer1_Timer()
Static otherdelay&
Select Case State
Case 0
lblOperation = "Idle"
State = 1
Case 1
lblOperation = "Loading Acc"
Accumulator = GenericGlobalCounter
State = 2
Case 2
lblOperation = "Incrementing"
Accumulator = Accumulator + 1
State = 3
Case 3
lblOperation = "Storing"
GenericGlobalCounter = Accumulator
TotalIncrements = TotalIncrements + 1
State = 4
Case 4
lblOperation = "Generic Code"
If otherdelay >= OtherCodeDelay Then
State = 0
otherdelay = 0
Else
otherdelay = otherdelay + 1
End If
End Select
UpdateDisplay
End Sub
Public Sub UpdateDisplay()
lblGlobalCounter = Str$(GenericGlobalCounter)
lblAccumulator = Str$(Accumulator)
lblVerification = Str$(TotalIncrements)
End Sub
이 프로그램은 타이머와 간단한 상태 머신을 사용하여 멀티 스레딩을 시뮬레이트합니다. State 변수는 이 프로그램이 순서대로 실행하는 다섯 가지 명령어를 설명합니다. 상태 0은 유휴 상태입니다. 상태 1은 지역 변수를 GenericGlobalCounter 글로벌 변수로 로드합니다. 상태 2는 지역 변수를 증가시킵니다. State 3은 결과를 GenericGlobalCounter 변수에 저장하고 TotalIncrements 변수 (GenericGlobalCounter 변수가 증가 된 횟수를 계산)를 증가시킵니다. 상태 4는 프로그램에서 다른 명령어를 실행하는 데 소요 된 시간을 나타내는 추가 지연을 추가합니다.
UpdateDisplay 함수는 폼의 세 개의 레이블을 업데이트하여 GenericGlobalCounter 변수의 현재 값, local accumulator 및 총 증분 횟수를 표시합니다.
각 타이머 틱은 현재 스레드의 CPU 주기를 나타냅니다. 프로그램을 실행하면 GenericGlobalCounter 변수의 값이 항상 TotalIncrements 변수와 정확히 일치 함을 알 수 있습니다. 이는 TotalIncrements 변수가 스레드가 GenericGlobalCounter를 증가시킨 횟수를 표시하기 때문에 의미가 있습니다.
그러나 Command1 단추를 클릭하고 양식의 두 번째 인스턴스를 시작하면 어떻게됩니까?
이 새 양식은 두 번째 스레드를 시뮬레이트합니다.
이제는 두 가지 양식이 동일한 GenericGlobalCounter 값을 로드하고 증분하여 저장하는 방식으로 지침이 정렬됩니다. 결과적으로, 각 스레드가 변수를 독립적으로 증가 시켰다고 믿었음에도 불구하고 값은 하나씩 만 증가합니다. 즉, 변수가 두 번 증가했지만 값은 하나씩 증가했습니다. 여러 양식을 실행하면 TotalIncrements 변수로 표시되는 증분 수가 GenericGlobalCounter 변수보다 훨씬 빠르게 증가하는 것을 빠르게 알 수 있습니다.
변수가 객체 잠금 수를 나타내는 경우 개체가 해제되어야하는 시점을 추적합니다. 리소스가 사용 중임을 나타내는 신호를 나타낼 경우 어떻게해야합니까?
이러한 유형의 문제로 인해 시스템에서 자원을 영구적으로 사용할 수 없거나 메모리에서 내부적으로 잠글 수 없거나 조기에 해제 될 수 있습니다. 응용 프로그램이 쉽게 손상 될 수 있습니다.
이 예제는 문제를 쉽게 볼 수 있도록 고안되었지만 OtherCodeDelay 변수의 값을 실험 해보십시오. 위험한 코드가 전체 프로그램에 비해 상대적으로 적을 때 문제가 덜 자주 나타날 것입니다. 이것이 좋게 들릴지도 모르지만, 그 반대는 사실입니다. 멀티 스레딩 문제는 매우 간헐적이며 발견하기가 어려울 수 있습니다. 이것은 멀티 스레딩이 신중한 설계를 요구한다는 것을 의미합니다.
멀티 스레딩 문제 방지
멀티 스레딩 문제를 피하는 비교적 쉬운 두 가지 방법이 있습니다.
전역 변수를 모두 사용하지 마십시오.
전역 변수가 사용되는 곳마다 동기화 코드를 추가하십시오.
첫 번째 방법은 Visual Basic에서 사용하는 방법입니다. Visual Basic 응용 프로그램에서 멀티 스레딩을 설정하면 모든 전역 변수가 특정 스레드에 대해 로컬이됩니다. 이는 Visual Basic에서 아파트 모델 스레딩을 구현하는 방식에 내재되어 있습니다. 나중에 자세히 설명합니다.
Visual Basic 5.0의 최초 릴리스에서는 사용자 인터페이스 요소가 없는 구성 요소에서만 멀티 스레딩을 허용했습니다. 이는 당시에 forms 엔진을 thread safe하게 할 수 있는 방법을 찾지 못했기 때문입니다. 예를 들어, Visual Basic에서 폼을 만들 때 VB는 묵시적인 전역 변수 이름을 제공합니다. 따라서 Form1이라는 폼이 있으면 별도의 폼 변수를 선언하는 대신 Form1.method를 사용하여 메서드에 직접 액세스 할 수 있습니다. 이 유형의 전역 변수는 앞서 보았던 종류의 멀티 스레딩 문제를 일으킬 수 있습니다. 폼 엔진 내에서 의심 할 여지없이 다른 문제들도 있었습니다. 멀티 스레딩을 위한 복잡한 안전성은 상당한 어려움이 될 수있는 패키지를 만드는 것입니다.
서비스 팩 2에서는 Visual Basic의 forms engine이 스레드로부터 안전 해졌습니다. 이것의 한 가지 징후는 각 스레드가 프로젝트에 정의 된 각 양식에 대해 고유 한 암시적 전역 변수를 가지고 있다는 것입니다.
서비스 팩 2의 새로운 기능
forms engine을 스레드로 안전하게 만들어 Service Pack 2를 사용하면 Visual Basic을 사용하여 다중 스레드 클라이언트 응용 프로그램을 만들 수있었습니다. 이것은 MTDemo2 프로젝트에서 시연됩니다.
(샘플 코드는 ftp.desaware.com/SampleCode/Articles/Thread.zip에서 다운로드 할 수 있습니다.)
다음과 같이 코드 모듈에서 시작 프로그램이 Sub Main으로 설정된 ActiveX Exe 프로그램으로 응용 프로그램을 정의해야합니다.
' MTDemo2 - Multithreading demo program
Option Explicit
Declare Function FindWindow Lib "user32" Alias "FindWindowA"
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Sub Main()
Dim f As frmMTDemo2
' We need this because Main is called on each new thread
Dim hwnd As Long
hwnd = FindWindow(vbNullString, "Multithreading Demo2")
If hwnd = 0 Then
Set f = New frmMTDemo2
f.Show
Set f = Nothing
End If
End Sub
처음에는 프로그램이 로드되어 응용 프로그램의 기본 양식을 표시합니다. Main 루틴은 모든 스레드의 시작 부분에서 실행되기 때문에 이것이 응용 프로그램의 첫 번째 스레드인지 여부를 알아낼 수있는 방법이 필요합니다. Visual Basic 아파트 모델은 단일 스레드에 대한 전역 변수를 유지하기 때문에 전역 변수를 사용하여 이를 찾을 수 없습니다. 이 예제에서는 FindWindow API 함수를 사용하여 예제의 기본 폼이 로드되었는지 확인합니다. 이것이 시스템 동기화 객체의 사용을 포함하여 메인 스레드인지를 확인하는 다른 방법이 있습니다. 그러나 이것도 다른 시간과 장소의 주제입니다.
멀티 스레드는 새 스레드에서 오브젝트를 작성하여 수행됩니다. 객체는 클래스 모듈을 사용하여 정의해야합니다. 이 경우 간단한 클래스 모듈은 다음과 같이 정의됩니다.
' MTDemo2 - Multithreading demo program
' Copyright ¸ 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Private Sub Class_Initialize()
Dim f As New frmMTDemo2
f.Show
Set f = Nothing
End Sub
우리는 폼 변수를 생성 한 후에 아무 것도 설정하지 않을 수 있습니다. 왜냐하면 폼을 표시하면 폼 변수가 로드 된 상태로 유지됩니다.
' MTDemo2 - Multithreading demo program
' Copyright ¸ 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Private Sub cmdLaunch1_Click()
Dim c As New clsMTDemo2
c.DisplayObjPtr Nothing
End Sub
Private Sub cmdLaunch2_Click()
Dim c As clsMTDemo2
Set c = CreateObject("MTDemo2.clsMTDemo2")
End Sub
Private Sub Form_Load()
lblThread.Caption = Str$(App.ThreadID)
End Sub
폼은 스레드 식별자를 폼의 레이블에 표시합니다. 이 폼에는 New 연산자를 사용하는 시작 버튼과 CreateObject 연산자를 사용하는 시작 버튼이 있습니다.
Visual Basic 환경에서 프로그램을 실행하면 양식이 항상 같은 스레드에 만들어져 있음을 알 수 있습니다. 이는 Visual Basic 환경이 단일 스레드 만 지원하기 때문입니다. 프로그램을 컴파일하면 CreateObject 방식으로 clsMTDemo2와 해당 양식을 새 스레드에 생성합니다.
왜 다중 스레드입니까?
많은 잠재적인 위험이 관련되어 있다면 왜 멀티 스레딩에 대한 모든 소란? 특정 상황에서 멀티 스레딩을 사용하면 성능이 크게 향상 될 수 있습니다. 어떤 경우에는 응용 프로그램 종료를 기다리는 것과 같은 특정 동기화 작업의 효율성을 향상시킬 수 있습니다. 이를 통해 응용 프로그램 아키텍처의 유연성을 높일 수 있습니다. 예를 들어 다음과 같은 코드를 사용하여 MTDemo2 응용 프로그램의 폼에 long 연산을 추가합니다.
Private Sub cmdLongOp_Click()
Dim l&
Dim s$
For l = 1 To 1000000
s = Chr$(l And &H7F)
Next l
End Sub
cmdLaunch1 단추를 사용하여 폼의 여러 인스턴스를 시작하십시오. 임의의 폼에서 cmdLongOp 버튼을 클릭하면 다른 모든 폼에서 작업이 멈추는 것을 볼 수 있습니다. 이것은 모든 양식이 단일 스레드에서 실행되고 있으며 해당 스레드가 긴 루프를 실행 중 일 때 사용 중이기 때문입니다. cmdLaunch2 단추 (컴파일 된 실행 파일 포함)를 사용하여 이것을 재현하고 양식의 cmdLongOp 단추를 누르면 해당 폼만 고정되고 다른 폼은 계속 활성화됩니다. 그것들은 그들 자신의 실행 쓰레드에서 실행 중이며, long loop 연산은 오직 자신의 쓰레드만을 묶어 놓는다. 물론 어떤 경우에도 이러한 종류의 긴 작업을 양식에 포함하면 안됩니다.
다음은 멀티 스레딩에 가치가있을 때의 간단한 요약입니다.
ActiveX EXE Server - 공유 리소스가 없습니다.
응용 프로그램간에 공유 할 것으로 예상되는 ActiveX EXE 서버가있을 때 다중 스레드는 응용 프로그램이 서로 간섭하지 않도록합니다. 한 응용 프로그램이 단일 스레드 서버의 오브젝트에 대해 긴 조작을 수행하면 다른 응용 프로그램은 서버가 사용 가능하게 될 때까지 대기 상태가됩니다. 멀티 스레딩은이 문제를 방지합니다. 그러나 공유 리소스에 대한 액세스를 중재하기 위해 ActiveX EXE 서버를 사용하려는 경우가 있습니다. 이에 대한 예는 내 ActiveX 구성 요소 개발 설명서에 설명 된 주식 시세 서버입니다. 이 경우 단일 스레드는 서버를 사용하는 모든 응용 프로그램간에 차례로 공유되는 주식 시세 서버를 실행합니다.
다중 스레드 클라이언트 - ActiveX EXE 서버로 구현
이 접근법의 간단한 형태가 MTDemo2 응용 프로그램에 나와 있습니다. 응용 프로그램이 단일 응용 프로그램 내에서 종료해야하지만 완전히 독립적으로 작동해야하는 여러 창을 지원하는 경우에 사용됩니다. 인터넷 브라우저는 각 브라우저 창이 자체 스레드에서 실행되는 다중 스레드 클라이언트의 좋은 예입니다. 멀티 스레딩은 훌륭한 이벤트 기반 디자인의 대체품으로 사용해서는 안됩니다.
멀티 스레딩 DLL
멀티 스레딩 DLL은 실제로 자체 스레드를 생성하지 않습니다. 이것은 단순히 개체를 요청하는 동일한 스레드에서 실행되는 개체를 만드는 DLL입니다. 예를 들어, DLL 인 다중 스레드 ActiveX 컨트롤은 컨트롤이 포함 된 폼과 동일한 스레드에서 실행되는 컨트롤을 만듭니다. 이렇게하면 인터넷 브라우저와 같은 다중 스레드 클라이언트의 효율성이 향상 될 수 있습니다.
다중 스레드 서버 DLL 또는 EXE
클라이언트 서버 아키텍처에서 길고 짧은 클라이언트 요청이 혼합되어 있으면 멀티 스레드가 성능을 향상시킬 수 있습니다. 그러나 모든 클라이언트 요청의 길이가 비슷한 경우 멀티 스레딩은 서버의 평균 응답 시간을 실제로 느리게 할 수 있습니다. 서버가 멀티 스레딩 (multithreading)이라는 사실로 인해 성능이 반드시 향상된다고 가정하지 마십시오.
스레딩 계약
믿거 나 말거나, 이것 모두는 소개의 방식으로되어 왔습니다. 일부 자료는 내 ActiveX 구성 요소 개발 설명서에서 훨씬 더 깊이 다룬 정보를 검토하며 다른 자료는 서비스 팩 2의 새로운 정보를 설명합니다.
이제는 COM을 사용하는 멀티 스레딩의 핵심 (모든 Visual Basic 개체 및 OLE를 사용하는 다른 Windows 응용 프로그램의 구성 요소 개체 모델을 기반으로하는 구성 요소 개체 모델)에 직접 질문하는 것을 허용 할 수 있습니다.
주어진:
멀티 스레딩은 잠재적으로 매우 위험하며 특히 멀티 스레딩을 지원하도록 설계되지 않은 멀티 스레드 코드를 시도하면 치명적인 오류와 시스템 충돌이 발생할 수 있습니다.
문제:
Visual Basic을 사용하면 단일 또는 다중 스레드 사용을 위해 설계되었는지 여부에 관계없이 개체를 만들고 단일 및 다중 스레드 환경에서 개체를 사용할 수 있습니까?
다른 말로하면 - 스레드로부터 안전하지 않도록 설계된 객체를 다중 스레드 Visual Basic 응용 프로그램에서 어떻게 사용할 수 있습니까? 다른 멀티 스레드 응용 프로그램은 어떻게 단일 스레드 Visual Basic 개체를 사용할 수 있습니까?
간단히 말해서 COM은 스레딩 문제를 어떻게 처리합니까?
COM에 대해 아는 경우 계약의 구조를 정의한다는 것을 알고 있습니다. COM 개체는 COM을 지원하는 모든 응용 프로그램이나 개체에서 성공적으로 사용할 수 있도록 특정 규칙을 따르는 데 동의합니다.
대부분의 사람들은 먼저 계약의 인터페이스 부분 (객체가 노출하는 메소드 및 속성)을 먼저 생각합니다.
하지만 COM이 계약의 일부로 스레딩을 정의한다는 사실을 알지 못할 수도 있습니다. 그리고 COM 계약의 어떤 부분과 마찬가지로 - 당신이 그것을 깨면, 당신은 매우 심각한 문제에 처해 있습니다.
Visual Basic은 자연스럽게이 부분을 숨기지 만 다음에 나오는 것을 이해하기 위해서는 COM 스레딩 모델에 대해 조금 배워야합니다.
단일 스레딩 모델 :
단일 스레드 서버는 구현할 가장 간단한 유형의 서버입니다. 이해하는 것도 가장 쉽습니다. EXE 서버의 경우 서버는 단일 스레드에서 실행됩니다. 모든 스레드는 해당 스레드에서 생성됩니다. 서버가 지원하는 각 객체에 대한 모든 메소드 호출은 해당 스레드에 도착해야합니다.
하지만 클라이언트가 다른 스레드에서 실행중인 경우에는 어떻게해야합니까? 이 경우 서버 개체에 대한 프록시 개체를 만들어야합니다. 이 프록시 객체는 클라이언트의 스레드에서 실행되며 실제 객체의 메서드와 속성을 반영합니다. 메서드가 프록시 객체에서 호출되면 객체의 스레드로 전환하는 데 필요한 모든 작업을 수행 한 다음 프록시에 전달 된 매개 변수를 사용하여 실제 객체의 메서드를 호출합니다. 당연히 이는 다소 시간이 걸리는 작업이지만 계약을 따를 수는 있습니다. 스레드를 전환하고 프록시 객체에서 실제 객체 및 뒤로 데이터를 전송하는이 프로세스를 마샬링이라고합니다. 내 ActiveX 구성 요소 개발 설명서 6 장에서 더 자세히 다룹니다.
DLL 서버의 경우 단일 스레딩 모델은 서버의 모든 개체를 만들고 서버에서 만든 첫 번째 개체와 동일한 스레드에서 호출해야합니다.
아파트 스레딩 모델
COM에서 정의한 아파트 모델 스레딩은 각 스레드마다 고유 한 전역 변수 집합이 필요하지 않습니다. 바로 Visual Basic이 아파트 모델을 구현 한 방법입니다. 아파트 모델은 각 개체가 자체 스레드에서 만들어 질 수 있지만 일단 개체가 만들어지면 해당 개체를 만든 동일한 스레드에서만 메서드와 속성을 호출 할 수 있다고 말합니다. 다른 스레드가 객체의 메소드에 액세스하려면 프록시를 통과해야합니다.
이것은 구현하기가 비교적 쉬운 모델입니다. 전역 변수를 제거하면 (Visual Basic 에서처럼) 아파트 모델은 자동으로 스레드 안전성을 부여합니다. 각 개체는 자체 스레드에서 효율적으로 실행되고 전역 변수가 없기 때문에 다른 개체 스레드와 상호 작용하지 않습니다 서로.
자유 스레딩 모델
자유 스레딩 모델은 기본적으로 모든 베팅이 꺼져 있다고 말합니다. 모든 객체는 모든 스레드에서 만들 수 있습니다. 모든 객체의 모든 메서드와 속성은 임의의 스레드에서 언제든지 호출 할 수 있습니다. 객체는 필요한 동기화 처리에 대한 모든 책임을집니다.
이것은 프로그래머가 모든 동기화를 처리해야하기 때문에 성공적으로 구현하는 가장 어려운 모델입니다. 사실, 최근까지 OLE 자체가이 스레딩 모델을 지원하지 않았습니다! 그러나 마샬링이 필요하지 않으므로 가장 효율적인 스레딩 모델입니다.
서버가 지원하는 모델은 무엇입니까?
응용 프로그램 또는 Windows 자체에서 서버가 사용하는 스레딩 모델을 어떻게 알 수 있습니까? 이 정보는 레지스트리에 포함되어 있습니다. Visual Basic에서 개체를 만들고 개체를 만들 때 레지스트리를 검사하여 프록시 개체와 마샬링이 필요한 경우를 결정합니다.
작성하는 각 오브젝트의 스레딩 요구 사항을 엄격히 준수하는 것은 클라이언트의 책임입니다.
CreateThread API
이제 Visual Basic에서 CreateThread API를 사용하는 방법에 대해 살펴 보겠습니다.
백그라운드 작업을 수행하기 위해 다른 스레드에서 실행하고 싶은 클래스가 있다고 가정 해보십시오. 이 유형의 제네릭 클래스는 다음 코드를 가질 수 있습니다 (MTDemo 3 예제에서).
' Class clsBackground ' MTDemo 3 - Multithreading example ' Copyright ¸ 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Event DoneCounting()
Dim l As Long
Public Function DoTheCount(ByVal finalval&) As Boolean
Dim s As String
If l = 0 Then
s$ = "In Thread " & App.threadid
Call MessageBox(0, s$, "", 0)
End If
l = l + 1
If l >= finalval Then
l = 0
DoTheCount = True
Call MessageBox(0, "Done with counting", "", 0)
RaiseEvent DoneCounting
End If
End Function
이 클래스는 DoTheCount 함수가 백그라운드 스레드의 연속 루프에서 반복적으로 호출 될 수 있도록 설계되었습니다. 객체 자체에 루프를 배치 할 수는 있지만, 여기에 표시된대로 객체를 설계하는 건 좋은 이유가 있음을 곧 알게 될 것입니다. 처음 DoTheCount 함수가 호출되면 MessageBox가 스레드 식별자를 표시합니다. 그러면 코드가 실행되는 스레드를 확인할 수 있습니다. MessageBox API는 API 함수가 스레드로부터 안전하기 때문에 VB MessageBox 명령 대신 사용됩니다. 카운팅이 완료되면 두 번째 MessageBox가 표시되고 작업이 완료되었음을 알리는 이벤트가 발생합니다.
백그라운드 스레드는 frmMTDemo3 형식의 다음 코드를 사용하여 시작됩니다.
Private Sub cmdCreateFree_Click()
Set c = New clsBackground
StartBackgroundThreadFree c
End Sub
The StartBackgroundThreadFree function is defined in modMTBack
module as follows:
Declare Function CreateThread Lib "kernel32" (ByVal _
lpSecurityAttributes As Long, ByVal dwStackSize As Long, _
ByVal lpStartAddress As Long, ByVal lpParameter As Long, _
ByVal dwCreationFlags As Long, _lpThreadId As Long) _
As Long
Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long
' Start the background thread for this object
' using the invalid free threading approach.
Public Function StartBackgroundThreadFree(ByVal qobj As _
clsBackground)
Dim threadid As Long
Dim hnd&
Dim threadparam As Long
' Free threaded approach
threadparam = ObjPtr(qobj)
hnd = CreateThread(0, 2000, AddressOf BackgroundFuncFree, _
threadparam, 0, threadid)
If hnd = 0 Then
' Return with zero (error)
Exit Function
End If
' We don't need the thread handle
CloseHandle hnd
StartBackgroundThreadFree = threadid
End Function
The CreateThread function takes six parameters:
lpSecurityAttributes is typically set to zero to use the default security attributes.
dwStackSize is the size of the stack. Each thread has its own stack.
lpStartAddress is the memory address where the thread starts. This must be an address of a function in a standard module obtained using the AddressOf operator.
lpParameter is a long 32 bit parameter that is passed to the function that starts the new thread.
dwCreationFlags is a 32 bit flag variable that lets you control the start of the thread (whether it is active, suspended, etc.). Details on these flags can be found in Microsoft's online 32 bit reference.
lpThreadId is a variable that is loaded with the unique thread identifier of the new thread.
이 함수는 스레드에 대한 핸들을 전달합니다.
이 경우 새 스레드에서 사용할 clsBackground 객체에 대한 포인터를 전달합니다. ObjPtr은 qobj 변수의 인터페이스 포인터 값을 검색합니다. 스레드가 작성되면 핸들은 CloseHandle 함수를 사용하여 닫힙니다. 이것은 스레드를 종료하지 않습니다. 스레드는 BackgroundFuncFree 함수가 종료 될 때까지 계속 실행됩니다. 그러나 핸들을 닫지 않으면 BackgroundFuncFree 함수가 종료 된 후에도 스레드 개체가 계속 존재하게됩니다. 시스템이 스레드에 할당 된 자원을 비울 수 있으려면 스레드에 대한 모든 핸들을 닫고 스레드를 종료해야합니다.
' A free threaded callback.
' This is an invalid approach, though it works
' in this case.
Public Function BackgroundFuncFree(ByVal param As _
IUnknown) As Long
Dim qobj As clsBackground
Dim res&
' Free threaded approach
Set qobj = param
Do While Not qobj.DoTheCount(100000)
Loop
' qobj.ShowAForm ' Crashes!
' Thread ends on return
End Function
이 함수의 매개 변수는 인터페이스 (ByVal param As IUnknown)에 대한 포인터입니다. COM에서는 모든 인터페이스가 IUnknown을 기반으로하므로이 매개 변수 유형은 원래 함수에 전달 된 인터페이스 유형에 관계없이 유효하므로이 문제를 해결할 수 있습니다. 그러나 param을 사용하기 위해서는 param을 즉시 특정 객체 유형으로 설정해야합니다. 이 경우 qobj는 StartBackgroundThreadFree 개체에 전달 된 원래 clsBackground 개체로 설정됩니다.
그런 다음 함수는 원하는 작업 (이 경우 반복 카운트)을 수행하는 동안 무한 루프를 시작합니다. 비슷한 접근법은 시스템 이벤트 (예 : 프로세스 종료)가 발생할 때까지 스레드를 일시 중단하는 대기 조작을 수행하는 것일 수 있습니다. 그런 다음 스레드는 클래스의 메소드를 호출하여 이벤트가 발생했음을 애플리케이션에 알릴 수 있습니다.
qobj 객체에 액세스하는 것은이 접근법의 자유로운 스레딩 특성 때문에 매우 빠르며 마샬링은 사용되지 않습니다.
그러나 clsBackground 객체에 폼을 표시하려고하면 응용 프로그램이 충돌한다는 것을 알 수 있습니다. 또한 완료 이벤트가 클라이언트 양식에서 절대로 발생하지 않음을 알 수 있습니다. 실제로이 접근법을 설명하는 Microsoft Systems Journal조차도이 접근 방식을 시도 할 때 작동하지 않는 몇 가지 사항이 있다는 경고를 많이 포함합니다.
이것이 Visual Basic의 결함입니까?
이 유형의 스레딩을 사용하여 응용 프로그램을 배포하려고 시도한 일부 사용자는 VB5 서비스 팩 2로 업그레이드 한 후 해당 응용 프로그램이 실패한 것으로 나타났습니다.
이것은 Microsoft가 이전 버전과의 호환성을 제대로 제공하지 못했음을 의미합니까?
두 가지 질문에 대한 대답은 다음과 같습니다.
Microsoft 또는 Visual Basic 문제가 아닙니다.
문제는 위의 코드가 가비지라는 점입니다.
문제는 간단합니다. Visual Basic은 단일 스레드 모델과 아파트 모델 모두에서 개체를 지원합니다. 다시 말해 보겠습니다. Visual Basic 개체는 단일 스레드 또는 아파트 모델 개체로 올바르게 작동한다는 COM 계약 아래의 문을 작성하는 COM 개체입니다. 즉, 각 객체는 해당 객체를 만든 동일한 스레드에서 모든 메서드 호출이 발생할 것을 기대합니다.
위에 표시된 예는이 규칙을 위반합니다.
그것은 COM 계약을 위반합니다.
이것은 무엇을 의미 하는가?
즉, Visual Basic이 업데이트되면 개체의 동작이 변경 될 수 있습니다.
즉, 다른 개체 또는 양식에 액세스하려는 개체의 시도가 비참하게 실패 할 수 있으며 해당 개체가 업데이트 될 때 오류 모드가 변경 될 수 있음을 의미합니다.
즉, 다른 객체가 추가, 삭제 또는 수정 될 때 현재 작동하는 코드가 갑자기 실패 할 수 있습니다.
즉, 응용 프로그램의 동작을 특성화하거나 특정 환경에서 작동하는지 또는 작동해야 하는지를 예측하는 것은 불가능합니다.
즉, 해당 시스템에서 코드가 작동하는지 예측할 수 없으며 사용중인 운영 체제, 사용중인 프로세서 수 및 기타 시스템 구성 문제에 따라 동작이 다를 수 있음을 의미합니다.
COM 계약을 위반하면 개체가 서로 또는 클라이언트와 성공적으로 통신 할 수 있도록 COM의 기능으로 더 이상 보호받지 못합니다.
이 접근법은 연금술을 프로그래밍하는 것입니다. 그것은 무책임하며 프로그래머는 절대 사용해서는 안됩니다. 기간.
CreateThread API 재검토
이제 일부 기사에 등장한 CreateThread API 접근법이 쓰레기 인 이유를 보여 드렸으므로 사실을 올바르게 작성하고 실제로이 API를 안전하게 사용할 수있는 방법을 보여주는 것이 공정한 것입니다.
트릭은 간단합니다. COM 스레딩 계약을 준수해야합니다. 이것은 더 많은 작업을 필요로하지만, 결과는 지금까지 신뢰성이 입증되었습니다.
MTDemo3 샘플은 frmMTDemo3 형식에서 다음과 같이 아파트 모델 백그라운드 클래스를 시작하는 다음 코드를 사용하여이를 보여줍니다.
Private Sub cmdCreateApt_Click()
Set c = New clsBackground
StartBackgroundThreadApt c
End Sub
지금까지는 자유 스레드 접근 방식과 매우 유사합니다.
클래스의 인스턴스를 생성하고 그 클래스를 시작하는 함수에 전달합니다.
배경 스레드입니다. 다음 코드는 modMTBack 모듈에 나타납니다.
' Structure to hold IDispatch GUID
Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Public IID_IDispatch As GUID
Declare Function CoMarshalInterThreadInterfaceInStream _
Lib "ole32.dll" (riid As GUID, ByVal pUnk As IUnknown, _
ppStm As Long) As Long
Declare Function CoGetInterfaceAndReleaseStream Lib _
"ole32.dll" (ByVal pStm As Long, riid As GUID, _
pUnk As IUnknown) As Long
Declare Function CoInitialize Lib "ole32.dll" _
(ByVal pvReserved As Long) As Long
Declare Sub CoUninitialize Lib "ole32.dll" ()
' Start the background thread for this object
' using the apartment model
' Returns zero on error
Public Function StartBackgroundThreadApt(ByVal qobj As _ clsBackground) Dim threadid As Long Dim hnd&, res& Dim threadparam As Long Dim tobj As Object Set tobj = qobj ' Proper marshaled approach
InitializeIID
res = CoMarshalInterThreadInterfaceInStream _ (IID_IDispatch, qobj, threadparam) If res <> 0 Then StartBackgroundThreadApt = 0 Exit Function End If hnd = CreateThread(0, 2000, AddressOf _ BackgroundFuncApt, threadparam, 0, threadid) If hnd = 0 Then ' Return with zero (error) Exit Function End If ' We don't need the thread handle CloseHandle hnd StartBackgroundThreadApt = threadid End Function
StartBackgroundThreadApt 함수는 free 스레딩보다 약간 복잡합니다.
첫 번째 새 함수는 InitializeIID입니다. 이 함수는 다음 코드를 다룹니다.
' Initialize the GUID structure
Private Sub InitializeIID()
Static Initialized As Boolean
If Initialized Then Exit Sub
With IID_IDispatch
.Data1 = &H20400
.Data2 = 0
.Data3 = 0
.Data4(0) = &HC0
.Data4(7) = &H46
End With
Initialized = True
End Sub
The CoMarshalInterThreadInterfaceInStream performs an interesting task. It collects all of the information needed to create a proxy for a specified interface and loads it into a stream object. In this example we use the IDispatch interface because we know that every Visual Basic class supports IDispatch, and we know that IDispatch marshalling support is built into Windows -- so this code will always work. We then pass the stream object to the new thread. This object is designed by Windows to be transferable between threads in exactly this manner, so we can pass it safely to the CreateThread function. The rest of the StartBackgroundThreadApt function is identical to the StartBackgroundThreadFree function.
The BackgroundFuncApt function is also more complex than the free threaded equivalent as shown below:
인터페이스 식별자를 고유하게 식별하고 인터페이스하는 16 바이트 구조가 필요합니다. 특히 IDispatch 인터페이스의 인터페이스 식별자가 필요합니다 (IDispatch에 대한 자세한 내용은 Developing ActiveX Components 책에서 찾을 수 있습니다). InitializeIID 함수는 단순히 IID_IDispatch 구조를 IDispatch 인터페이스 식별자의 올바른 값으로 초기화합니다. 이 값은 원래 레지스트리 뷰어 유틸리티를 사용하여 얻습니다.
왜이 식별자가 필요한가요?
COM 스레딩 계약을 따르려면 clsBackground 객체에 대한 프록시 객체를 만들어야합니다. 원래 개체 대신 새 스레드에 프록시 개체를 전달해야합니다. 프록시 객체의 새 스레드에 의한 호출은 현재 스레드로 마샬링됩니다.
CoMarshalInterThreadInterfaceInStream은 흥미로운 작업을 수행합니다. 지정된 인터페이스에 대한 프록시를 작성하는 데 필요한 모든 정보를 수집하여 이를 스트림 오브젝트에 로드합니다. 이 예제에서는 모든 Visual Basic 클래스가 IDispatch를 지원한다는 것을 알고 있기 때문에 IDispatch 인터페이스를 사용합니다. Windows에 IDispatch 마샬링 지원이 내장되어 있으므로 이 코드가 항상 작동합니다. 그런 다음 스트림 객체를 새 스레드로 전달합니다. 이 객체는 Windows에서 스레드간에 정확하게 이 방식으로 전송 가능하도록 설계되었으므로 안전하게 CreateThread 함수에 전달할 수 있습니다. StartBackgroundThreadApt 함수의 나머지는 StartBackgroundThreadFree 함수와 동일합니다.
BackgroundFuncApt 함수는 아래 표시된 free threaded equivalent 보다 더 복잡합니다.
' A correctly marshaled apartment model callback.
' This is the correct approach, though slower.
Public Function BackgroundFuncApt(ByVal param As Long) _ As Long Dim qobj As Object Dim qobj2 As clsBackground Dim res& ' This new thread is a new apartment, we must ' initialize OLE for this apartment ' (VB doesn't seem to do it) res = CoInitialize(0)
' Proper apartment modeled approach
res = CoGetInterfaceAndReleaseStream(param,
IID_IDispatch, qobj)
Set qobj2 = qobj
Do While Not qobj2.DoTheCount(10000)
Loop
qobj2.ShowAForm
' Alternatively, you can put a wait function here,
' then call the qobj function when the wait is satisfied
' All calls to CoInitialize must be balanced
CoUninitialize
End Function
첫 번째 단계는 새 스레드에 대한 OLE 하위 시스템을 초기화하는 것입니다. 이것은 마샬링 코드가 올바르게 작동하는 데 필요합니다. CoGetInterfaceAndReleaseStream은 원본 clsBackground 개체에 대한 프록시 개체를 만들고 다른 스레드에서 데이터를 전송하는 데 사용되는 스트림 개체를 해제합니다. 새 개체에 대한 IDispatch 인터페이스가 qobj 변수에로드됩니다. 이제는 다른 인터페이스를 얻을 수 있습니다. 프록시 개체는 지원할 수있는 모든 인터페이스에 대해 데이터를 올바르게 마샬링합니다.
이제 루프가 객체 자체 대신에이 함수에 왜 배치되는지 알 수 있습니다. qobj2.DoTheCount 함수를 처음 호출하면 코드가 원래 스레드에서 실행되고 있음을 알 수 있습니다! 객체에 대한 메소드를 호출 할 때마다 실제로 프록시 객체에 대한 메소드를 호출합니다. 현재 스레드가 일시 중단되고 메서드 요청이 원래 스레드에 마샬링되며 개체를 만든 동일한 스레드의 원래 개체에서 메서드가 호출됩니다. 루프가 객체에 있다면 원래 스레드를 정지시킬 것입니다.
이 방법에 대한 좋은 점은 모든 것이 작동한다는 것입니다. clsBackground 객체는 양식을 표시하고 안전하게 이벤트를 발생시킬 수 있습니다. 물론 폼과 클라이언트와 동일한 스레드에서 실행 중일 수 있습니다. 이 접근법의 단점은 물론 느리다는 것입니다. 스레드 스위치 및 마샬링은 상대적으로 느린 작업입니다. 실제로 여기에 표시된 것처럼 백그라운드 작업을 구현하고 싶지는 않습니다.
그러나 BackgroundFuncApt 함수 자체에 백그라운드 작업을 배치 할 수 있다면이 접근법은 매우 잘 작동 할 수 있습니다! 예를 들어 백그라운드 스레드가 백그라운드 계산 또는 시스템 대기 작업을 수행하도록 할 수 있습니다. 완료되면 객체에서 클라이언트에서 이벤트를 발생시키는 메소드를 호출 할 수 있습니다. 백그라운드 함수에서 수행되는 작업량과 비교하여 메소드 호출 수가 적 으면 매우 효과적인 결과를 얻을 수 있습니다.
객체를 사용할 필요가없는 백그라운드 작업을 수행하려면 어떻게해야합니까? 분명히 COM 스레딩 계약의 문제점은 사라집니다. 그러나 다른 문제가 나타납니다. 백그라운드 스레드가 전경 스레드로 완료 신호를 보내는 방법은 무엇입니까? 데이터를 어떻게 교환 할 것인가? 두 스레드는 어떻게 동기화됩니까? API 호출을 적절하게 사용하면 이러한 모든 작업이 가능합니다. 이벤트, 뮤텍스, 세마포 및 대기 타이머와 같은 동기화 개체에 대한 내용은 Win32 API에 대한 Visual Basic 5.0 프로그래머 용 가이드를 참조하십시오.
또한 프로세스간에 데이터를 교환하는 데 도움이 될 수있는 메모리 매핑 파일의 예가 포함되어 있습니다. 전역 변수를 사용하여 데이터를 교환 할 수도 있지만 Visual Basic에서는이 동작을 보장하지 않습니다 (즉, 현재 작동하더라도 미래에 작동 할 것이라는 보장이 없음) . 이 경우 데이터 교환을 위해 API 기반 기술을 사용하는 것이 좋습니다. 그러나 여기에 표시된 객체 기반 접근법의 장점은 스레드간에 데이터를 교환하는 문제를 간단하게 처리 할 수 있다는 것입니다.
결론 # 1
저는 경험이 풍부한 Windows 프로그래머로부터 OLE가 지금까지 배워야 할 가장 어려운 기술이라고 들었습니다. 나는 동의한다. 그것은 광대 한 주제이고 그것의 부분은 이해하기가 매우 어렵습니다. Visual Basic은 언제나처럼 복잡성의 대부분을 숨 깁니다.
"팁 및 기술"접근 방식을 사용하는 멀티 스레딩과 같은 고급 기술을 활용할 유혹이 강합니다. 이 유혹은 때로는 특정 솔루션을 제시하는 기사에 의해 권장되며, 기술을 잘라내어 자신의 응용 프로그램에 넣을 수 있습니다.
Windows API에 대한 원래의 Visual Basic Programmer 's Guide를 작성했을 때 나는 그 접근법을 명시적으로 부인했습니다. 나는 당신이 이해하지 못하는 어플리케이션에 코드를 포함시키는 것이 일반적으로 무책임하며 실제 지식은 얻을 수는 없지만 단기간에 노력할 가치가 있다는 것을 느꼈습니다.
따라서 내 API 서적은 신속한 응답과 쉬운 솔루션을 제공하지 않고, 프로그래머가 가장 진보 된 기술조차도 지능적으로 정확하게 적용 할 수 있도록 API 사용법을 가르쳐서 책에 표시된 것 이상으로 빠르게 설계되었습니다. 필자는 액티브 X, ActiveX 및 COM 및 객체 지향 프로그래밍의 원리를 구현 세부 사항에 들어가기 전에 논의하는 데 많은 시간을 할애하면서 ActiveX 구성 요소 개발에 대한 저서에 이와 동일한 접근 방식을 적용했습니다.
Visual Basic 분야에서의 많은 경력과 Desaware의 사업의 대부분은 Visual Basic 프로그래머의 고급 기술을 가르쳐 왔습니다. 스레딩 기술을 뒤로 잡아서 이 원리를 배신한다는 비판을 통해 이 기사를 읽은 독자는 그 요점을 놓쳤다.
예, 저는 첨단 기술을 가르치고 시연합니다 - 그러나 나는 더 큰 그림을 그리워하지 않습니다. 내가 가르치는 고급 기술은 Windows의 규칙 및 사양과 일치해야 합니다. 가능한 한 사용하기에 안전해야 합니다. 그들은 장기적으로 지지 할 수 있어야 합니다. Windows 또는 Visual Basic이 변경 될 때 중단되지 않아야합니다.
나는 부분적인 성공만을 주장 할 수 있습니다 - 때로는 그리기가 힘들며 Microsoft는 언제든지 규칙을 자유롭게 변경할 수 있습니다. 그러나 나는 항상 그것을 염두에두고 사람들이 내가 한계를 밀고 있다고 생각하는 곳을 경고하려고 노력합니다.
여기에 표시된이 멀티 스레딩 토론이 기본 기술을 잘 이해하지 않고 "간단한 기술"을 적용 할 때의 위험성을 보여주기를 바랍니다.
나는 CreateThread 사용법의 아파트 모델 버전이 절대적으로 정확하다는 것을 약속 할 수 없다. 단지 그것이 나의 이해와 테스트에서 최상으로 안전하다는 것이다.
내가 놓친 다른 요소가있을 수 있습니다 - OLE는 실제로 복잡하고 OLE DLL과 Visual Basic 자체가 계속 변경됩니다. 필자가 가장 잘 알고있는 바로는 COM 규칙을 따르고 있으며 경험적 증거에 따르면 Visual Basic 5.0의 런타임은 표준 모듈에서 백그라운드 스레드 코드를 실행하기에 충분히 안전합니다.
VB6에 관한 후속편 # 1-
다음 의견은 VB6 릴리스 이후에 작성되었습니다.
한숨 ... 많은 독자들이 나의 원래 지점을 놓친 것 같습니다. 아이디어는 VB 프로그래머가 Visual Basic에서 CreateThread를 사용하도록 장려하는 것이 아닙니다. Visual Basic에서 CreateThread를 사용하지 않아야하는 이유를 명확하고 정확하게 설명했습니다.
따라서 Visual Basic 6 이 VB5보다 스레드 세이프 (thread-safe)가 적으면 이 기사에서 언급한 샘플 프로그램을 깨고 무엇을 할 수 있습니까? 내가 다시 가서 샘플을 수정하고 VB6에서 작동하도록 만들 수 있다고 가정합니다. 그러나 이후 버전의 Visual Basic에서도 같은 문제가 발생할 수 있습니다.
Visual Basic은 ActiveX 서버의 멀티 스레드 클라이언트를 포함한 멀티 스레딩에 대한 훌륭한 지원을 제공합니다 (COM / ActiveX 구성 요소 개발 안내서 최신판에서 매우 자세하게 설명되어 있습니다). Visual Basic 문서에서 정의한 규칙을 준수하고 Visual Basic에서 CreateThread API를 사용하지 않는 것이 좋습니다.
CreateThread를 계속 추구하는 사람들은 Declare 문을 모두 제거하고 대신 형식 라이브러리를 사용해야합니다. 이 방법으로 문제를 해결할 것이라고 약속하지는 않지만 초기 테스트에서 필요한 첫 번째 단계임을 나타냅니다.
후속편 # 2 - SpyWorks 6.2 관련
2000 년 4 월 ...
결국 사람들에게 CreateThread를 사용하지 말라고 말하는 것은 결국 만족스러운 대답이 아니었습니다. 백그라운드 작업과 VB DLL의 NT 동기화 개체를 사용하여 스레드를 만드는 방법에 대한 정보를 계속 요청했습니다. 여러분이 아마 알고 있듯이, 사람들이 Visual Basic으로 쉽지 않거나 불가능한 것을 요청하면 조만간 SpyWorks의 새로운 기능으로 나타납니다. 버전 6.2에서는 dwBackThread라는 구성 요소를 포함 시켰습니다.이 구성 요소를 사용하면 VB DLL의 객체를 자체 스레드에 생성한 다음 백그라운드 작업을 트리거 할 수 있습니다. 이 구성 요소는 필요한 모든 마샬링 및 정리 작업을 처리하므로 멀티 스레딩을 수행할 때와 마찬가지로 안전합니다. 가장 중요한 것은 - 모든 COM 스레딩 규칙을 따르므로 갑자기 사용하지 않는 VB 나 구성 요소에 대해 걱정할 필요가 없습니다. 자세한 내용은 SpyWorks의 제품 페이지를 참조하십시오 .ÿ