Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / Visual Basic Новый топик    Ответить
 Модуль для работы с многопоточностью на VB6.  [new]
ATM-TURBO 2
Member

Откуда:
Сообщений: 147
Всем привет!
Представляю модуль для работы с многопоточностью на VB6 для Standard EXE проектов. Данный модуль разработан на основе этого решения в котором исправлены некоторые баги и добавлен дополнительный функционал. Модуль не требует никаких дополнительных зависимостей и библиотек типов, работает как в IDE (все функции работают в главном потоке) так и в скомпилированном виде.



Для начала работы с модулем нужно вызывать функцию Initialize, которая инициализирует данные необходимые для работы (инициализирует критические секции для монопольного доступа к куче хидеров и потокам маршалинга, модифицирует VBHeader (тут написано для чего), выделяет TLS слот для передачи параметров потоку).
Основная функция создания потока - vbCreateThread которая является аналогом функции CreateThread.
' // Create a new thread
Public Function vbCreateThread(ByVal lpThreadAttributes As Long, _
                               ByVal dwStackSize As Long, _
                               ByVal lpStartAddress As Long, _
                               ByVal lpParameter As Long, _
                               ByVal dwCreationFlags As Long, _
                               ByRef lpThreadId As Long, _
                               Optional ByVal bIDEInSameThread As Boolean = True) As Long

Функция создает поток и вызывает функцию переданную в параметре lpStartAddress с параметром lpParameter.
В IDE вызов сводится к простому вызову по указателю реализованному через DispCallFunc. В скомпилированном варианте данная функция работает по-другому. Т.к. для работы потока требуется инициализация проектно-специфичных данных, а также инициализация рантайма, параметры переданные в lpStartAddress и lpParameter временно сохраняются в куче посредством функции PrepareData, а поток создается в функции ThreadProc, которая занимается непосредственно инициализацией и вызовом уже пользовательской функции с пользовательским параметром. Данная функция создает копию структуры VBHeader через CreateVBHeaderCopy и изменяет данные размещения публичных переменных в структурах VbPublicObjectDescriptor.lpPublicBytes, VbPublicObjectDescriptor.lpStaticBytes (что не было сделано в предыдущем варианте) так чтобы глобальные переменные не затрагивались при инициализации. Далее VBDllGetClassObject вызывает функцию FakeMain (адрес которой записан в модифицированную структуру VBHeader). Для передачи пользовательских параметров используется TLS слот (т.к. функция Main не принимает параметры, подробности тут). В FakeMain уже параметры извлекаются из TLS и вызывается пользовательская процедура. Возвращаемое значение функции также передается обратно через TLS. Тут есть один интересный момент связанный с копией хидера, который был не учтен в предыдущей версии. Т.к. рантайм использует хидер после завершения потока (при DLL_THREAD_DETACH) мы не можем освободить хидер в процедуре ThreadProc, и произойдет утечка памяти. Для предотвращения утечки памяти используется куча фиксированного размера, хидеры не очищаются пока есть место в этой куче. Как только место заканчивается (а оно выделяется в функции CreateVBHeaderCopy) происходит очистка ресурсов. Первый DWORD хидера на самом деле хранит ID потока в котором был создан и в функции FreeUnusedHeaders происходит проверка всех хидеров в куче. Если поток завершен - место освобождается (хотя ID может повторятся, но это не играет особой роли т.к. в любом случае в куче будут свободные места и если хидер не освободился в одном случае то он будет освобожден позже). Из-за того что очистка может быть вызвана сразу из нескольких потоков доступ к очистке разделяется критической секцией tLockHeap.tWinApiSection и если какой-то поток уже занимается очисткой функция возвратит True что означает что вызывающий поток должен немного подождать и память будет доступна.

