Многопоточность в VB6 часть 4

Автор обещает много интересных штучек.

Модератор: The trick

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Многопоточность в VB6 часть 4

Сообщение The trick » 17.02.2015 (Вт) 1:13



Всем привет. Сейчас у меня мало времени, поэтому я уже не так часто уделяю внимание бейсику и реже появляюсь на форумах. Сегодня я опять буду говорить о многопоточности, на этот раз в Standart EXE. Сразу скажу что все о чем я пишу является моим личным исследованием и может в чем-то не соответствовать действительности; также из-за моего недостатка времени я буду дополнять этот пост по мере дальнейшего прогресса в исследовании данного вопроса. Итак начнем.
Как я говорил до этого для того чтобы многопоточность работала нужно инициализировать рантайм. Без инициализации мы можем работать очень ограниченно, в том смысле что COM не будет работать, т.е. грубо говоря вся мощь бейсика будет недоступна. Можно работать с API, объявленными в tlb, некоторыми функциями, также убирая проверку __vbaSetSystemError, можно использовать Declared-функции. Все предыдущие публикации показывали работу в отдельных DLL, и мы легко могли инициализировать рантайм используя VBDllGetClassObject функцию для этого. Сегодня мы попытаемся инициализировать рантайм в обычном EXE, т.е. не используя внешние зависимости. Не для кого не секрет что любое приложение написанное в VB6 состоит из хидера проекта, в котором содержится очень много информации о проекте которую рантайм использует для работы:
Код: Выделить всё
Type VbHeader
    szVbMagic               As String * 4
    wRuntimeBuild           As Integer
    szLangDll               As String * 14
    szSecLangDll            As String * 14
    wRuntimeRevision        As Integer
    dwLCID                  As Long
    dwSecLCID               As Long
    lpSubMain               As Long
    lpProjectInfo           As Long
    fMdlIntCtls             As Long
    fMdlIntCtls2            As Long
    dwThreadFlags           As Long
    dwThreadCount           As Long
    wFormCount              As Integer
    wExternalCount          As Integer
    dwThunkCount            As Long
    lpGuiTable              As Long
    lpExternalCompTable     As Long
    lpComRegisterData       As Long
    bszProjectDescription   As Long
    bszProjectExeName       As Long
    bszProjectHelpFile      As Long
    bszProjectName          As Long
End Type

В этой структуре большое количество полей описывать все я не буду, отмечу только что эта структура ссылается на множество других структур. Некоторые из них нам понадобятся в дальнейшем, например поле lpSubMain, в котором содержится адрес процедуры Main, если она определена, иначе там 0.
Подавляющее большинство EXE файлов начинаются со следующего кода:
Код: Выделить всё
PUSH xxxxxxxx
CALL MSVBVM60.ThunRTMain

Как раз xxxxxxxx указывает на структуру VBHeader. Эта особенность позволит найти эту структуру внутри EXE для инициализации рантайма. В одной из предыдущих частей я описывал как достать из ActiveX DLL эту структуру - для этого нужно было считать данные в одной из экспортируемых функций (к примеру DllGetClassObject). Для получения из EXE - мы также воспользуемся тем-же методом. Для начала нужно найти точку входа (entry point), т.е. адрес с которого начинается выполнение EXE. Этот адрес можно получить из структуры IMAGE_OPTIONAL_HEADER - поле AddressOfEntryPoint. Сама структура IMAGE_OPTIONAL_HEADER расположена в PE заголовке, а PE заголовок находится по смещению заданному в поле e_lfanew структуры IMAGE_DOS_HEADER, ну а структура IMAGE_DOS_HEADER расположена по адресу App.hInstance (или GetModuleHandle). Указатель на VbHeader будет лежать по смещению AddressOfEntryPoint + 1, т.к. опкод команды push в данном случае 0x68h. Итак, собирая все вместе, получим функцию для получения хидера:
Код: Выделить всё
' // Get VBHeader structure
Private Function GetVBHeader() As Long
    Dim ptr     As Long
    ' Get e_lfanew
    GetMem4 ByVal hModule + &H3C, ptr
    ' Get AddressOfEntryPoint
    GetMem4 ByVal ptr + &H28 + hModule, ptr
    ' Get VBHeader
    GetMem4 ByVal ptr + hModule + 1, GetVBHeader
   
End Function

Теперь если передать эту структуру функции VBDllGetClassObject в новом потоке, то, грубо говоря, эта функция запустит наш проект на выполнение согласно переданной структуре. Конечно смысла в этом мало - это тоже самое что начать выполнение приложения заново в новом потоке. Например если была задана функция Main, то и выполнение начнется опять с нее, а если была форма, то с нее. Нужно как-то сделать так, чтобы проект выполнялся с другой, нужной нам, функции. Для этого можно изменить поле lpSubMain структуры vbHeader. Я тоже сначала сделал так, но это ничего не дало. Как выяснилось, внутри рантайма есть один глобальный объект, который хранит ссылки на проекты и связанные с ними объекты и если передать тот же самый хидер в VBDllGetClassObject, то рантайм проверит, не загружался ли такой проект, и если загружался, то просто запустит новую копию без разбора структуры vbHeader, на основании предыдущего разбора. Поэтому я решил поступить так - можно скопировать структуру vbHeader в другое место и использовать ее. Сразу замечу, что в этой структуре последние 4 поля - это смещения относительно начала структуры, поэтому при копировании струкутуры их нужно будет скорректировать. Если теперь попробовать передать эту структуру в VBDllGetClassObject, то все будет отлично если в качестве стартапа установлена Sub Main, если же форма, то будет запущена и форма и после нее Main. Для исключения такого поведения нужно поправить кое-какие данные на которые ссылается хидер. Я пока точно не знаю что это за данные, т.к. не разбирался в этом, но "поковырявшись" внутри рантайма я нашел их место положение. Поле lpGuiTable структуры vbHeader ссылается на список структур tGuiTable, которые описывают формы в проекте. Структуры идут последовательно, число структур соответствует полю wFormCount структуры vbHeader. В сети я так и не нашел нормальное описание структуры tGuiTable, вот что есть:
Код: Выделить всё
Type tGuiTable
    lStructSize          As Long
    uuidObjectGUI        As uuid
    Unknown1             As Long
    Unknown2             As Long
    Unknown3             As Long
    Unknown4             As Long
    lObjectID            As Long
    Unknown5             As Long
    fOLEMisc             As Long
    uuidObject           As uuid
    Unknown6             As Long
    Unknown7             As Long
    aFormPointer         As Long
    Unknown8             As Long
End Type

