Используя несколько потоков, у нас появляется возможность вызывать методы асинхронно, с сохранением синхронности; т.е. мы сможем вызывать методы как и в отдельном потоке, так и в своем. Например методы требующие большой вычислительной загрузки стоит вызывать асинхронно и получать, при окончании, уведомление в виде события. Такие методы (свойства) которые работают быстро, можно вызывать синхронно.
Одна из проблем создания потока на VB6 в Standart EXE, является невозможность использования вызовов WinAPI функций через Declare. В отличии от функций задекларированных в библиотеки типов и попадающих в импорт, Declared-функции после каждого вызова устанавливают свойство объектной переменной Err.LastDllError. Делается это посредством вызова функции __vbaSetSystemError из MSVBVM. Объект Err, является потокозависимым, а ссылка на него находится в локальном хранилище потока (TLS). Для каждого потока должен создаваться свой объект Err, иначе при вызове функции __vbaSetSystemError, рантайм запросит ссылку из хранилища, а у нас ее там нет (точнее там 0) и произойдет чтение по неправильному адресу, как следствие вылет.
Чтобы предотвратить такое поведение, можно декларировать функции в tlb, тогда не будет вызываться функция __vbaSetSystemError. Также можно инициализировать Err объект, например создать объект из DLL в новом потоке, тогда рантайм инициализирует этот объект сам. Но для создания нового объекта, нужно сначала инициализировать поток для работы с COM, для этого нужно вызвать CoInitialize(Ex), но мы не можем вызывать функции. Можно ее задекларировать в tlb (только ее одну), тогда все честно; также можно ее вызвать из ассемблерного переходника к примеру или любым другим способом. Я всегда иду по другому. Зачем мне LastDllError? Я могу просто напросто сам вызвать GetLastError когда мне надо. Поэтому я просто нахожу адрес функции __vbaSetSystemError и пишу первой инструкцией выход из процедуры (ret). Это конечно не так красиво, но зато надежно и быстро. Можно сделать так только для одной функции CoInitialize, а потом восстановить __vbaSetSystemError.
Теперь мы можем вызывать Declared функции в новом потоке, что дает нам безграничные возможности. После создания объекта (CreateObject), мы можем вызывать его методы, свойства, получать от него события и т.д., но просто так ссылку между потоками нельзя передавать, т.к. могут возникнуть ошибки из-за одновременного доступа к данным и т.п. Для передачи ссылки между потоками существует маршалинг. Мы будем использовать универсальный маршаллер, т.к. у нас ActiveX DLL имеет в себе библиотеку типов. Принцип работы я подробно расписывать не буду, для этого есть в сети много статей. Общий смысл в том, что вместо прямого вызова метода объекта, используется RPC запрос в другой компьютер/процесс/поток. Для обработки запросов нужно использовать цикл обработки сообщений, и раз так вышло, то и связь между потоками сделаем через сообщения.
Для теста я написал простейшую ActiveX DLL с помощью которой можно скачать файл из сети, которая имеет несколько методов и генерирует события.
- Код: Выделить всё
- ' Класс MultithreadDownloader - класс загрузчика
 ' © Кривоус Анатолий Анатольевич (The trick), 2014
 
 Option Explicit
 
 Public Enum ErrorCodes
 OK
 NOT_INITIALIZE
 ERROR_CREATING_DST_FILE
 End Enum
 
 Private Declare Function InternetCloseHandle Lib "wininet" (ByRef hInternet As Long) As Boolean
 Private Declare Function InternetOpen Lib "wininet" Alias "InternetOpenW" (ByVal lpszAgent As Long, ByVal dwAccessType As Long, ByVal lpszProxy As Long, ByVal lpszProxyBypass As Long, ByVal dwFlags As Long) As Long
 Private Declare Function InternetOpenUrl Lib "wininet" Alias "InternetOpenUrlW" (ByVal hInternet As Long, ByVal lpszUrl As Long, ByVal lpszHeaders As Long, ByVal dwHeadersLength As Long, ByVal dwFlags As Long, ByRef dwContext As Long) As Long
 Private Declare Function InternetReadFile Lib "wininet" (ByVal hFile As Long, lpBuffer As Any, ByVal dwNumberOfBytesToRead As Long, ByRef lpdwNumberOfBytesRead As Long) As Integer
 Private Declare Function HttpQueryInfo Lib "wininet" Alias "HttpQueryInfoW" (ByVal hRequest As Long, ByVal dwInfoLevel As Long, lpBuffer As Any, ByRef lpdwBufferLength As Long, ByRef lpdwIndex As Long) As Long
 Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileW" (ByVal lpFileName As Long, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, lpSecurityAttributes As Any, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Long) As Long
 Private Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, lpOverlapped As Any) As Long
 Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
 
 Private Const INTERNET_OPEN_TYPE_PRECONFIG As Long = 0
 Private Const INTERNET_FLAG_RELOAD As Long = &H80000000
 Private Const HTTP_QUERY_CONTENT_LENGTH As Long = 5
 Private Const HTTP_QUERY_FLAG_NUMBER As Long = &H20000000
 Private Const CREATE_ALWAYS As Long = 2
 Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80
 Private Const INVALID_HANDLE_VALUE As Long = -1
 Private Const GENERIC_WRITE As Long = &H40000000
 
 Public Event Complete()
 Public Event Error(ByVal Code As Long)
 Public Event Progress(ByVal Size As Currency, ByVal TotalSize As Currency, cancel As Boolean)
 
 Private mBufferSize As Long
 Private mError As ErrorCodes
 
 Dim hInternet As Long
 
 Public Property Get ErrorCode() As ErrorCodes
 ErrorCode = mError
 End Property
 
 Public Property Get BufferSize() As Long
 BufferSize = mBufferSize
 End Property
 Public Property Let BufferSize(ByVal Value As Long)
 If Value > &H1000000 Or Value < &H400 Then Err.Raise vbObjectError, "MultithreadDownloader", "Wrong buffer size": Exit Property
 mBufferSize = Value
 End Property
 
 Public Sub Download(URL As String, Filename As String)
 Dim hFile As Long
 Dim hDst As Long
 Dim fSize As Currency
 Dim total As Long
 Dim prgSize As Currency
 Dim cancel As Boolean
 Dim buf() As Byte
 
 If hInternet = 0 Then mError = NOT_INITIALIZE: RaiseEvent Error(mError): Exit Sub
 hFile = InternetOpenUrl(hInternet, StrPtr(URL), 0, 0, INTERNET_FLAG_RELOAD, 0)
 
 If hFile = 0 Then mError = Err.LastDllError: RaiseEvent Error(mError): Exit Sub
 
 If HttpQueryInfo(hFile, HTTP_QUERY_CONTENT_LENGTH Or HTTP_QUERY_FLAG_NUMBER, fSize, 8, 0) Then
 hDst = CreateFile(StrPtr(Filename), GENERIC_WRITE, 0, ByVal 0&, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0)
 If hDst = INVALID_HANDLE_VALUE Then mError = ERROR_CREATING_DST_FILE: RaiseEvent Error(mError): Exit Sub
 ReDim buf(mBufferSize - 1)
 Do
 If InternetReadFile(hFile, buf(0), mBufferSize, total) = 0 Then
 mError = Err.LastDllError
 RaiseEvent Error(mError)
 InternetCloseHandle hFile
 Exit Sub
 End If
 WriteFile hDst, buf(0), total, 0, ByVal 0&
 prgSize = prgSize + CCur(total) / 10000@
 RaiseEvent Progress(prgSize, fSize, cancel)
 Loop While (total = mBufferSize) And Not cancel
 CloseHandle hDst
 RaiseEvent Complete
 Else
 mError = Err.LastDllError
 RaiseEvent Error(mError)
 End If
 InternetCloseHandle hFile
 mError = OK
 End Sub
 
 Private Sub Class_Initialize()
 ' Инициализация WinInet
 hInternet = InternetOpen(StrPtr(App.ProductName), INTERNET_OPEN_TYPE_PRECONFIG, 0, 0, 0)
 mBufferSize = &H10000
 End Sub
 
 Private Sub Class_Terminate()
 ' Деинициализация
 If hInternet Then InternetCloseHandle hInternet
 End Sub