Еще одной из возможностей модуля является возможность инициализации рантайма и проекта и вызова callback-фнкции. Это может пригодится для сallback-функций которые могут быть вызваны в контексте произвольного потока (к примеру InternetStatusCallback). Для этого служат функции InitCurrentThreadAndCallFunction и InitCurrentThreadAndCallFunctionIDEProc. Первая функция используется в скомпилированном приложении и принимает адрес функции обратного вызова которая будет вызвана после инициализации рантайма, а также параметра который будет передан в эту функцию. В callback-процедуре передается адрес первого параметра чтобы потом в пользовательской процедуре ссылаться на него:
' // This function is used in compiled form
Public Function CallbackProc( _
                ByVal lThreadId As Long, _
                ByVal sKey As String, _
                ByVal fTimeFromLastTick As Single) As Long
    ' // Init runtime and call CallBackProc_user with VarPtr(lThreadId) parameter
    InitCurrentThreadAndCallFunction AddressOf CallBackProc_user, VarPtr(lThreadId), CallbackProc
End Function

' // Callback function is called by runtime/window proc (in IDE)
Public Function CallBackProc_user( _
                ByRef tParam As tCallbackParams) As Long

End Function

CallBackProc_user - будет вызвана уже с инициализированным рантаймом.

Для работы в IDE данная функция не подойдет, потому что в IDE все работает в главном потоке. Для отладки в IDE используется функция InitCurrentThreadAndCallFunctionIDEProc которая возвращает адрес ассемблерного переходника который транслирует вызов в главный поток и вызовет пользовательскую функцию в контексте главного потока. Данная функция принимает адрес пользовательской callback-функции и размер параметров в байтах. В качестве параметра пользовательской функции она всегда передает адрес первого параметра. Немного расскажу о работе этого механизма в IDE. Для трансляции вызова из вызывающего потока в главный поток используется message-only окно. Данное окно создается посредством вызова функции InitializeMessageWindow. При первом вызове создается WindowProc процедура со следующим кодом:
    CMP DWORD [ESP+8], WM_ONCALLBACK
    JE SHORT L
    JMP DefWindowProcW
 L:  PUSH DWORD PTR SS:[ESP+10]
    CALL DWORD PTR SS:[ESP+10]
    RETN 10

Как видно из кода данная процедура "слушает" сообщение WM_ONCALLBACK которое содержит в параметре wParam - адрес функции, а в параметре lParam параметры. При получении этого сообщение она вызывает данную процедуру с данным параметром, остальные сообщения игнорируются. Данное сообщение отправляется как-раз ассемблерным переходником из caller-потока.
Далее создается окно и хендл этого окна и хендл кучи сохраняются в данных класса окна. Это используется для устранения утечки памяти в IDE, т.к. если класс один раз зарегистрирован то потом данные параметры можно получить в любой сессии отладки. Сама callback-функция генерируется в InitCurrentThreadAndCallFunctionIDEProc, но сначала проверяется не была ли уже создана подобная процедура (чтобы не создавать одинаковые переходники). Сам переходник имеет следющий код:
LEA EAX, [ESP+4]
PUSH EAX
PUSH pfnCallback
PUSH WM_ONCALLBACK
PUSH hMsgWindow
Call SendMessageW
RETN lParametersSize

Как видно из ода, что при вызове callback-функции вызов транслируется через SendMessage в главный поток. Параметр lParametersSize используется для правильного восстановления стека.