Как выяснилось внутри рантайма есть код, который проверяет поле Unknown5 каждой структуры:
Изображение
Я проставил комментарии; из них видно что Unknown5 содержит флаги и если установлен 5-й бит, то происходит запись ссылки на какой-то объект, заданный регистром EAX, в поле со смещением 0x30 объекта заданного регистром EDX. Что за объекты - я не знаю, возможно позже разберусь с этим, нам важен сам факт записи какого-то значения в поле со смещением 0x30. Теперь, если дальше начать исследовать код то можно наткнутся на такой фрагмент:
Изображение
Скажу что объект на который указывает ESI, тот же самый объект что в предыдущей рассматриваемой процедуре (регистр EDX). Видно что тестируется значение этого поля на -1 и на 0, и если там любое из этих чисел то запускается процедура Main (если она задана); иначе запускается первая форма.
Итак, теперь чтобы гарантированно запускать только Sub Main, мы изменяем флаг lpGuiTable.Unknown5, сбрасывая пятый бит. Для установки новой Sub Main и модификации флага я создал отдельную процедуру:
Код: Выделить всё
' // Modify VBHeader to replace Sub Main
Private Sub ModifyVBHeader(ByVal newAddress As Long)
    Dim ptr     As Long
    Dim old     As Long
    Dim flag    As Long
    Dim count   As Long
    Dim size    As Long
   
    ptr = lpVBHeader + &H2C
    ' Are allowed to write in the page
    VirtualProtect ByVal ptr, 4, PAGE_READWRITE, old
    ' Set a new address of Sub Main
    GetMem4 newAddress, ByVal ptr
    VirtualProtect ByVal ptr, 4, old, 0
   
    ' Remove startup form
    GetMem4 ByVal lpVBHeader + &H4C, ptr
    ' Get forms count
    GetMem4 ByVal lpVBHeader + &H44, count
   
    Do While count > 0
        ' Get structure size
        GetMem4 ByVal ptr, size
        ' Get flag (unknown5) from current form
        GetMem4 ByVal ptr + &H28, flag
        ' When set, bit 5,
        If flag And &H10 Then
            ' Unset bit 5
            flag = flag And &HFFFFFFEF
            ' Are allowed to write in the page
            VirtualProtect ByVal ptr, 4, PAGE_READWRITE, old
            ' Write changet flag
            GetMem4 flag, ByVal ptr + &H28
            ' Restoring the memory attributes
            VirtualProtect ByVal ptr, 4, old, 0
           
        End If
        count = count - 1
        ptr = ptr + size
       
    Loop
   
End Sub

Теперь, если попробовать запустить эту процедуру перед передачей хидера в VBDllGetClassObject, то будет запускаться процедура, определенная нами. Впрочем многопоточность уже будет работать, но это не удобно, т.к. отсутствует механизм передачи параметра в поток как это реализовано в CreateThread. Для того чтобы сделать полный аналог CreateThread я решил создать аналогичную функцию, которая будет проводить все инициализации и после выполнять вызов переданной функции потока вместе с параметром. Для того чтобы была возможность передать параметр в Sub Main, я использовал локальное хранилище потока (TLS). Мы выделяем индекс для TLS. После выделения индекса мы сможем задавать значение этого индекса, специфичное для каждого потока. В общем идея такова, создаем новый поток, где стартовой функцией будет специальная функция ThreadProc, в параметр которой передаем структуру из двух полей - адреса пользовательской функции и адреса параметра. В этой процедуре мы будем инициализировать рантайм для нового потока и сохранять в TLS переданный параметр. В качестве процедуры Main создадим бинарный код, который будет доставать данные из TLS, формировать стек и прыгать на пользовательскую функцию. В итоге получился такой модуль:
modMultiThreading.bas
Код: Выделить всё
' modMultiThreading.bas - The module provides support for multi-threading.
' © Кривоус Анатолий Анатольевич (The trick), 2015

Option Explicit

Private Type uuid
    data1       As Long
    data2       As Integer
    data3       As Integer
    data4(7)    As Byte
End Type

Private Type threadData
    lpParameter As Long
    lpAddress   As Long
End Type

Private tlsIndex    As Long  ' Index of the item in the TLS. There will be data specific to the thread.
Private lpVBHeader  As Long  ' Pointer to VBHeader structure.
Private hModule     As Long  ' Base address.
Private lpAsm       As Long  ' Pointer to a binary code.

' // 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, _
                               lpThreadId As Long) As Long
    Dim InIDE   As Boolean
   
    Debug.Assert MakeTrue(InIDE)
   
    If InIDE Then
        Dim ret As Long
       
        ret = MsgBox("Multithreading not working in IDE." & vbNewLine & "Run it in the same thread?", vbQuestion Or vbYesNo)
        If ret = vbYes Then
            ' Run function in main thread
            ret = DispCallFunc(ByVal 0&, lpStartAddress, CC_STDCALL, vbEmpty, 1, vbLong, VarPtr(CVar(lpParameter)), CVar(0))
            If ret Then
                Err.Raise ret
            End If
        End If
       
        Exit Function
    End If
   
    ' Alloc new index from thread local storage
    If tlsIndex = 0 Then
       
        tlsIndex = TlsAlloc()
       
        If tlsIndex = 0 Then Exit Function
       
    End If
    ' Get module handle
    If hModule = 0 Then
       
        hModule = GetModuleHandle(ByVal 0&)
       
    End If
    ' Create assembler code
    If lpAsm = 0 Then
       
        lpAsm = CreateAsm()
        If lpAsm = 0 Then Exit Function
       
    End If
    ' Get pointer to VBHeader and modify
    If lpVBHeader = 0 Then
   
        lpVBHeader = GetVBHeader()
        If lpVBHeader = 0 Then Exit Function
       
        ModifyVBHeader lpAsm
       
    End If
   
    Dim lpThreadData    As Long
    Dim tmpData         As threadData
    ' Alloc thread-specific memory for threadData structure
    lpThreadData = HeapAlloc(GetProcessHeap(), 0, Len(tmpData))
   
    If lpThreadData = 0 Then Exit Function
    ' Set parameters
    tmpData.lpAddress = lpStartAddress
    tmpData.lpParameter = lpParameter
    ' Copy parameters to thread-specific memory
    GetMem8 tmpData, ByVal lpThreadData
    ' Create thread
    vbCreateThread = CreateThread(ByVal lpThreadAttributes, _
                                  dwStackSize, _
                                  AddressOf ThreadProc, _
                                  ByVal lpThreadData, _
                                  dwCreationFlags, _
                                  lpThreadId)
   
End Function

' // Initialize runtime for new thread and run procedure
Private Function ThreadProc(lpParameter As threadData) As Long
    Dim iid         As uuid
    Dim clsid       As uuid
    Dim lpNewHdr    As Long
    Dim hHeap       As Long
    ' Initialize COM
    vbCoInitialize ByVal 0&
    ' IID_IUnknown
    iid.data4(0) = &HC0: iid.data4(7) = &H46
    ' Store parameter to thread local storage
    TlsSetValue tlsIndex, lpParameter
    ' Create the copy of VBHeader
    hHeap = GetProcessHeap()
    lpNewHdr = HeapAlloc(hHeap, 0, &H6A)
    CopyMemory ByVal lpNewHdr, ByVal lpVBHeader, &H6A
    ' Adjust offsets
    Dim names()     As Long
    Dim diff        As Long
    Dim Index       As Long
   
    ReDim names(3)
    diff = lpNewHdr - lpVBHeader
    CopyMemory names(0), ByVal lpVBHeader + &H58, &H10
   
    For Index = 0 To 3
        names(Index) = names(Index) - diff
    Next
   
    CopyMemory ByVal lpNewHdr + &H58, names(0), &H10
    ' This line calls the binary code that runs the asm function.
    VBDllGetClassObject VarPtr(hModule), 0, lpNewHdr, clsid, iid, 0
    ' Free memeory
    HeapFree hHeap, 0, ByVal lpNewHdr
    HeapFree hHeap, 0, lpParameter
   
End Function

' // Get VBHeader structure
Private Function GetVBHeader() As Long
    Dim ptr     As Long
   
    ' Get e_lfanew
    GetMem4 ByVal hModule + &H3C, ptr
    ' Get AddressOfEntryPoint
    GetMem4 ByVal ptr + &H28 + hModule, ptr
    ' Get VBHeader
    GetMem4 ByVal ptr + hModule + 1, GetVBHeader
   