Код в принципе простой, если прочитать описание API функций. При вызове метода Download, начинает выполнятся загрузка, периодически (зависит от размера буфера) генерируется событие Progress. При ошибке генерируется событие Error, и при окончании Complete. BufferSize - задает размер буфера, при заполнении которого генерируется событие. Код демонстрационный и содержит недочеты.
Класс я назвал MultithreadDownloader, а библиотеку MTDownloader, соответственно ProgID этого объекта MTDownloader.MultithreadDownloader. После компиляции получаем описание интерфейсов через OleView, PEExplorer и т.п. В моем примере CLSID = {20FAEF52-0D1D-444B-BBAE-21240219905B}, IID = {DF3BDB52-3380-4B78-B691-4138300DD304}. Также я поставил галочку RemoteServerFiles чтобы получить на выходе библиотеку типов для нашей DLL, и будем подключать ее вместо DLL для гарантированного запуска приложения.
Код клиентского приложения:
Форма -
- Код: Выделить всё
- ' frmDownloader.frm - форма загрузчика
 ' © Кривоус Анатолий Анатольевич (The trick), 2014
 
 Option Explicit
 
 ' Объявляем объектную переменную с подпиской на события
 Dim WithEvents Downloader As MTDownloader.MultithreadDownloader
 
 Dim param As ThreadData ' Данные потока
 Dim tid As Long ' ИД потока
 Dim hThread As Long ' Описатель потока
 Dim mCancel As Boolean ' Если отмена закачки
 Dim mActive As Boolean ' Если активна закачка
 
 ' // Отмена
 Private Sub cmdCancel_Click()
 mCancel = True
 End Sub
 
 ' // Скачать файл
 Private Sub cmdDownload_Click()
 Dim ptr As Long
 ' Проверяем, идет ли уже вызов
 If WaitForSingleObject(param.hEvent, 0) = WAIT_OBJECT_0 Then
 ' Упаковываем параметры
 ptr = MT_DOWNLOAD_packParam(txtURL.Text, txtPath.Text)
 If ptr Then
 mCancel = False
 mActive = True
 ' Очистка прогрессбара
 picProgress.Cls
 ' Отправляем запрос на асинхронный вызов метода в другом потоке
 PostThreadMessage tid, WM_MT_DOWNLOAD, 0, ptr
 Else
 MsgBox "Не удалось упаковать параметры", vbCritical
 End If
 Else
 MsgBox "Скачивание еще идет", vbInformation
 End If
 End Sub
 
 ' // Окончание загрузки
 Private Sub Downloader_Complete()
 mActive = False
 MsgBox "Загрузка завершена"
 End Sub
 
 ' // Ошибка загрузки
 Private Sub Downloader_Error(ByVal Code As Long)
 mActive = False
 MsgBox "Ошибка"
 End Sub
 
 ' // Прогресс
 Private Sub Downloader_Progress(ByVal Size As Currency, ByVal TotalSize As Currency, cancel As Boolean)
 Dim sVal As String
 Dim wTxt As Single
 
 cancel = mCancel
 picProgress.Cls
 picProgress.Line (0, 0)-(Size / TotalSize, 1), vbRed, BF
 
 sVal = Format(Size / TotalSize, "##0%")
 wTxt = picProgress.TextWidth(sVal)
 picProgress.CurrentX = (1 - wTxt) / 2
 picProgress.CurrentY = 0
 picProgress.Print sVal
 picProgress.Refresh
 
 End Sub
 
 Private Sub Form_Initialize()
 InitCommonControlsEx 3435973.8623@
 End Sub
 
 Private Sub Form_Load()
 Dim iid As UUID
 Dim obj As MTDownloader.MultithreadDownloader
 ' Удаляем вылет Declared функций
 RemoveLastDllError
 ' Создаем синхронизирующий объект
 param.hEvent = CreateEvent(ByVal 0&, 1, 0, 0)
 ' Создаем поток
 hThread = CreateThread(ByVal 0&, 0, AddressOf ThreadProc, ByVal VarPtr(param), 0, tid)
 If hThread = 0 Then
 MsgBox "Не удалось создать поток", vbCritical
 End
 End If
 ' Ждем инициализацию объекта
 WaitForSingleObject param.hEvent, INFINITE
 ' Если успешно
 If param.IStream Then
 ' Преобразуем интерфейс в бинарную форму
 IIDFromString StrPtr(IID_MultithreadDownloader), iid
 ' Получаем отмаршаленный указатель на объект
 CoGetInterfaceAndReleaseStream param.IStream, iid, obj
 Set Downloader = obj
 Else
 MsgBox "Не удалось создать объект", vbCritical
 End
 End If
 ' Проверяем корректность инициализации объекта
 If Downloader.ErrorCode = NOT_INITIALIZE Then
 MsgBox "Объект не инициализирован", vbCritical
 End
 End If
 ' Задаем размер буфера в 100 кб
 Downloader.BufferSize = &H10000
 End Sub
 
 Private Sub Form_Unload(cancel As Integer)
 If mActive Then
 MsgBox "Идет загрузка"
 cancel = True
 Exit Sub
 End If
 If tid Then
 ' Освобождаем объект
 Set Downloader = Nothing
 ' Отправляем запрос на завершение потока
 PostThreadMessage tid, WM_QUIT, 0, 0
 ' Ждем завершение, т.к. поток ссылается на param
 WaitForSingleObject hThread, INFINITE
 ' Закрываем описатели
 CloseHandle hThread
 CloseHandle param.hEvent
 End If
 End Sub