Следующая особенность модуля - это создание объектов в отдельном потоке причем создавать можно как приватные объекты (для этого используется метод основанный на коде модуля NameBasedObjectFactory by firehacker). Для создания проектных классов используется функция CreatePrivateObjectByNameInNewThread для ActiveX-публичных классов CreateActiveXObjectInNewThread и CreateActiveXObjectInNewThread2. Прежде чем создавать экземпляры проектных классов нужно сначала разрешить маршалинг данных объектов посредством вызова функции EnablePrivateMarshaling. Данные функции принимают идентификатор класса (ProgID/CLSID для ActiveX и имя для проектных классов), идентификатор интерфейса (по умолчанию используется IDispatch/Object). При успешном вызове функции возвращают отмаршаленый объект и ID асинхронного вызова. Для скомпилированного варианта это ID потока для IDE - указатель на объект. Объекты создаются и "живут" в функции ActiveXThreadProc. Жизнь объектов контролируется через счетчик ссылок (когда он равен 1 значит только ActiveXThreadProc ссылается на объект и можно его удалять и завершать поток.
Вызывать методы можно синхронно - просто вызывать метод как обычно, либо асинхронно - используя процедуру AsynchDispMethodCall. Данная процедура принимает ID асинхронного вызова, имя метода, тип вызова, объект который получит уведомление о вызове, имя метода уведомления и список параметров. Процедура копирует параметры во временную область, маршалит объект уведомления и отправляет данные потоку объекта через WM_ASYNCH_CALL. Стоит отметить что здесь не поддерживается маршалинг параметров, поэтому следует с осторожностью передавать ссылки на объекты. Если нужно отмаршалить объектную ссылку то следует использовать синхронный метод для маршалинга объектов и далее вызвать асинхронный метод. Процедура возвращается немедленно. В потоке ActiveXThreadProc данные извлекаются и выполняется синхронный вызов посредством MakeAsynchCall. Тут все просто, вызывается CallByName для объекта потока и CallByName для уведомления. Метод уведомления имеет следующий прототип:
Public Sub CallBack(ByVal vRet As Variant)
, где vRet принимает возвращаемое значение метода.

Следующие функции предназначены для маршалинга: Marshal, Marshal2, UnMarshal, FreeMarshalData. Первая создает информацию о маршалинге (Proxy) интерфейса и заносит ее в поток который возвращает. Принимает в параметре pInterface идентификатор интерфейса (по умолчанию IDispatch/Object). Функция UnMarshal наоборот принимает поток и на основе информации создает Proxy-объект. Опционально можно освободить объект потока. Marshal2 делает тоже самое что и Marshal за исключением того что она позволяет создавать Proxy-объект множество раз в разных потоках. FreeMarshalData соответственно освобождает данные и поток.
Если к примеру требуется перенести ссылку на объект между двумя потоками то достаточно вызвать пару Marshal/UnMarshal соответственно в потоке создавшем объект и потоке принимающем ссылку. В другом случае если к примеру есть один глобальный объект и нужно передать ссылку на него во множество потоков (к примеру объект логирования) то в потоке объекта вызывается Marshal2, а в клиентских потоках вызывается UnMarshal с параметром bReleaseStream равным False. Когда данные больше не нужны вызывается FreeMarshalData.

Функция WaitForObjectThreadCompletion предназначена для ожидания завершения объектного потока и принимает ID асинхронного вызова. Эту функцию желательно вызывать всегда при завершении основного процесса, поскольку объектный поток так или иначе может взаимодействовать с основным потоком и его объектами (к примеру если объектный поток имеет маршаленую ссылку на интерфейс основного потока).

Функция SuspendResume предназначена для приостановления/возобновления объектного потока. bSuspend определяет усыпить либо возобновить поток.
Помимо модуля также в архиве есть несколько примеров работы с ним:
  • Callback - проект в котором продемонстрирована работа с callback-функцией вызываемой из разных потоков. Также там содержится дополнительный проект нативной dll (на VB6) которая вызывает периодически функцию в разных потоках;
  • JuliaSet - генерация фрактала Julia параллельно в нескольких потоках (задается пользователем);
  • CopyProgress - Копирование папки в отдельном потоке с отображением прогресса копирования;
  • PublicMarshaling - Создание публичных объектов (Dictionary) в разных потоках и вызов их методов (синхронно/асинхронно);
  • PrivateMarshaling - Создание приватных объектов в разных потоках и вызов их методов (синхронно/асинхронно);
  • MarshalUserInterface - Создание приватных объектов в разных потоках и вызов их методов (синхронно/асинхронно) на основе пользовательских интерфейсов (содержит tlb и Reg-Free манифест).

    Модуль слабо тестировался, поэтому возможны баги. Буду очень рад любым замечаниям, по мере возможности буду их исправлять.
    Всем спасибо за внимание!

    К сообщению приложен файл (VBMultithreadingModule.zip - 47Kb) cкачать
  • 13 июн 18, 10:16    [21487470]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    vb_sub
    Member

    Откуда:
    Сообщений: 379
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?
    13 июн 18, 10:52    [21487639]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    ATM-TURBO 2
    Member

    Откуда:
    Сообщений: 147
    vb_sub
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?

    Это сродни вопросу - есть ли такая острая нужда в многопоточности для C++? Каков будет ответ?
    13 июн 18, 11:15    [21487722]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    Eolt
    Member

    Откуда:
    Сообщений: 1306
    vb_sub
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?


    да
    13 июн 18, 12:24    [21487930]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    Eolt
    Member

    Откуда:
    Сообщений: 1306
    ATM-TURBO 2
    Всем привет!
    Представляю модуль для работы с многопоточностью на VB6 для Standard EXE проектов.


    Это круто. Вечером обязательно буду тестить. Обязательно закиньте свой модуль на vbforums Там самое крупное vb6 сообщество на данный момент и много людей попробуют ваш код.
    13 июн 18, 12:27    [21487946]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    vb_sub
    Member

    Откуда:
    Сообщений: 379
    ATM-TURBO 2
    vb_sub
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?

    Это сродни вопросу - есть ли такая острая нужда в многопоточности для C++? Каков будет ответ?

    Просто новые проекты подразумевающие использование многопоточности маловероятно, что будут писаться на Basic, а старые как правило, если уже много лет без многопоточности проработали, то и вряд ли кто-то будет ставить эксперименты с рабочим кодом. Чисто с исследовательской точки зрения конечно весьма похвально.
    13 июн 18, 12:40    [21488019]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    vb_sub
    Member

    Откуда:
    Сообщений: 379
    Eolt
    vb_sub
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?


    да

    можно пару примеров в каких ситуациях?
    13 июн 18, 12:42    [21488029]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    ATM-TURBO 2
    Member

    Откуда:
    Сообщений: 147
    Eolt
    Это круто. Вечером обязательно буду тестить. Обязательно закиньте свой модуль на vbforums Там самое крупное vb6 сообщество на данный момент и много людей попробуют ваш код.

    Да, я всегда там дублирую.
    +
    Сорри за мой английский, учил немецкий в школе )


    vb_sub
    Просто новые проекты подразумевающие использование многопоточности маловероятно, что будут писаться на Basic, а старые как правило, если уже много лет без многопоточности проработали, то и вряд ли кто-то будет ставить эксперименты с рабочим кодом. Чисто с исследовательской точки зрения конечно весьма похвально.

    Люди пишут еще на VB6, на том же vbforums часто возникают подобные вопросы, к примеру вот и вот. Соответственно также много решений к примеру вот и вот.

    vb_sub
    можно пару примеров в каких ситуациях?

    Да в любых. Выше привел пример с InternetStatusCallback или к примеру CoreAudio. И это примеры где без многопоточности просто не будет работать проект, а так любые примеры где используются потоки на любых других языках.
    13 июн 18, 13:16    [21488153]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    Eolt
    Member

    Откуда:
    Сообщений: 1306
    vb_sub
    можно пару примеров в каких ситуациях?


    Любой фоновый процесс надолго замораживающий UI.
    Можно по-человечески использовать отдельный поток для него, а не древний костыль с DoEvents
    14 июн 18, 10:14    [21490159]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    Roman Mejtes
    Member

    Откуда: г. Пермь
    Сообщений: 2960
    vb_sub
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?

    есть такие люди, которым важно превозмогать :)
    а вообще, я знаю очень большой и жирный + VB6, приложения написанные на этом языке работают начиная с Windows 95 и заканчивая Window 10 нативно, без всяких танцев с бубнов и фреймворков. Запускается гарантировано везде.
    15 июн 18, 00:45    [21492464]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    vb_sub
    Member

    Откуда:
    Сообщений: 379
    Roman Mejtes,
    regsvr32.exe one love.
    15 июн 18, 11:02    [21493080]     Ответить | Цитировать Сообщить модератору
     Re: Модуль для работы с многопоточностью на VB6.  [new]
    Eolt
    Member

    Откуда:
    Сообщений: 1306
    Roman Mejtes
    vb_sub
    ATM-TURBO 2,
    есть ли такая острая нужда в многопоточности для VB6?

    есть такие люди, которым важно превозмогать :)
    а вообще, я знаю очень большой и жирный + VB6, приложения написанные на этом языке работают начиная с Windows 95 и заканчивая Window 10 нативно, без всяких танцев с бубнов и фреймворков. Запускается гарантировано везде.


    Еще на ReactOS работает.
    15 июн 18, 11:07    [21493112]     Ответить | Цитировать Сообщить модератору
    Все форумы / Visual Basic Ответить