End Function

' // Modify VBHeader to replace Sub Main
Private Sub ModifyVBHeader(ByVal newAddress As Long)
    Dim ptr     As Long
    Dim old     As Long
    Dim flag    As Long
    Dim count   As Long
    Dim size    As Long
   
    ptr = lpVBHeader + &H2C
    ' Are allowed to write in the page
    VirtualProtect ByVal ptr, 4, PAGE_READWRITE, old
    ' Set a new address of Sub Main
    GetMem4 newAddress, ByVal ptr
    VirtualProtect ByVal ptr, 4, old, 0
   
    ' Remove startup form
    GetMem4 ByVal lpVBHeader + &H4C, ptr
    ' Get forms count
    GetMem2 ByVal lpVBHeader + &H44, count
   
    Do While count > 0
        ' Get structure size
        GetMem4 ByVal ptr, size
        ' Get flag (unknown5) from current form
        GetMem4 ByVal ptr + &H28, flag
        ' When set, bit 5,
        If flag And &H10 Then
            ' Unset bit 5
            flag = flag And &HFFFFFFEF
            ' Are allowed to write in the page
            VirtualProtect ByVal ptr, 4, PAGE_READWRITE, old
            ' Write changet flag
            GetMem4 flag, ByVal ptr + &H28
            ' Restoring the memory attributes
            VirtualProtect ByVal ptr, 4, old, 0
           
        End If
       
        count = count - 1
        ptr = ptr + size
       
    Loop
   
End Sub

' // Create binary code.
Private Function CreateAsm() As Long
    Dim hMod    As Long
    Dim lpProc  As Long
    Dim ptr     As Long
   
    hMod = GetModuleHandle(ByVal StrPtr("kernel32"))
    lpProc = GetProcAddress(hMod, "TlsGetValue")
   
    If lpProc = 0 Then Exit Function
   
    ptr = VirtualAlloc(ByVal 0&, &HF, MEM_RESERVE Or MEM_COMMIT, PAGE_EXECUTE_READWRITE)
   
    If ptr = 0 Then Exit Function
   
    ' push  tlsIndex
    ' call  TLSGetValue
    ' pop   ecx
    ' push  DWORD [eax]
    ' push  ecx
    ' jmp   DWORD [eax + 4]
   
    GetMem4 &H68, ByVal ptr + &H0:          GetMem4 &HE800, ByVal ptr + &H4
    GetMem4 &HFF590000, ByVal ptr + &H8:    GetMem4 &H60FF5130, ByVal ptr + &HC
    GetMem4 &H4, ByVal ptr + &H10:          GetMem4 tlsIndex, ByVal ptr + 1
    GetMem4 lpProc - ptr - 10, ByVal ptr + 6
   
    CreateAsm = ptr
   
End Function

Private Function MakeTrue(value As Boolean) As Boolean
    MakeTrue = True: value = True
End Function

Все API декларации я сделал в отдельной библиотеке типов - EXEInitialize.tlb. Пока найден один недостаток - не работают формы с приватными контролами, если разберусь в чем причина - исправлю. Работает только в скомпилированном варианте.
В архиве содержится несколько тестов.
1-й: создание формы в новом потоке, с возможностью блокировки ввода посредством длинного цикла.
2-й: обработка событий от объекта, метод которого вызван в другом потоке. Сразу скажу так делать нельзя и неправильно, т.к. передавать между потоками ссылку без маршаллинга опасно и может привести к глюкам, к тому же обработка события выполняется в другом потоке. Этот пример я оставил в качестве демонстрации работы многопоточности, а не для использования в повседневных задачах.
3-й: демонстрация изменения значения общей переменной в одном потоке и считывание его из другого.

Всем удачи!


Update: 27.05.2015

В IDE запуск осуществляется в главном потоке по желанию.
Добавлен 4-й тест - получение списка простых чисел в отдельном потоке.

Update: 01.06.2015

Добавлена нативная DLL которая экспортирует vbCreateThread.
Внесены изменения в код чтобы поддерживать эту возможность.
Скачать материалы.
UA6527P

Debugger
Продвинутый гуру
Продвинутый гуру
Аватара пользователя
 
Сообщения: 1667
Зарегистрирован: 17.06.2006 (Сб) 15:11

Re: Многопоточность в VB6 часть 4

Сообщение Debugger » 17.02.2015 (Вт) 12:49

Очень любопытно!

Ждем ревью Хакера :)

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Многопоточность в VB6 часть 4

Сообщение Хакер » 19.02.2015 (Чт) 2:19

Debugger писал(а):Ждем ревью Хакера :)

Я, конечно, не читал предыдущие части статьи, а только может быть мельком их просмотрел непосредственно публикации.

Но я вот что хочу сказать.

Мой многопоточный кирпич называется Stable Multithreading. Прямо так незамысловато и называется, это отражено везде: и в названии папки с проектом
Изображение
и в префиксе Smt (stable multithreading) в большинстве основных внутренних функций кирпича
Изображение
.

Ключевое слово здесь конечно же stable. Потому что цель была в том, что дать людям именно стабильную многопоточность в VB, потому что нестабильная многопоточность была доступна им ещё с прошлого века хотя бы через банальное использование CreateThread в лоб. Разные трюки могли немного преуменьшать вероятность наступление катастрофы в отдельно взятом потоке (а затем и во всём процессе), но не исключали этого полностью. А я задался целью сделать такую многопоточность, чтобы многопоточное приложение вообще никогда не падало, как это в силе для обычных однопоточных VB-приложений, если только они не ковыряются в памяти и не делают грязных трюков, а остаются в рамках VB-парадигмы безопасности.

Ну так вот, самое главное, что я хочу сказать: НЕ БУДЕТ НИКАКОЙ стабильной многопоточности в Standard EXE, пока скомпилированый проект не обеспечен изоляцией потоко-специфичных данных, то есть пока он не будет скомпилирован с настройкой Threading Model = Apartament Threaded (вместо Single Threaded), установленной в свойствах проекта, что заставит скомпилированный проект для потоко-специфичных данных (в частности — для глобальных переменных проекта) использовать TLS, а не секцию данных.

Однако великое разочарование состоит в том, что в режиме Standard EXE параметр Threading Model заблокирован и выставлен в Single Threaded, и его нельзя поменять, как это легко можно сделать в режиме ActiveX DLL или Active Control. В проектах типа ActiveX EXE он тоже заблокирован, но как бы под капотом выставлен как раз многопоточный режим, так что тут проблем нет. Нам нужно, чтобы и в Standard EXE проектах переключатель стоял в многопоточном положении, но он заблокирован.

Так что ещё раз: НЕ БУДЕТ никакой стабильности работы МП-процесса на VB6, пока потоко-специфичные данные лежат в секции данных (что вполне годится для реально-singlethreaded-программ), а не в TLS.

Но добиться этого не так-то просто, ведь переключатель в StandardEXE-проектах — заблокирован.