Модуль -
- Код: Выделить всё
- ' modMain.bas - главный модуль загрузчика
 ' © Кривоус Анатолий Анатольевич (The trick), 2014
 
 Option Explicit
 
 Public Type POINTAPI
 x As Long
 y As Long
 End Type
 Public Type msg
 hwnd As Long
 message As Long
 wParam As Long
 lParam As Long
 time As Long
 pt As POINTAPI
 End Type
 Public Type UUID
 Data1 As Long
 Data2 As Integer
 Data3 As Integer
 Data4(0 To 7) As Byte
 End Type
 
 Public Declare Function GetMessage Lib "user32" Alias "GetMessageW" (lpMsg As msg, ByVal hwnd As Long, ByVal wMsgFilterMin As Long, ByVal wMsgFilterMax As Long) As Long
 Public Declare Function TranslateMessage Lib "user32" (lpMsg As msg) As Long
 Public Declare Function DispatchMessage Lib "user32" Alias "DispatchMessageW" (lpMsg As msg) As Long
 Public Declare Function PostThreadMessage Lib "user32" Alias "PostThreadMessageW" (ByVal idThread As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
 Public Declare Function GetMem1 Lib "msvbvm60" (Src As Any, Dst As Any) As Long
 Public Declare Function GetMem4 Lib "msvbvm60" (Src As Any, Dst As Any) As Long
 Public Declare Function VirtualProtect Lib "kernel32" (ByVal lpAddress As Long, ByVal dwSize As Long, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
 Public Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleW" (ByVal lpModuleName As Long) As Long
 Public Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, ByVal lpProcName As String) As Long
 Public Declare Function CreateThread Lib "kernel32" (lpThreadAttributes As Any, ByVal dwStackSize As Long, ByVal lpStartAddress As Long, lpParameter As Any, ByVal dwCreationFlags As Long, lpThreadId As Long) As Long
 Public Declare Function CoInitialize Lib "ole32" (pvReserved As Any) As Long
 Public Declare Function CoUninitialize Lib "ole32" () As Long
 Public Declare Function CLSIDFromString Lib "ole32" (ByVal lpsz As Long, pclsid As UUID) As Long
 Public Declare Function IIDFromString Lib "ole32" (ByVal lpsz As Long, lpiid As UUID) As Long
 Public Declare Function CoCreateInstance Lib "ole32" (rclsid As UUID, ByVal pUnkOuter As Long, ByVal dwClsContext As Long, riid As UUID, ppv As Any) As Long
 Public Declare Function CoMarshalInterThreadInterfaceInStream Lib "ole32.dll" (riid As UUID, ByVal pUnk As IUnknown, ppStm As Long) As Long
 Public Declare Function CoGetInterfaceAndReleaseStream Lib "ole32.dll" (ByVal pStm As Long, riid As UUID, pUnk As Any) As Long
 Public Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
 Public Declare Function SetEvent Lib "kernel32" (ByVal hEvent As Long) As Long
 Public Declare Function ResetEvent Lib "kernel32" (ByVal hEvent As Long) As Long
 Public Declare Function CreateEvent Lib "kernel32" Alias "CreateEventW" (lpEventAttributes As Any, ByVal bManualReset As Long, ByVal bInitialState As Long, ByVal lpName As Long) As Long
 Public Declare Function lstrcpy Lib "kernel32" Alias "lstrcpyW" (lpString1 As Any, lpString2 As Any) As Long
 Public Declare Function lstrlen Lib "kernel32" Alias "lstrlenW" (lpString As Any) As Long
 Public Declare Function HeapAlloc Lib "kernel32" (ByVal hHeap As Long, ByVal dwFlags As Long, ByVal dwBytes As Long) As Long
 Public Declare Function HeapFree Lib "kernel32" (ByVal hHeap As Long, ByVal dwFlags As Long, lpMem As Any) As Long
 Public Declare Function GetProcessHeap Lib "kernel32" () As Long
 Public Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
 Public Declare Function InitCommonControlsEx Lib "comctl32" (icc As Any) As Long
 
 Public Const CLSCTX_INPROC_SERVER As Long = 1
 Public Const PAGE_EXECUTE_READWRITE As Long = &H40&
 Public Const CLSID_MultithreadDownloader As String = "{20FAEF52-0D1D-444B-BBAE-21240219905B}"
 Public Const IID_MultithreadDownloader As String = "{DF3BDB52-3380-4B78-B691-4138300DD304}"
 Public Const WM_APP As Long = &H8000&
 Public Const WM_QUIT As Long = &H12
 Public Const WM_MT_DOWNLOAD As Long = WM_APP ' Сообщение потоку чтобы вызвать метод
 Public Const INFINITE As Long = -1&
 Public Const WAIT_OBJECT_0 As Long = 0
 
 Public Type ThreadData
 hEvent As Long ' Объект синхронизации
 IStream As Long ' Объект потока, получающий ссылку на отмаршалленый объект MultithreadDownloader
 End Type
 
 ' // Удаляем вылеты Declare функций, жертвуя Err.LastDllError
 ' // Если использовать tlb, то не нужна.
 Public Sub RemoveLastDllError()
 Dim hMod As Long
 Dim lpProc As Long
 ' Получаем адрес функции __vbaSetSystemError
 hMod = GetModuleHandle(StrPtr("msvbvm60"))
 lpProc = GetProcAddress(hMod, "__vbaSetSystemError")
 ' Делаем ret
 VirtualProtect lpProc, 1, PAGE_EXECUTE_READWRITE, 0
 GetMem1 &HC3, ByVal lpProc
 End Sub
 
 ' // Функция потока
 Public Function ThreadProc(value As ThreadData) As Long
 Dim clsid As UUID
 Dim iid As UUID
 Dim obj As MTDownloader.MultithreadDownloader
 
 ' Инициализируем COM
 CoInitialize ByVal 0&
 ' Инициализируем CLSID и IID для создания и управления объектом
 IIDFromString StrPtr(IID_MultithreadDownloader), iid
 CLSIDFromString StrPtr(CLSID_MultithreadDownloader), clsid
 ' Создаем объект MTDownloader.MultithreadDownloader
 If CoCreateInstance(clsid, 0, CLSCTX_INPROC_SERVER, iid, obj) = 0 Then
 ' Маршаллинг для отлова событий в другом потоке
 CoMarshalInterThreadInterfaceInStream iid, obj, value.IStream
 ' Объект инициализирован
 SetEvent value.hEvent
 Else
 ' Объект не инициализирован
 SetEvent value.hEvent
 ' Деинициализация
 CoUninitialize
 ' Выход
 Exit Function
 End If
 
 Dim msg As msg
 Dim ret As Long
 Dim URL As String
 Dim fle As String
 
 ' Цикл обработки сообщений в новом потоке
 Do
 ret = GetMessage(msg, 0, 0, 0)
 If ret = -1 Or ret = 0 Then Exit Do
 ' Проверяем сообщения
 Select Case msg.message
 Case WM_MT_DOWNLOAD
 ' Получаем запакованные параметры, они лежат последовательно
 ret = lstrlen(ByVal msg.lParam)
 URL = Space(ret)
 lstrcpy ByVal StrPtr(URL), ByVal msg.lParam
 ret = lstrlen(ByVal msg.lParam + (ret + 1) * 2)
 fle = Space(ret)
 lstrcpy ByVal StrPtr(fle), ByVal msg.lParam + LenB(URL) + 2
 ' Сбрасываем событие, чтобы нельзя было вызвать метод еще раз пока не отработает предыдущий вызов
 ResetEvent value.hEvent
 ' Вызываем метод
 obj.Download URL, fle
 ' Устанавливаем событие - объект свободен
 SetEvent value.hEvent
 ' Очищаем параметры
 HeapFree GetProcessHeap(), 0, ByVal msg.lParam
 Case Else
 TranslateMessage msg
 DispatchMessage msg
 End Select
 Loop
 
 ' Удаляем объекты
 Set obj = Nothing
 ' Деинициализация
 CoUninitialize
 
 End Function
 
 ' // Упаковка параметров последовательно
 Public Function MT_DOWNLOAD_packParam(URL As String, fileName As String) As Long
 MT_DOWNLOAD_packParam = HeapAlloc(GetProcessHeap(), 0, LenB(URL) + LenB(fileName) + 4)
 If MT_DOWNLOAD_packParam Then
 lstrcpy ByVal MT_DOWNLOAD_packParam, ByVal StrPtr(URL)
 lstrcpy ByVal MT_DOWNLOAD_packParam + LenB(URL) + 2, ByVal StrPtr(fileName)
 End If
 End Function
Разберем подробно код. При загрузке формы (Form_Load), мы пропатчиваем рантайм для исключения ошибки использования Declared функций в неинициализированном потоке (RemoveLastDllError). Принцип я описал выше. Если мы создаем объект в другом потоке, то нам нужно как-то проверить в основном потоке, создался ли объект. Для этого я использовал простейший синхронизирующий объект - событие с ручным сбросом. Иницииализируем его в сброшенном состоянии. Далее создаем поток в функции ThreadProc, в качестве параметра передаем структуру из синхронизирующего события и ссылки на объект потока (Stream), который нужен для маршалинга. В этом объекте веренется ссылка на отмаршаленый указатель на объект. При успехе ждем срабатывания события (WaitForSingleObject). На этом основной поток приостанавливает свое выполнение пока мы не установим событие hEvent. В новом потоке сначала инициализируем COM (CoInitialize), переводим CLSID и IID в двоичную форму, создаем объект (CoCreateInstance). Здесь, если не нужна обработка ошибок, то можно использовать CreateObject("MTDownloader.MultithreadDownloader"). В данном коде для создания объекта я использовал CoCreateInstance, т.к. до создания первого объекта мы не можем включить обработку ошибок (причина описана выше), после создания первого объекта далее можно создавать через VB-шный CreateObject. Если обработка ошибок не нужна, то можно сразу использовать CreateObject. При успехе выполняем маршалинг, для этого вызываем функцию CoMarshalInterThreadInterfaceInStream, которая записывает в Stream (поток) информацию для создания прокси-объекта в другом потоке. Устанавливаем событие, этим самым говорим основному потоку что инициализация прошла. При неудаче также выставляем событие и деинициализируем COM в данном потоке, выходим (поток завершен). Признаком успешной инициализации становится ссылка IStream. Далее в этом потоке переходим к стандартному циклу обработки сообщений. Т.к. мы установили событие, основной поток просыпается и мы проверяем, успешно ли прошла инициализация. Если в IStream записана ссылка, значит все хорошо, иначе ошибка.
Далее получаем указатель на прокси объект из потока вызовом CoGetInterfaceAndReleaseStream, тем самым также освобождаем объект Stream. Присваиваем нашей объектной переменной, подписанной на события, указатель на прокси-объект. Всеми этими манипуляциями теперь мы можем обращаться к объекту в другом потоке и получать от него события. Проверяем корректно ли инициализировался сам объект (hInternet<>0), и устанавливаем размер буфера в 64 кб, информация будет обновляться при очередной порции закаченных данных в 64 кБ. На этом инициализация закончена.
Для того, чтобы нельзя было выполнить несколько запросов на закачку, мы будем синхронизировать запросы по созданному событию. Иначе просто если несколько раз щелкнуть на кнопке Download, то данные будут закачиваться последовательно, если ошибочно нажать 2 раза, то файл скачается 2 раза и перезапишется, ошибок не будет. При нажатии мы проверяем статус события, если оно установлено то закачки в данной момент нет. Для передачи данных в другой поток, выполним транспортировку (маршалинг) параметров в другой поток (MT_DOWNLOAD_packParam). Для этого выделим память в куче и скопируем данные (в нашем случае URL и FileName) в нее, а ссылку передадим в созданный поток. Сохранять я решил самым простым способом - 2 unicode-строки последовательно с завершающими нуль-терминалами. Передаем ссылку на параметры в очередь потока через PostThreadMessage, в качестве номера сообщения используем первый незанятый идентификатор WM_APP, который я назвал WM_MT_DOWNLOAD. В другом потоке в цикле, при получении сообщения WM_MT_DOWNLOAD, вытаскиваем параметры из кучи и вызываем метод Download, предварительно сбросив событие hEvent. Все. Пока выполняется метод мы не сможем вызвать его опять, а благодаря маршалингу мы получаем уведомления от объекта в виде событий в основном потоке. Обработчики событий элементарные и в пояснениях не нуждаются. Единственное что хочу добавить, что для размера файла я выбрал Currency, т.к. 64-битных целых чисел нет, а Currency это почти тоже самое, только деленное на 10000(10).
Помимо асинхронных вызовов у нас также остается возможность синхронного вызова, т.е. в коде формы вполне законно можем написать Downloader.Download URL, FileName. Можно сравнить преимущества и недостатки асинхронноги и синхронного вызовов.
Пример не требует регистрации ActiveX DLL, достаточно положить ее в ту же папку благодаря манифесту. В итоге имеем многопоточное приложение которое работает на любой машине без запроса админских прав.

_____________________________________________
Многопоточность на VB6 реальна, и практически реализуема. В этой части я описал метод создания объекта в отдельном потоке и подписку на его события. Если нам не нужно иметь связи с объектом в разных потоках, то код многократно укорачивается (убирается цикл обработки, маршалинг и т.п.); можно даже создать объект в ассемблерной вставке - что позволит ограничено отлаживать такой код в IDE. Но все эти приемы я опишу как-нибудь в другой раз. Всем удачи!