И проблема усугубляется вот чем: нет, вы не можете сказать «ну хорошо, я психологически готов к тому, что все мои глобальные переменные будут общими между всеми потоками, я просто буду иметь это в виду и напишу код, который учитывает это обстоятельство, и там, где мне действительно понадобится изоляция данных между потоками, я буду сам использовать TLS-механизм вручную». Увы, но это не сработает. Потому что в набор потоко-специфичных данных, которым нужна изоляция, входят не только все ваши глобальные, но и ряд внутрирантаймовых переменных, о которых вы не догадываетесь. То есть какими бы чудесным и совершенным не были бы лично вы и ваш вылизанный код, который бы использовал глобальные переменные очень аккуратно, и вдумчиво бы использовал TLS там, где это надо для межпоточной изоляции, ваш проект всё равно будет падать в случайные моменты времени, потому что изоляция тех структур, которые используются «под капотом» для работы VB, не обеспечена.

Один из примеров, которые я всегда привожу — апп-глобальный объект. Это «невидимый» объект, в пространстве имён и в зоне видимости в VB-коде нет идентификатора, через который можно обратиться к ссылке на этот апп-глобальный объект, но сам объект есть, и ссылка на него тоже есть, и сам объект очень важен, потому что, например, когда вы пишете App.Path, App — это одно из свойств этого аппглобального объекта, наряду с другими свойствами, такими как Forms, Printers или Screen. Так вот, апп-глобальный объект должен быть у каждого потока свой, и ссылка у каждого потока на апп-глобальный объект должна быть своя, и это именно так и происходит в проектах типа ActiveX DLL, ActiveX Control и ActiveX EXE, когда параметр Threading Model выставлен в Apartament Threaded, но в проекте Standard EXE ссылка на апп-глобальный объект будет храниться в секции данных — одна, на все VB-потоки, которые вы породите. И это неминуемо ведёт к беде, как бы вы ни пытались этого избежать косвенными методами.

То есть, к примеру, если вы скажете «ну окей, я просто не буду использовать App, Screen и тому подобные члены апп-глобального объекта», — это вас ни разу не спасёт от беды, потому что вне зависимости от того, используете ли вы его, проблемы будут, ибо один-на-все-потоки объект продолжает участвовать в подсчёте ссылок (AddRef/Release), а счётчик ссылок не thread-safe, насколько я помню, при каждом создании нового потока внутренности VB будут пытаться создать новый апп-глобальный объект для нового потока на месте старого объекта. В общем, каша.

Другой пример: как я уже сказал, в набор потоко-специфичных данных входят внутренние вещи (такие как ссылка на апп-глобальный объект), но и все глобальные переменные проекта тоже туда входят. И пусть вас устраивает тот факт, что глобальные переменные оказываются действительно глобальными, но тут происходит коллизия с кое-какой магией самого VB. Потому что VB-проект (живущий в скомпилированном виде — в том числе) устроен так, что он волшебным образом знает, что и где у него лежит. В том смысле, что он знает обо всех своих глобальных переменных: он знает где они (смещения в памяти), он знает сколько их, какого они размера (в байтах) и какого типа. Исходя из типа, он знает, как из зачистить и деинициализировать: например, если это ссылка на объект, то перед занулением будет вызван IUnknown::Release, если это стринг — перед занулением будет вызвана SysFreeString, если это Variant-переменная, перед занулением будет вызвана соотвествующая функция для уничтожения Variant-значения, если это массив — то функция для уничтожения SA-дескриптора и данных, а если же это простые скалярные переменные — они будут просто перезаписаны нулями. VB использует это для зачистки и сбора мусора, поэтому там хранятся только сведения, необходимые для этой задачи (адресам, размеры, типы), и не хранятся, например, имена переменных. Что ещё интересно: этот механизм используется и для обычных модулей, и для классов. Что касается обычных модулей, а значит и всех глобальных (да и не только глобальных) переменных проекта, то механизм для них активируется дважды — первый раз при рождении нового потока, а второй раз — при смерти этого потока. На самом деле, я говорю не правильно, так как речь идёт не о рождении и смерти потока, а о рождении и смерти контекста (я сам этот термин, как известно, придумал), потому что не для каждого потока процесса будет существовать контекст, контекст может родиться значительно позже рождения потока, и смерть контекста не всегда предшествует смерти потока.

Так вот, то, что я называю контекстами (ещё раз напомню: контексты — это результат декартового произведения множества потоков процесса на множество VB-проектов, живущих в АП процесса), каждый раз, когда требуется инициализация контекста, механизм срабатывает первый раз и зачищает память, отведённую под глобальные (и не только) модульные переменные и инициализирует их начальными значениями (0, Nothing, Empty, vbNullString), и каждый раз, когда близится смерть контекста, механизм срабатывает второй раз и зачищает всё то, что не зачистил программист явно, вызывая все Release-ы для объектных ссылок и уничтожая все остальные данные.

И этот механизм работает в том числе и в проектах типа Standard EXE: да только вот все модульные переменные при этом живут в секции данных и являются общими для всех потоков процесса (из которых, по замыслу создателей, в проектах подобного типа только 1 поток будет работать с этими данными), что нисколько не волнует механизм зачистки, который в случае завершения одного из VB-потоков придёт и зачистит все модульные переменные (и перезапишет всё нулями), которые у нас, как я уже сказал, общие для всех потоков. Таким образом, у нас не только нет изоляции глобальных переменных между потоками, у нас вообще глобальные переменные становятся неюзабельными, потому что при каждом завершении одного из потоков мы теряем все значения.

Кто-то может подумать, что он сможет избежать затирания всех глобальных переменных, если потоки будут самозавершаться вызовом функции ExitThread. Действительно, зачистки не будет, но выходить из потока таким образом — это глубоко неправильный метод, потому что в этом случае мы не даём рантайму освободить выделенные для потока внутенние структуры, и таким образом создаём утечку памяти. Я думаю, не должно быть удивительным, что из VB-потоков нельзя выходить вызовом ExitThread: даже в Си при использовании стандартной библиотеки и многопоточности, предписывается выходить из потока вызовом _endthread(), чтобы стандартная библиотека могла освободить выделенные под поток ресурсы. Чего уж говорить о VB.

Так вот, без переключения опции Threading Model и принуждения компилятора VB к использованию TLS для потоко-специфичного барахла, мы не получим никакой стабильности в работы МП-процесса.

Естественно, я не сдался и не опустил руки на этом моменте и выработал два решения:
  • Как минимум в своём кирпиче сделать эмуляцию наличия использования TLS. Т.е. проект был скомпилирован без использования настоящего TLS, но в реальности усилиями кирпича создаётся эффект использования TLS (то есть у разных потоков их потоко-специфичные данные действительно хранятся в разных местах в памяти и каждый поток имеет свою копию (свой экземпляр) набора глобальных данных).
  • Как максимум — разреверсить и хакнуть (сама эта отдельно взятая затея тоже зашла очень далеко, но это совершенно другая история) VB IDE и компилятор до такой степени, чтобы можно было принудить VB компилировать даже StandardEXE-проекты с использованием TLS для потоко-специфичных данных. Сделать Add-In для VB, который позволит разблокировать параметр Threading Model в режиме Standard EXE и менять его там.

У меня есть определённые внутренние правила, своего рода политика в отношении кода, проектов и Add-in'ов. Она гласит: add-in'ы — это, конечно, хорошо, но надо делать проекты так, чтобы они могли быть скомпилированными на свежеустановленной VB IDE без всяких нестандартных add-in'ов, и нехорошо, если какой-то проект не компилируется без установки какого-то Add-In'а, и совсем плохо, если без Add-In'а он всё-таки компилируется, но работает неправильно, что выясняется только потом.

Поэтому я решил так: не важно, сделаю ли я вообще когда-нибудь такой Add-in, кирпич обязан обеспечивать стабильную работу проектов, скомпилированных и без подобного Add-in'а (то есть скомпилированных неподходящим параметром Threading Model). Не выпущу Add-in — многопоточные проекты будут работать всё равно и будут работать стабильно за счёт использования первого решения (эмуляции TLS). Выпущу Add-in — хорошо; если у того, кто компилирует многопоточный проект, использующий мой кирпич, окажется ещё и мой Add-in, то мой кирпич будет просто отдыхать и не будет ничего эмулировать (будет использоваться родное штатное TLS, код будет работать очень быстро), но если Add-In-а не окажется, кирпич сэмулирует недостающее TLS, код всё равно будет стабильно работать, хоть и медленнее.

Поэтому большая (можете ставить ударение и так, и так) часть работы по созданию кирпича — это создание механизма эмуляции TLS на пустом месте, которую я назвал EHBTLS (тут EHB расшифровывается как exception handling-based):
Изображение

EHBTLS — это весьма непростая штука (тем не менее, она на 100 процентов готова, если я правильно помню). Ты, Debugger, можешь понять, насколько серьёзные вещи тут происходят, найдя в имени одной из функций слово «ModRM», если конечно не забыл из моих уроков, что это такое. С некоторой долей пафоса можно сказать, что EHBTLS — это маленькая виртуальная машина, которая виртуализирует обращения к потоко-специфичной информации, идущие к секции данных, и перенаправляет их на потоки-специфичные хранилища в зависимости от того, какой поток (и в какое место) совершает обращение (чтение или запись).

Так что проблема в общем решимая, но без её решения ни о какой стабильности не может идти и речи. Да, после инициализации контекста рантайма для потока, новый поток не падает при первом же обращении к рантайму, как это было раньше (при использовании голого подхода с CreateThread). Да, это прогресс, но этого совершенно мало для стабильной работы, иначе бы я опубликовал кирпич ещё в октябре 2011-го, когда нашёл способ инициализации рантайма для нового потока. И не надо делать ложных выводов на основании того, что тестовые проектики вроде бы как работают. Это только кажется. Они работают благодаря удачи и везению, но на деле будут глючить и падать, и как это всегда есть в мире многопоточного софта, отладка многопоточных багов — это очень сложная отладка, потому что они проявляются раз на тысячу случаев. Стоит поверить мне, я не одну ночь провёл, изучая вопросы стабильности.

Я не знаю точно, но предполагаю, что у автора эта проблема никак не решена: по крайней мере, судя по простоте описания того, как он добился работы нового решения, кажется, что никаких мер не предпринималось. Я не знаю, в курсе ли автор вообще о существовании этой проблемы. Если в курсе, то хорошо, а если не просто в курсе, а ещё и решил, то стоит рассказать, как.

Я, естественно, хочу доделать свой кирпич и призываю дождаться его выхода, хотя могу честно сказать, что сейчас не представляю, когда у меня будет время вернуться за работу над ним. У меня была мысль опубликовать незаконченный код где-нибудь на GitHub-е или Bitbucket-е и дать сообществу доделать его до конца, но я боюсь, что вряд ли кто-то разберётся сам, что и как там доводить до ума и доделает всё это как подобает — всё-таки там код очень замудрёный — я сейчас вспоминаю, как пару лет назад мне потребовалось больше, чем целый вечер, чтобы просто глядя на код разобраться и понять: что я закончил, на чём остановился, что осталось доделать и какие проблемы разрешить, всего спустя год после того, как я приостановил не прекращавшуюся в течение месяца работу над кодом.

Конечно, я могу сказать, что я сразу замахнулся на многое: и просто инициализация рантайма для нового потока (что безусловно необходимо), и опциональная эмуляция TLS (что тоже безусловно необходимо), и забота о том, чтобы кирпич работал со всеми известными науке версиями (билдами) библиотеки MSVBVMx0.dll (включая, кажется, даже 5-ую версию), и даже такое устройство кирпича, что он способен обеспечить стабильную многопоточность даже в том случае, если в АП одного и того же процесса окажутся загруженными несколько разных скомпилированных VB-проектов, каждый из которых нуждается в многопоточности, и каждый из которых использует разную версию рантайм-библиотеки (за счёт SxS-перенаправления или за счёт переименования библиотеки в импорте — тут вот есть товарищи, которые хотят избавиться от импорта msvbvm, а были и такие, которые хотели её переименовать в, к примеру, fun.dll, поменяв имя файла DLL и подправив таблицу импорта). Вот из-за всего этого: из-за собственно самой многопоточности, эмуляции TLS, поддержки ранне- и поздне- подгружаемых VB-модулей, нуждающихся в многопоточности, и использующих конкурирующие версии рантаймов, опционально ещё и переименованные на уровне файлов — код кирпича и получился очень сложным и требующим много времени и сил на разбирательство с ним.

Насколько я помню (а я могу существенно ошибаться), я остановился вот на чём: стремясь уложиться в парадигму VB, я следовал принципу, что каждый поток должен автоматически завершаться тогда, когда в нём не остаётся ничего, что удерживало бы его в живом состоянии, по аналогии с тем, как обычный Standard EXE, не требуя явного цикла, живёт, пока живут объекты, поддерживающие жизнь процесса, такие как открытый экземпляр формы. Где-то там была проблема с тем, что поток вроде бы как завершался, но через диспетчер задач (и отладчик) было видно, что поток продолжает выполняться. Я не знаю, в курсе ли ты, Анатолий, что делает такая штука, как MSO-компонент-менеджер, но тебе ещё придётся столкнуться с ним, чтобы заставить эту часть жизненного цикла потока (я говорю про его смерть) работать чисто.

А вообще же, мне вспоминается высказывание из «Собачьего сердца», не дословно: зачем испытывать невероятные трудности, искуственно создавая человека, когда любая баба родит его когда угодно? Искусственное сердце для человека занимает целый ящик, устроено невероятно сложно и требует бешеных денег на своё создание и обслуживание, и в общем-то не дарует человеку полноценную жизнь, а ведь естественное сердце у людей вырастает само собой и не требует миллионы долларов и многих часов работы учёных, чтобы появиться и функционировать. И вряд ли человек, пока у него нет с ним проблем, задумывается, насколько это чудесно, что оно просто работает, и насколько сложно заменить его работу, если его вдруг не станет. (Тут я понял, что кто-то может посчитать, будто бы я ошибочно думаю, что в «Собачьем сердце» кто-то создавал искусственное сердце — так вот нет, то, что в двух соседних предложениях упоминаются сначала «Собачье сердце», а потом аппарат «искуственное сердце» — это чистой воды случайность, и связи между ними искать не надо)

Это я к тому, что очень сложно для готового скомпилированного экзешника, при том не совсем правильно скомпилированного (без использования TLS), приделать нечто, что обеспечивало бы стабильную многопоточность вне зависимости от версии рантайма и прочих факторов. Это можно, но это очень сложно, при том, что это было бы точно так же «можно», хоть и в миллион раз легче, если бы у меня был доступ к исходникам VB — достаточно было бы совсем чуть-чуть подправить исходники рантайма, просто сделав функцию, порождающую новый полноценный VB-поток, экспортируемой (она и сейчас там есть, но она не экспортируется, поэтому приходится очень сложно её искать), и подправив исходники IDE так, чтобы в режиме Standard EXE параметр «Threading Model» не блокировался, и автор проекта мог по желанию выбрать использование TLS для глобальных переменных. Всё, две маленькие вещи (экспорт функции наружу и отмена чисто косметической блокировки параметра) решили бы проблему на высшем уровне.

Но пока у меня нет исходников, а значит приходится приконтачиваться к уже скомпилированной (чуть-чуть не так, как нам надо) IDE, к уже скомпилированным (чуть-чуть не так, как нам надо) рантаймам (коих версий — куча, и все с небольшими отличиями внутреннего устройства) и к компилируемым (но чуть-чуть не так, как нам надо) выходным VB-проектам. Что сродни гигантским медицинским усилиям, прилагаемым к ребёнку, которому не повезло родиться вообще без ног, чтобы дать ему возможность ходить, на фоне того факта, что в общем-то ребёнок с ногами может быть рождён без особых трудозатрат.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Re: Многопоточность в VB6 часть 4

Сообщение The trick » 19.02.2015 (Чт) 22:20

Хакер, спасибо за ревью!
Насчет обращений к глобальным объектам. Действительно апп-глобальный объект один, но при правильном обращении к нему никаких неприятностей быть не должно. Не зря я эту публикацию назвал "часть 4", т.к. для того чтобы читать эту, сначала нужно прочитать предыдущие. Например в 1-й я писал что для обращение к объектам в другом потоке нужно использовать маршаллинг, в 3-ей для обращения к разделяемым данным я использовал мьютекс. Обращение к глобальным данным всегда должно быть засинхронизировано к примеру критическими секциями и т.п. Насчет этого никаких вопросов не должно быть в принципе. Если я собираюсь писать многопоточную программу, то я должен четко представлять что это такое и избегать специфичных проблем многопоточности (детлоки, состояние гонки и т.п.), а если я этого не понимаю то и многопоточность для меня - враг. Если разграничить обращение к глобальным объектам, то все будет в порядке. Ссылки на объекты будут корректно считаться и он не будет уничтожаться когда не нужно и пересоздаваться вновь.
Хакер писал(а): а счётчик ссылок не thread-safe, насколько я помню, при каждом создании нового потока внутренности VB будут пытаться создать новый апп-глобальный объект для нового потока на месте старого объекта

В данной реализации нет.
Хакер писал(а): например, если это ссылка на объект, то перед занулением будет вызван IUnknown::Release, если это стринг — перед занулением будет вызвана SysFreeString, если это Variant-переменная, перед занулением будет вызвана соотвествующая функция для уничтожения Variant-значения, если это массив — то функция для уничтожения SA-дескриптора и данных, а если же это простые скалярные переменные — они будут просто перезаписаны нулями.

В данной реализации такого не наблюдается - глобальные переменные уничтожаются только после завершения главного потока.
Хакер писал(а):Таким образом, у нас не только нет изоляции глобальных переменных между потоками, у нас вообще глобальные переменные становятся неюзабельными, потому что при каждом завершении одного из потоков мы теряем все значения.

Тоже самое. Также в предыдущей публикации все переменные во всех потоках при Single-threaded были общими и не занулялись при создании потока иначе бы приложение не работало.
Хакер писал(а):Я не знаю точно, но предполагаю, что у автора эта проблема никак не решена: по крайней мере, судя по простоте описания того, как он добился работы нового решения, кажется, что никаких мер не предпринималось. Я не знаю, в курсе ли автор вообще о существовании этой проблемы. Если в курсе, то хорошо, а если не просто в курсе, а ещё и решил, то стоит рассказать, как.

Насчет разделяемых данных я никак не заботился, т.к. во-первых считаю что механизм взаимодействия с разделяемыми данными должен оставаться на совести программиста. Например есть общий массив данных который представляет собой изображение, и я обрабатываю эти данные в другом потоке. Мне нет смысла как-то синхронизировать обращения, я просто в одном потоке обрабатываю, в другом по таймеру (или еще как-нибудь) вывожу результат. А если этот массив является данными. нуждающимися в атомарном доступе (как в предыдущем примере с внедрением), то я синхронизирую (из-за этого получаю замедление). Во-вторых после трюка с копирование хидера у меня не стали затиратся данные как раньше, поэтому все переменные стали действительно общими и не обнуляются при создании потока. Также хочу отметить, что я особо не тестировал все это дело, а выложил сюда дабы другие могли "обкатать" и сказать мне о багах, как например Вы. У Вас больше опыта работы в этом деле и конечно я прислушиваюсь к тому что Вы говорите и беру на заметку. Я сразу написал -
The trick писал(а):Сразу скажу что все о чем я пишу является моим личным исследованием и может в чем-то не соответствовать действительности

-
Хакер писал(а):Я не знаю, в курсе ли ты, Анатолий, что делает такая штука, как MSO-компонент-менеджер, но тебе ещё придётся столкнуться с ним, чтобы заставить эту часть жизненного цикла потока (я говорю про его смерть) работать чисто.

Я не в курсе. Я не реверсил рантайм настолько глубоко и не сталкивался с этим. Обычно я что-то исследую в MSVBVM что-то методом от противного (если например функция вызывает кучу других функций и трассировать код ужасно долго), если что-то где-то не работает я смотрю где изменяются данные которые не должны изменятся и анализирую это делая так чтобы они туда не писались, и так пока не будет работать программа. Возможно я встречался с этим менеджером косвенно. Если будет необходимость то я изучу этот вопрос самостоятельно; если Вы напишете об этом, то изучу исходя из Вашего описания.
UA6527P

Jack Ferre
Продвинутый пользователь
Продвинутый пользователь
Аватара пользователя
 
Сообщения: 132
Зарегистрирован: 17.02.2014 (Пн) 14:31
Откуда: Казахстан, Костанай

Re: Многопоточность в VB6 часть 4

Сообщение Jack Ferre » 21.02.2015 (Сб) 17:53

Win8 - Всё ок.

WinXP - При закрытии потока (в некоторых случаях) зависает процесс (так же почему-то панель задач) (загрузки ЦП от процесса нет), висит секунд 15 и исчезает.

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Re: Многопоточность в VB6 часть 4

Сообщение The trick » 21.02.2015 (Сб) 18:02

Jack Ferre писал(а):WinXP - При закрытии потока (в некоторых случаях) зависает процесс (так же почему-то панель задач) (загрузки ЦП от процесса нет), висит секунд 15 и исчезает.

В каких именно случаях, и каким образом закрывается поток?
UA6527P

Jack Ferre
Продвинутый пользователь
Продвинутый пользователь
Аватара пользователя
 
Сообщения: 132
Зарегистрирован: 17.02.2014 (Пн) 14:31
Откуда: Казахстан, Костанай

Re: Многопоточность в VB6 часть 4

Сообщение Jack Ferre » 21.02.2015 (Сб) 18:13

Просто исчезает. Без сообщения о падении и записи в журнале приложений.

Закоментировал вот это:
HeapFree hHeap, 0, ByVal lpNewHdr
Всё ок. Видимо VB заботится об очистке заголовка хоть Вы его сами и создали.

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Re: Многопоточность в VB6 часть 4

Сообщение The trick » 21.02.2015 (Сб) 18:31

Jack Ferre писал(а):Всё ок. Видимо VB заботится об очистке заголовка хоть Вы его сами и создали.

Такого не может быть, т.к. по умолчанию заголовок располагается в секции .text.
Ты (давай на "ты") сказал что при закрытии потока висит процесс. Каким образом ты закрываешь поток? Или он сам закрывается?
UA6527P

Jack Ferre
Продвинутый пользователь
Продвинутый пользователь
Аватара пользователя
 
Сообщения: 132
Зарегистрирован: 17.02.2014 (Пн) 14:31
Откуда: Казахстан, Костанай

Re: Многопоточность в VB6 часть 4

Сообщение Jack Ferre » 21.02.2015 (Сб) 20:21

Сорри за большую паузу

The trick писал(а):Каким образом ты закрываешь поток?

Пример 1: при закрытии формы, Пример 2: при закрытии msgbox-а.

Я изменил Пример 2 -> убрал вывод msgbox-а, что бы каждый не закрывать вручную. Открываю 10+ приложений, в каждом щелкаю по несколько раз "Pause". На части из них счетчик потоков в диспетчере задач не падает (после sleep 5000) до 1, они зависают. Без очистки заголовка на всех счетчик падает до 1 потока.

UPD:
Уточнение по
HeapFree hHeap, 0, ByVal lpNewHdr
Сам вызов успешный. Проблема перед выходом из функции.
Последний раз редактировалось Jack Ferre 23.02.2015 (Пн) 9:11, всего редактировалось 2 раз(а).

The trick
Постоялец
Постоялец
 
Сообщения: 774
Зарегистрирован: 26.06.2010 (Сб) 23:08

Re: Многопоточность в VB6 часть 4

Сообщение The trick » 21.02.2015 (Сб) 21:20

Посмотрю...
UA6527P

ger_kar
Продвинутый гуру
Продвинутый гуру
Аватара пользователя
 
Сообщения: 1957
Зарегистрирован: 19.05.2011 (Чт) 19:23
Откуда: Кыргызстан, Иссык-Куль, г. Каракол

Re: Многопоточность в VB6 часть 4

Сообщение ger_kar » 24.02.2015 (Вт) 18:38

Прочитал внимательно тему (некоторые моменты перечитал несколько раз) и подумалось вот о чем. А есть ли смысл городить огород и непременно использовать Standart EXE вместо ActiveX? Что такого специфичного содержит проект Standart EXE, чего нельзя было бы добиться используя ActiveX EXE?
Бороться и искать, найти и перепрятать

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Многопоточность в VB6 часть 4

Сообщение Хакер » 24.02.2015 (Вт) 18:41

ger_kar писал(а):Что такого специфичного содержит проект Standart EXE, чего нельзя было бы добиться используя ActiveX EXE?

Возможность запуска не из под администратора? ActiveX EXE при запуске проверяет свою регистрацию, и если он не зарегистрирован — пытается это сделать, что естественно заканчивается тотальным обломолом, если процесс запущен непривилегированным пользователем.

Этот как минимум.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

ger_kar
Продвинутый гуру
Продвинутый гуру
Аватара пользователя
 
Сообщения: 1957
Зарегистрирован: 19.05.2011 (Чт) 19:23
Откуда: Кыргызстан, Иссык-Куль, г. Каракол

Re: Многопоточность в VB6 часть 4

Сообщение ger_kar » 24.02.2015 (Вт) 18:54

Вообще конечно, да, это проблема. Но может поработать над её решением? Это гораздо проще решения проблемы привнесения многопоточности в стандартный проект (по крайней мере мне думается что проще).
Бороться и искать, найти и перепрятать

NashRus
Постоялец
Постоялец
 
Сообщения: 386
Зарегистрирован: 18.03.2006 (Сб) 1:16

Re: Многопоточность в VB6 часть 4

Сообщение NashRus » 26.02.2015 (Чт) 9:01

я говорил вроде уже.
регистрацию ActiveX EXE перенес в ветку Current User и он нормально запускался.
т.е. некий ланчер, заносит регистрацию в Current User и стартует ActiveX EXE с расширением .bin, например, чтобы пользователя не смущать.

ну так костыль, если очень хочется.

Old_Maple
Обычный пользователь
Обычный пользователь
 
Сообщения: 54
Зарегистрирован: 25.10.2016 (Вт) 12:03

Re: Многопоточность в VB6 часть 4

Сообщение Old_Maple » 05.04.2017 (Ср) 15:15

Анатолий (The trick), добрый день!
Я пытаюсь использовать Ваш опыт многопоточности при формировании множества террейнов в одном ландшафте.
Описание работы программы:
После запуска программы в ландшафте формируется 9 секторов-террейнов.
Камера устанавливается в центре центрального сектора.
Пол мере продвижения по ландшафту и после пересечения границы сектора-террейна образуются 3 новых сектора-террейна, прилегающих к пересечсенному террейну, взамен трех старых. Террейны между собой соединяются посредством экспоненциальной функции. В дальнейшем попробую их состыковать NURBS-поверхностями.
Движение по ландшафту осуществляется клавишами W,S,A,D - вперед, назад, влево и вправо соответственно. Q и Z - вверх и вниз. Кстати, если подняться достаточно высоко, то при движении можно увидеть как формируются новые секторы-террейны по направлению движения.
Суть в том, что я использую вызов API-функций в потоке таких, например, как memcpy.
Код: Выделить всё
Public Declare Sub memcpy Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

Я бОльшую часть кода заимствовал из Вашей программы-примера Landscape и примеров программ Михаила (Mikle) - Cam.
Кстати, пришлось через Declare объявить еще одну функцию, которой нет в Вашей tlb:
Код: Выделить всё
Private Declare Function WaitForMultipleObjects Lib "kernel32" (ByVal nCount As Long, lpHandles As Long, ByVal bWaitAll As Long, ByVal dwMilliseconds As Long) As Long

Она мне нужна была для синхронизации окончания всех потоков, формирующих сегменты террейнов.
Но, к сожалению, при компиляции и запуске программы, многопоточность срабатывает не всегда корректно. При размерах секторов-террейнов меньших 128 (переменная MapSize) вылеты программы случатся реже. Но при увеличении размеров - вылеты чаще. Хотя в IDE работает все нормально. Закономерности вылета программы, увы, найти не могу. Быть может влияет время отработки потока? Внутри циклов потока стоит DoEvents. Быть может что-то не так делаю. Если есть возможность что-то посоветовать, то с нетерпением жду Вашего совета.
Сам пример здесь:
https://yadi.sk/d/oNmPgpBm3GgVV2
Последний раз редактировалось Old_Maple 11.04.2017 (Вт) 23:32, всего редактировалось 1 раз.
Veritas est aeterna!

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Многопоточность в VB6 часть 4

Сообщение Mikle » 05.04.2017 (Ср) 19:00

Old_Maple писал(а):Закономерности вылета программы, увы, найти не могу.

Я бы для начала погонял версию без многопоточности, чтобы проверить, что именно там проблемы.
А вообще, какая задача? Бесконечный неповторяющийся ландшафт? Или с большим периодом повтора сойдёт? Или сойдёт просто достаточно большой размер, вместо бесконечности?
Зачем вообще приплетать сюда мультитрединг? Ладно, пусть генерация трёх кусков, это тяжёлая задача для одного такта. Но зачем производить её за один такт, можно ведь, к примеру, начинать генерировать тайл, когда приближаешься к краю ближе 25% его размера. Зачем вообще тайлы, можно генерировать каждый раз одну строку с нужной стороны.

Old_Maple
Обычный пользователь
Обычный пользователь
 
Сообщения: 54
Зарегистрирован: 25.10.2016 (Вт) 12:03

Re: Многопоточность в VB6 часть 4

Сообщение Old_Maple » 05.04.2017 (Ср) 20:23

Спасибо, Mikle, что откликнулся!
Mikle писал(а):А вообще, какая задача?

Хочу попробовать создать ландшафт, который бы генерировался однозначно и не имел бы повторяемости на промежутках от -2 147 483 647 до 2 147 483 648, используя предсказуемый генератор ПСЧ. Чтобы в любом месте на этом ландшафте строить объекты, не храня саму карту высот в памяти.
Mikle писал(а):Бесконечный неповторяющийся ландшафт?

Да, все правильно! :)
Mikle писал(а):Или с большим периодом повтора сойдёт?

У меня период повтора пока получается через 4 294 967 296 пикселей. При такой повторямости можно создать земную поверхность с разрещением порядка 5x5 кв.мм
Mikle писал(а):Или сойдёт просто достаточно большой размер, вместо бесконечности?

Надо, чтобы был не просто большой размер, но и высокая предсказуемость высоты в любой точке, чтобы точно привязать объекты, которые находятся на большом удалении.
Mikle писал(а):Зачем вообще приплетать сюда мультитрединг?

Дело в том, что на серверах одновременно может быть несколько пользователей. Для каждого из них формируется свой террейн (локально), например из 9-ти сегментов. Они могут строить там свои объекты, привязываясь к местности. Если все это делать в одном потоке, то сервер не справится и с десятком игроков. А мультитрейдингу, работающему на многоядерных процессорах, вполне это под силу.
Mikle писал(а):Но зачем производить её за один такт, можно ведь, к примеру, начинать генерировать тайл, когда приближаешься к краю ближе 25% его размера.

Тогда это не решает задачу скачкообразного перемещения ГГ ( а-ля телепортация). :)
Mikle писал(а):Зачем вообще тайлы, можно генерировать каждый раз одну строку с нужной стороны.

Можно, разумеется. Но три маленьких тайла (в разных потоках) генеряться быстрее, чем один - такого же размера как три. Это я уже проверил. :)
Veritas est aeterna!

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Многопоточность в VB6 часть 4

Сообщение Mikle » 05.04.2017 (Ср) 21:17

Old_Maple писал(а):Тогда это не решает задачу скачкообразного перемещения ГГ ( а-ля телепортация).

Телепортация, это не плавное движение, вполне допустимо, если изображение на долю секунды тормознёт.
Мне кажется, что для начала нужно определиться с максимально быстрым предсказуемым ГСЧ, являющимся ф-цией с двумя параметрами.

Old_Maple
Обычный пользователь
Обычный пользователь
 
Сообщения: 54
Зарегистрирован: 25.10.2016 (Вт) 12:03

Re: Многопоточность в VB6 часть 4

Сообщение Old_Maple » 05.04.2017 (Ср) 23:07

Mikle писал(а):Мне кажется, что для начала нужно определиться с максимально быстрым предсказуемым ГСЧ, являющимся ф-цией с двумя параметрами.

Я описал его здесь: http://bbs.vbstreets.ru/viewtopic.php?f=1&t=56093
Veritas est aeterna!

Mikle
Изобретатель велосипедов
Изобретатель велосипедов
Аватара пользователя
 
Сообщения: 4147
Зарегистрирован: 25.03.2003 (Вт) 14:02
Откуда: Туапсе

Re: Многопоточность в VB6 часть 4

Сообщение Mikle » 06.04.2017 (Чт) 10:12

Old_Maple писал(а):Я описал его здесь: viewtopic.php?f=1&t=56093

Да, я помню. Но там 2D вариант достаточно громоздкий, у меня, к примеру, есть такой вариант:
Код: Выделить всё
Function Noise(ByVal x As Long, ByVal y As Long) As Single
  Dim d As Double

  d = 2.669 * (x + 0.78) * (y + 3.722)
  d = (d - Int(d)) * (x + 2.18) * (y + 1.226)
  Noise = d - Int(d)
End Function

Далее, конкретно для генерации карты высот хорошее распределение не особо требуется, а вот чтобы не было заметной повторяемости - желательно. Может, проигнорировав распределение, получится ещё повысить быстродействие?
Ещё далее, сам по себе шум в качестве карты высот плох, в той теме его предлагается сглаживать, не уточняя как. Гораздо лучше подходит шум Перлина или октавный шум, а ещё интереснее возвести его в квадрат: http://www.gamedev.ru/code/forum/?id=179462#m6
Впрочем, это всё имеет мало отношения к мультитредингу, может обсудить где-нибудь отдельно?

Old_Maple
Обычный пользователь
Обычный пользователь
 
Сообщения: 54
Зарегистрирован: 25.10.2016 (Вт) 12:03

Re: Многопоточность в VB6 часть 4

Сообщение Old_Maple » 06.04.2017 (Чт) 11:48

Mikle писал(а):Впрочем, это всё имеет мало отношения к мультитредингу, может обсудить где-нибудь отдельно?

Согласен, генерацию карты высот можно обсудить в другом топике.
Но вот что делать с мультитредингом? Почему маленькие тайлы (64x64) он проглатывает, а большие, требующие на обработку большее количество миллисекунд - "срывает"?
Veritas est aeterna!

Old_Maple
Обычный пользователь
Обычный пользователь
 
Сообщения: 54
Зарегистрирован: 25.10.2016 (Вт) 12:03

Re: Многопоточность в VB6 часть 4

Сообщение Old_Maple » 06.04.2017 (Чт) 19:49

В общем, Mikle, разобрался в чем дело, посыпаю себе голову пеплом. :)
Если в потоке вызываются методы Direct3D или обращение к ее типам и классам (типа -
Код: Выделить всё
vtxBufTerr(SC.m, SC.n).Lock 0, LenVrtxTerr, ptr, 0
CopyMemory ByVal ptr, VertTerr(0, 0), LenVrtxTerr
vtxBufTerr(SC.m, SC.n).Unlock

то необходимо "симофорить" (RenderFlag = False) в конструкции:
Код: Выделить всё
Do
    DoEvents
    DoControl
       
    If RenderFlag Then
      d3dev.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &H7FC0FF, 1, 0
      d3dev.BeginScene
      D3DXMatrixIdentity Mtx
      d3dev.SetTransform D3DTS_WORLD, Mtx
      SetRenderWaterMode
      For m = 0 To SqrSecMap1
        For n = 0 To SqrSecMap1
          d3dev.SetStreamSource 0, vtxBufWat(m, n), 0, LenVertWat
          d3dev.SetIndices idxBufWat(m, n)
          d3dev.DrawIndexedPrimitive D3DPT_TRIANGLELIST, 0, 0, vtxCtWat, 0, idxCtWat / 3
        Next
      Next
      SetRenderTerrainMode
      For m = 0 To SqrSecMap1
        For n = 0 To SqrSecMap1
          d3dev.SetStreamSource 0, vtxBufTerr(m, n), 0, LenVertTerr
          d3dev.SetIndices idxBufTerr(m, n)
          d3dev.DrawIndexedPrimitive D3DPT_TRIANGLELIST, 0, 0, vtxCtTerr, 0, idxCtTerr / 3
        Next
      Next
      d3dev.EndScene
      d3dev.Present ByVal 0, ByVal 0, 0, ByVal 0
   End If
   FPS = FPS + 1
Loop Until IsStop

Такая вот заковыка получилась. :)
Veritas est aeterna!


Вернуться в The trick

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 0

    TopList