Потоки в Visual Basic (исходники), Різне, Програмування, статті

Зміст


Тільки тому, що Ви повинні,
щось робити не завжди означає,
що у Вас вийде …


З появою оператора AddressOf, частина індустрії ПО стала орієнтуватися на авторів, які показують як з використанням Visual Basic вирішувати раніше неможливі завдання. Інша частина швидко охопила консультантів, допомагають користувачам, що мають проблеми при вирішенні таких завдань.


Проблема не в Visual Basic або в технології. Проблема в тому, що більшість авторів застосовують одне і теж правило до AddressOf методикам, що більшість компаній по розробці ПО вважають, що якщо Ви повинні щось зробити, то Ви зможете. Ідея про те, що застосування самої нової і останньої технології повинно, за визначенням, бути найкращим вирішенням проблеми, широко поширена в індустрії ПЗ. Ця ідея невірна. Розгортання технології повинно управлятися насамперед проблемою, яку необхідно вирішити вирішити, а не технологією, яку хтось пробує Вам впарити ;).


Дуже погано, що через те, що компанії часто нехтують згадуванням про обмеження і недоліки їх інструментальних засобів, автори інколи бувають не в змозі звернути увагу читачів на слідства деяких методик, які вони описують. І журнали і книги іноді нехтують своєю відповідальністю, щоб упевнитися, що практика програмування, яку вони описують, є прийнятною.


Програмісту дуже важливо вибрати необхідний інструмент для своєї роботи. Це – ваше завдання, щоб розробити код, який працює тепер не тільки на одній специфічній платформі, але також працює на різних платформах і системних конфігураціях. Ваш код повинен бути добре документований і підтриманий іншими програмістами, які беруть участь в проекті. Ваш код повинен слідувати правилам, продиктованими операційною системою або стандартами, які Ви використовуєте. Відмова так робити може привести до проблем в майбутньому, оскільки системи і програмне забезпечення постійно удосконалюються.


Недавні статті в Microsoft Systems Journal і Visual Basic Programmer “s Journal представили програмістам на Visual Basic можливість використання функції API CreateThread, щоб безпосередньо підтримувати багатопотоковий режим під Visual Basic. Після цього, один читач поскаржився, що моя книга Visual Basic Programmer “s Guide to the Win32 API є неповною, тому що я не описав в ній цю функцію і не продемонстрував цю технологію. Ця стаття – частково є відповіддю цього читачеві, і частково – відповіддю на інші статті, написаними на цю тему. Ця стаття також є доповненням до глави 14 моєї книги “Розробка ActiveX компонент на Visual Basic 5.0” щодо нових можливостей, забезпечуваних Visual Basic 5.0 Service Pack 2.


Швидкий огляд багатопоточності


Якщо Ви вже добре розбираєтеся в технології багатопотокового режиму, то Ви можете пропустити цей розділ і продовжувати читання з розділу, названого “Що нового в Service Pack 2.”


Кожен, хто використовує Windows, знає, що Windows здатне робити більше ніж одну річ одночасно. Може одночасно виконувати декілька програм, при одночасному програванні компакт-диска, посилці факсу та пересилання файлів. Кожен програміст знає (або повинен знати) що ЦЕНТРАЛЬНИЙ ПРОЦЕСОР комп’ютера може тільки виконувати одну команду одночасно (проігноруємо існування багатопроцесорних машин). Як єдиний ЦЕНТРАЛЬНИЙ ПРОЦЕСОР може виконувати безліч завдань?


Це робиться швидким перемиканням між багатьма завданнями. Операційна система містить в пам’яті всі програми, які запущені в даний момент. Це дозволяє центральному процесору виконувати програми по черзі. Кожен раз відбувається перемикання між програмами, при цьому змінюється вміст внутрішніх регістрів, включаючи покажчик команди і покажчик вершини стека. Кожна з таких “завдань” називається потоком виконання (thread of execution).


У простій многозадачной системі, кожна програма має емеет єдиний потік. Це означає, що ЦЕНТРАЛЬНИЙ ПРОЦЕСОР починає виконання команд на початку програми і продовжує дотримуючись інструкцій в послідовності, визначеною програмою до тих пір, поки програма не завершується.


Скажімо, програма має п’ять команд: BCD і E, які виконуються послідовно (ніяких переходів немає в цьому прикладі). Коли програма має один потік, команди будуть завжди виконувати в точно тому ж самому порядку: A, B, C, D і E. Дійсно, ЦЕНТРАЛЬНИЙ ПРОЦЕСОР може зажадати часу для виконання інших команд в інших програмах, але вони не будуть впливати на цю програму, якщо немає конфлікт над загальними ресурсами системи, але це вже окрема тема для розмови.


Просунута багатопотокова операційна система типу Windows дозволяє додатком виконувати більше ніж один потік одночасно. Скажімо, команда D в нашому типовому додатку могла створити новий потік, який стартував командою B і далі виконував послідовність команд C і E. Перший потік був би все ще A, B, C, D, E, але коли команда D виконається, виникне новий потік, який виконає команди б B, C, E (тут команди D вже не буде, інакше ми отримаємо ще один потік).


У якому порядку будуть слідувати команди в цьому додатку?


Це могло б бути:
























Thread 1  A  B  C  D    E     
Thread 2          B    C  E 

Або так:
























Thread 1  A  B  C  D      E   
Thread 2          B  C    E 

Або отак:
























Thread 1  A  B  C  D        E 
Thread 2          B  C  E   

Іншими словами, коли Ви починаєте новий потік виконання в програмі, Ви ніколи не можете знати точний порядок, в якому команди в двох потоках виконуватися щодо один одного. Два потоки повністю незалежні.


Чому – це проблема?


Імітатор багатопоточності


Розглянемо проект MTDemo:


Проект містить один модуль коду, в якому міститься дві глобальних змінних:

” MTDemo – Multithreading Demo program
” Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Public GenericGlobalCounter As Long
Public TotalIncrements As Long
“Цей проект містить одну форму – frmMTDemo1, яка містить
“Наступний код:
” MTDemo – Multithreading Demo program
” Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Dim State As Integer
” State = 0 – Idle
” State = 1 – Loading existing value
” State = 2 – Adding 1 to existing value
” State = 3 – Storing existing value
” State = 4 – Extra delay
Dim Accumulator As Long
Const OtherCodeDelay = 10
Private Sub Command1_Click()
Dim f As New frmMTDemo1
f.Show
End Sub
Private Sub Form_Load()
Timer1.Interval = 750 + Rnd * 500
End Sub
Private Sub Timer1_Timer()
Static otherdelay&
Select Case State
Case 0
lblOperation = “Idle”
State = 1
Case 1
lblOperation = “Loading Acc”
Accumulator = GenericGlobalCounter
State = 2
Case 2
lblOperation = “Incrementing”
Accumulator = Accumulator + 1
State = 3
Case 3
lblOperation = “Storing”
GenericGlobalCounter = Accumulator
TotalIncrements = TotalIncrements + 1
State = 4
Case 4
lblOperation = “Generic Code”
If otherdelay >= OtherCodeDelay Then
State = 0
otherdelay = 0
Else
otherdelay = otherdelay + 1
End If
End Select
UpdateDisplay
End Sub
Public Sub UpdateDisplay()
lblGlobalCounter = Str$(GenericGlobalCounter)
lblAccumulator = Str$(Accumulator)
lblVerification = Str$(TotalIncrements)
End Sub

Ця програма для моделювання багатопотокового режиму використовує таймер і простий кінцевий автомат. Мінлива State описує п’ять команд, які ця програма виконує. State = 0 – неактивний стан. State = 1 завантажує локальну змінну глобальної змінної GenericGlobalCounter. State = 2 збільшує на одиницю локальну змінну. State = 3 запам’ятовує результат у змінній GenericGlobalCounter і збільшує змінну TotalIncrements (яка рахує кількість збільшень змінної GenericGlobalCounter). State = 3 додає додаткову затримку, що представляє собою час, витрачений на виконання інших команд у програмі.


Функція UpdateDisplay оновлює три мітки на формі, які показують поточне значення змінної GenericGlobalCounter, локального суматора, і загальної кількості збільшень.


Кожен сигнал таймера моделює цикл центральний процесор в поточному потоці. Якщо Ви запустите програму, то побачите, що значення змінної GenericGlobalCounter буде завжди точно так само змінної TotalIncrements, тому що змінна TotalIncrements показує кількість збільшень лічильника GenericGlobalCounter потоком.


Але що трапиться, коли Ви натискаєте кнопку Command1 і запустіть другий примірник форми? Ця нова форма змоделює другий потік.


Час від часу, команди вишикуються в лінію таким чином, що обидві форми завантажать однакове значення GenericGlobalCounter, збільшать і збережуть його. В результаті, значення лічильника збільшиться тільки на одиницю, навіть при тому, що кожен потік думав, що він незалежно збільшує значення лічильника. Іншими словами, мінлива була збільшена двічі, але значення збільшилося тільки на одиницю. Якщо Ви запускаєте кілька форм, то відразу помітите, що число збільшень, що подається змінної TotalIncrements, росте набагато швидше, ніж лічильник GenericGlobalCounter.


Що, якщо змінна представляє Об’єктовий рахунок блокування – який стежить, коли об’єкт має бути звільнений? Що, якщо вона являє собою сигнал, який вказує, що ресурс знаходиться у використанні?


Така проблема може привести до появи ресурсів, постійно недоступних в системі, до об’єкта, блокується в пам’яті, або передчасно звільненому. Це може привести до збоїв програми.


Цей приклад був розроблений, щоб досить просто побачити проблему, але спробуйте поекспериментувати зі значенням змінної OtherCodeDelay. Коли небезпечний код відносно невеликий в порівнянні з всій програмою, проблеми з’являться менш часто. Хоча це й звучить обнадійливо, але істина полягає в наступному. Проблеми багатопотокового режиму можуть бути надзвичайно нестійкі і їх важко виявити. Це означає, що багато-режим вимагає обережного підходу до проектування програми.


Рішення проблем багатопоточності


Є два відносно простих способи уникнути проблем багатопотокового режиму.


Уникайте загального використання глобальних змінних.


Додайте код синхронізації скрізь, де використовуються глобальні змінні.


Перший підхід використовується в основному в Visual Basic. Коли Ви включаєте багатопотоковий режим в Visual Basic додатки, всі глобальні змінні стануть локальними для специфічного потоку. Це властиво способу, з яким Visual Basic виконує apartment model threading – докладніше про це пізніше.


Початковий випуск Visual Basic 5.0 дозволяв використовувати багатопоточність тільки в компонентах, які не мали ніяких елементів призначеного для користувача інтерфейсу. Так було тому що вони не мали безпечного потоку управління формами. Наприклад: коли Ви створюєте форму в Visual Basic, VB дає їй ім’я глобальної змінної (таким чином, якщо Ви маєте форму, іменовану Form1, Ви можете безпосередньо звертатися до її методам, використовуючи Form1.метод замість того, щоб оголосити окрему змінну форми). Цей тип глобальної змінної може викликати проблеми багатопотокового режиму, які Ви бачили раніше. Були безсумнівно інші проблеми всередині управління формами.


З service pack 2, управління формами Visual Basic було зроблено безпечним потоком. Це говорить про те, що кожен потік має власну глобальну змінну для кожної форми, визначеної у проекті.


Що нового в Service Pack 2


Зробивши потік управління формами безпечним, Service pack 2 надав можливість з допомогою Visual Basic створювати клієнтські додатки, що використовують багато-режим.


Додаток має бути визначено як програма ActiveX Exe з установкою запуску з Sub Main:

” MTDemo2 – Multithreading demo program
” Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Declare Function FindWindow Lib “user32” Alias “FindWindowA” _
(ByVal lpClassName As String, ByVal lpWindowName As String) _
As Long
Sub Main()
Dim f As frmMTDemo2
” We need this because Main is called on each new thread
Dim hwnd As Long
hwnd = FindWindow(vbNullString, “Multithreading Demo2”)
If hwnd = 0 Then
Set f = New frmMTDemo2
f.Show
Set f = Nothing
End If
End Sub

Перший раз програма завантажує і відображає основну форму додатку. Підпрограма Main має з’ясувати, чи є це першим потоком програми, тому цей код виконується при старті кожного потоку. Ви не можете використовувати глобальну змінну, щоб це з’ясувати, тому що Visual Basic apartment model зберігає глобальні змінні специфічними для одиночного потоку. У цьому прикладі використовується функція API FindWindow, щоб перевірити, чи була завантажена основна форма прикладу. Є інші способи з’ясувати, чи є це основним потоком, включаючи використання об’єктів синхронізації системи – Але це окрема тема для розмови.


Багатопотоковий режим реалізується створенням об’єкта в новому потоці. Об’єкт повинен бути визначений, використовуючи модуль класу. В цьому випадку, простий модуль класу визначається наступним чином:

” MTDemo2 – Multithreading demo program
” Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Private Sub Class_Initialize()
Dim f As New frmMTDemo2
f.Show
Set f = Nothing
End Sub

Ми можемо встановити змінну форми як nothing після того, як вона створена, тому що після відображення форми вона буде збережена.

” MTDemo2 – Multithreading demo program
” Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Private Sub cmdLaunch1_Click()
Dim c As New clsMTDemo2
c.DisplayObjPtr Nothing
End Sub
Private Sub cmdLaunch2_Click()
Dim c As clsMTDemo2
Set c = CreateObject(“MTDemo2.clsMTDemo2”)
End Sub
Private Sub Form_Load()
lblThread.Caption = Str$(App.ThreadID)
End Sub

Форма відображає ідентифікатор потоку в мітці на формі. Форма містить дві командні кнопки, одна з яких використовує оператор New, інша-використовує оператор CreateObject.


Якщо Ви запустіть програму всередині середовища Visual Basic, то побачите, що форми завжди створюються в одному і тому ж потоці. Це відбувається, тому що середа Visual Basic підтримує тільки одиночний потік. Якщо Ви скомпілюєте і запустіть програму, то побачите, що підхід, який використовує CreateObject створює і clsMTDemo2 та її форму в новому потоці.


Чому багатопоточність


Звідки вся метушня щодо багатопотокового режиму, якщо він включає так багато потенційної небезпеки? Тому що, в деяких ситуаціях, багато-режим може значно покращувати ефективність програми. В деяких випадках це може покращувати ефективність деяких операцій синхронізації типу очікування завершення програми. Це дозволяє зробити архітектуру програми більш гнучкою. Наприклад, операція Add a long у формі MTDEMO2 з наступним кодом:

Private Sub cmdLongOp_Click()
Dim l&
Dim s$
For l = 1 To 1000000
    s = Chr$(l And &H7F)
Next l
End Sub

Запустіть кілька екземплярів форми, використовуючи кнопку cmdLaunch1. Коли Ви натискаєте на кнопку cmdLongOp на будь-який з форм, то побачите, що ця дія заморожує операції на всіх інших формах. Так відбувається, тому що всі форми виконуються в одиночному потоці – і цей потік зайнятий виконанням довгого циклу. Якщо Ви запустіть кілька екземплярів форми кнопкою cmdLaunch2 і нажімете кнопку cmdLongOp на форму, то тільки ця форма буде заморожена – інші форми будуть активними. Вони виконуються у власних потоках, і довгий цикл буде виконуватися тільки у власному потоці. Звичайно, в будь-якому випадку, Ви ймовірно не повинні розміщувати тривалі операції такого типу в ваших формах.


Далі коротке резюме, коли важливий багато-режим:


Сервер ActiveX EXE – без загальних ресурсів.



Коли Ви маєте ActiveX EXE сервер, який Ви збираєтеся спільно використовувати серед декількох додатків, багато-режим запобігає програми від небажаних взаємодій з один одним. Якщо один додаток виконує довгу операцію на об’єкті в однопоточному сервері, інші додатки будуть витіснені, тобто будуть чекати, коли звільниться сервер. Багатопотоковий режим рещается цю проблему. Однак, є випадки, де Ви можете хотіти використовувати ActiveX EXE сервер, щоб регулювати доступ до загальнодоступних ресурсів (shared resource). Наприклад, сервер stock quote, описаний в моїй книзі Developing ActiveX Components. У цьому випадку сервер stock quote виконується в одиночному потоці і що доступний для всіх додатків, що використовують сервер по черзі.


Багатопотоковий клієнт – виконуваний як ActiveX EXE сервер



Проста форма цього підходу продемонстрована в додатку MTDEMO2. Цей підхід використовується, коли програма підтримує множинні вікна, які повинні виходити з однієї програми, але працювати повністю незалежно. Інтернет-браузер – хороший приклад такого багатопотокового клієнта, де кожне вікно виконується у власному потоці. Тут слід звернути увагу на те, що багато-режим не повинен використовуватися як заміна для гарного подієво керованого проекту.


Багатопотокові сервери DLL або EXE



В архітектурі клієнт-сервер, багато-режим може збільшити ефективність, якщо Ви маєте суміш довгих і коротких клієнтських запитів. Будьте уважним, хоча – якщо всі ваші клієнтські запити мають подібну довжину, багато-режим може фактично сповільнювати середній час відповіді сервера! Ніколи не приймайте на віру той факт, що якщо ваш сервер є багатопотоковий, то обов’язково його ефективність збільшиться.


Угода про потоках


Вірите чи ні, але все це було введенням. Частина цього матеріалу є оглядом матеріалу, який описаний в моїй книзі Developing ActiveX Components, інша частина матеріалу описує нову інформацію для service pack 2.


Тепер, дозволите задавати питання, яке має відношення до багатопотоковому режимі, що використовує COM (модель багатокомпонентних об’єктів, на якій базуються не тільки всі Visual Basic об’єкти, але й інші windows програми, що використовують технології OLE).


Дано:

Багатопотоковий режим є потенційно небезпечним взагалі, і особливо спроби багатопотокового кодування додатків, які не розроблені для підтримки багатопотокового режиму, швидше за все призведуть до фатальних помилок і збоїв системи.


Питання:

Як це можливо, що Visual Basic дозволяє Вам створювати об’єкти і використовувати їх з поодинокими і багато-середовищами безвідносно до того, розроблені вони для одиночного або багатопотокового використання?


Іншими словами – Як багатопотокові Visual Basic додатки можуть використовувати об’єкти, які не розроблені для безпечного виконання в многопоточной середовищі? Як можуть інші багатопотокові програми використовувати однопоточні об’єкти Visual Basic?


Коротко: як COM підтримує потоки?

Якщо Ви знаєте COM, то Ви знаєте, що COM визначає структуру угоди. Об’єкт COM погоджується слідувати деяким правилам так, щоб цим можна було успішно користуватися з будь-якої програми або об’єкта, який підтримує COM.


Більшість людей спочатку думає про інтерфейсній частини угоди – про методи і властивості, які надає об’єкт.


Але Ви не можете не знати того, що COM також визначає потоковість як частина угоди. І подібно будь-якій частині угоди COM – якщо Ви порушуєте ці умови, то будете мати проблеми. Visual Basic, природно, приховує від Вас більшість механізмів COM, але щоб зрозуміти як використовувати багатопоточність в Visual Basic, Ви повинні розібратися COM моделі потоків.


Модель одиночного потоку:



Однопотоковий сервер – найпростіший тип реалізації сервера. І найпростіший для розуміння. В цьому випадку EXE сервер виконується в одиночному потоці. Всі об’єкти створюються в цьому потоці. Всі виклики методів кожного об’єкта, підтримуваного сервером повинні прибути в цей потік.



Але що буде, якщо клієнт виконується в іншому потоці? У тому випадку, для об’єкта сервера повинен бути створений проміжний об’єкт (proxy object). Цей проміжний об’єкт виконується в потоці клієнта і відображає методи і властивості фактичного об’єкта. Коли викликається метод проміжного об’єкта, він виконує операції, необхідні для підключення до потоку об’єкта, а потім викликає метод фактичного об’єкта, використовуючи параметри, передані до проміжного об’єкту. Природно, що цей підхід вимагає значного часу на виконання завдання, проте він дозволяє виконати всі угоди. Цей процес перемикання потоків і пересилання даних від проміжного об’єкта до фактичного об’єкту і назад називається marshalling. Ця тема обговорюється в главі 6 моєї книги Developing ActiveX Components.



У разі DLL серверів, одиночна потокова модель вимагає, щоб всі об’єкти в сервері створювалися і викликалися в тому ж самому потоці що і перший об’єкт, створений сервером.


Модель Apartment Threading



Зверніть увагу, що модель Apartment Threading як визначено COM не вимагає, щоб кожен потік мав власний набір глобальних змінних. Visual Basic таким чином реалізує модель Apartment Threading. Модель Apartment Threading декларує, що кожен об’єкт може бути створений у власному потоці, проте, як тільки об’єкт створений, його методи і властивості можуть викликатися тільки тим же самим потоком, яка створив об’єкт. Якщо об’єкт іншого потоку захоче мати доступ до методів цього об’єкту, то він повинен діяти через проміжний об’єкт.



Така модель відносно проста для реалізації. Якщо Ви усуваєте глобальні змінні (як робить Visual Basic), модель Apartment Threading автоматично гарантує безпеку потоку – так як кожен об’єкт дійсно виконується у власному потоці, і завдяки відсутності глобальних змінних, об’єкти в різних потоках не взаємодіють один з одним.


Модель вільних потоків



Модель вільних потоків (Free Threading Model) полягає в наступному .. Будь-який об’єкт може бути створений у будь-якому потоці. Всі методи і властивості будь-якого об’єкта можуть бути викликає в будь-який час з будь-якого потоку. Об’єкт приймає на себе всю відповідальність за обробку будь-якої необхідної синхронізації.



Це найважча у реалізації модель, так як потрібно, щоб всю синхронізацію обробляв програміст. Фактично до недавнього часу, технологія OLE безпосередньо не підтримувала цю модель! Однак, з тих пір marshalling ніколи не потрібно і це найбільш ефективна модель потоків.


Яку модель підтримує ваш сервер?


Як додаток або сама Windows дізнається, яку модель потоків використовує сервер? Ця інформація включена до реєстру (registry). Коли Visual Basic створює об’єкт, він перевіряє системний реєстр, щоб визначити, в яких випадках потрібно використовувати проміжний об’єкт (proxy object) і в яких – marshalling.


Ця перевірка є обов’язком клієнта і необхідна для суворої підтримки вимог багатопоточності для кожного об’єкта, якого він створює.


Функція API CreateThread


Тепер давайте подивимося, як з Visual Basic може використовуватися функція API CreateThread. Скажімо, Ви маєте клас, що Ви хочете виполненять в іншому потоці, наприклад, щоб виконати деяку фонову операцію. Характерний клас такого типу міг би мати наступний код (з прикладу MTDemo 3):

” Class clsBackground
” MTDemo 3 – Multithreading example
” Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Event DoneCounting()
Dim l As Long
Public Function DoTheCount(ByVal finalval&) As Boolean
Dim s As String
If l = 0 Then
s$ = “In Thread ” & App.threadid
Call MessageBox(0, s$, “”, 0)
End If
l = l + 1
If l >= finalval Then
l = 0
DoTheCount = True
Call MessageBox(0, “Done with counting”, “”, 0)
RaiseEvent DoneCounting
End If
End Function

Клас розроблений так, щоб функція DoTheCount могла неодноразово викликатися з безперервного циклу у фоновому потоці. Ми могли б помістити цикл безпосередньо в сам об’єкт, але ви незабаром побачите, що були вагомі причини для проектування об’єкта як показано в прикладі. При першому виклику функції DoTheCount з’являється MessageBox, в якому показано ідентифікатор потоку, за яким ми можемо визначити потік, в якому виконується код. Замість VB команди MessageBox використовується MessageBox API, тому що функція API, як відомо, підтримує безпечне виконання потоків. Другий MessageBox з’являється після того, як закінчений підрахунок і Згенеровано подія, яка вказує, що операція закінчена.


Фоновий потік запускається за допомогою наступного коду у формі frmMTDemo3:

Private Sub cmdCreateFree_Click()
Set c = New clsBackground
StartBackgroundThreadFree c
End Sub

Функція StartBackgroundThreadFree визначена в модулі modMTBack наступним чином:

Declare Function CreateThread Lib “kernel32” _
(ByVal lpSecurityAttributes As Long, ByVal _
dwStackSize As Long, ByVal lpStartAddress As Long, _
ByVal lpParameter As Long, ByVal dwCreationFlags _
As Long, lpThreadId As Long) As Long
Declare Function CloseHandle Lib “kernel32” _
(ByVal hObject As Long) As Long
” Start the background thread for this object
” using the invalid free threading approach.
Public Function StartBackgroundThreadFree _
(ByVal qobj As clsBackground)
Dim threadid As Long
Dim hnd&
Dim threadparam As Long
” Free threaded approach
threadparam = ObjPtr(qobj)
hnd = CreateThread(0, 2000, AddressOf _
BackgroundFuncFree, threadparam, 0, threadid)
If hnd = 0 Then
” Return with zero (error)
Exit Function
End If
	” We don”t need the thread handle
CloseHandle hnd
StartBackgroundThreadFree = threadid
End Function

Функція CreateThread має шість параметрів:


Функція повертає дескриптор потоку.


В цьому випадку ми передаємо покажчик на об’єкт clsBackground, який ми будемо використовувати в новому потоці. ObjPtr відновлює значення покажчика інтерфейсу в змінну qobj. Після створення потоку закривається дескриптор за допомогою функції CloseHandle. Ця дія не завершує потік, – потік продовжує виконуватися до виходу з функції BackgroundFuncFree. Однак, якщо ми не закрили дескриптор, то об’єкт потоку буде існувати навіть після виходу з функції BackgroundFuncFree. Всі дескриптори потоку повинні бути закриті і при завершенні потоку система звільняє зайняті потоком ресурси.


Функція BackgroundFuncFree має наступний код:


” A free threaded callback.

” A free threaded callback.
” This is an invalid approach, though it works
” in this case.
Public Function BackgroundFuncFree(ByVal param As _
IUnknown) As Long
Dim qobj As clsBackground
Dim res&
” Free threaded approach
Set qobj = param
Do While Not qobj.DoTheCount(100000)
Loop
	” qobj.ShowAForm ” Crashes!
” Thread ends on return
End Function

Параметром цієї функції є-вказівник на інтерфейс (ByVal param As IUnknown). При цьому ми можемо уникнути неприємностей, тому що під COM кожен інтерфейс грунтується на IUnknown, так що такий тип параметра допустимо незалежно від типу інтерфейсу, переданого функції. Ми, однак, повинні негайно визначити param як тип об’єкта, щоб потім його використовувати. В цьому випадку qobj установливают як об’єкт clsBackground, який був переданий до об’єкта StartBackgroundThreadFree.


Функція потім виконує нескінченний цикл, протягом якого може виконуватися будь-яка необхідна операція, в цьому випадку повторний рахунок. Подібний підхід міг би використовуватися тут, щоб виконати операцію очікування, яка призупиняє потік поки не проізойдкт системне подія (типу завершення процесу). Потік потім міг би викликати метод класу, щоб повідомити додатку, що подія відбулася.


Доступ до об’єкту qobj надзвичайно швидкий через використання підходу вільного потоку (free threading) – ніяка переадресація (marshalling) при цьому не використовується.


Зверніть увагу на те, що якщо Ви спробуєте використовувати об’єкт clsBackground, який показує форму, то це призведе до збоїв програми. Зверніть також увагу на те, що подія завершення ніколи не відбувається в клієнтської формі. Дійсно, навіть Microsoft Systems Journal, який описує цей підхід, містить дуже багато попереджень про те, що при використанні цього підходу є деякі речі, які не працюють.


Деякі розробники, хто пробували розгортати додатки, які застосовують цей тип багатопоточності, виявили, що їх застосування викликають збої після оновлення до VB5 service pack 2.


Чи є це дефектом Visual Basic?


Чи означає це, що Microsoft не забезпечила сумісність?


Відповідь на обидва питання: Немає


Проблема не в Microsoft або Visual Basic.


Проблема полягає в тому, що вищезгаданий код є сміттям.


Проблема проста – Visual Basic підтримує об’єкти і в моделі одиночного потоку і в apartment model. Дозвольте мені перефразувати це: об’єкти Visual Basic є COM об’єктами і вони, згідно COM угодою, будуть правильно працювати як в моделі одиночного потоку так і в apartment model. Це означає, що кожен об’єкт очікує, що будь-які виклики методів будуть відбуватися в тому ж самому потоці, який створив об’єкт.


Приклад, показаний вище, порушує це правило.


Це порушує угоду COM.


Що це означає?


Ви бачите, що як тільки Ви порушуєте угоду COM, Ви більше не захищені тими можливостями COM, які дозволяють об’єктам успішно взаємодіяти один з одним і з клієнтами.


Цей підхід є програмної алхімією. Це безвідповідально і жоден програміст не повинен коли-небудь використовувати це. Точка.


Назад до функції API CreateThread


Тепер, коли я показав Вам, чому підхід до використання CreateThread API, показаний в деяких статтях, є сміттям, я покажу Вам, як можна використати цю API функцію безпечно. Прийом простий – Ви повинні просто твердо держатись угоди COM про потоках. Це займе трохи більше часу і зусиль, але практика показала, що виходять дуже надійні результати.


Приклад MTDEMO3 демонструє цей підхід у формі frmMTDemo3, що має код, який запускає клас фону в apartment model наступним чином:

Private Sub cmdCreateApt_Click()
Set c = New clsBackground
StartBackgroundThreadApt c
End Sub

Поки це виглядає дуже схоже на підхід вільних потоків. Ви створюєте екземпляр класу і передаєте його функції, яка запускає фоновий потік. У модулі modMTBack з’являється наступний код:

” Structure to hold IDispatch GUID
Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Public IID_IDispatch As GUID
Declare Function CoMarshalInterThreadInterfaceInStream Lib _
“ole32.dll” (riid As GUID, ByVal pUnk As IUnknown, _
ppStm As Long) As Long
Declare Function CoGetInterfaceAndReleaseStream Lib _
“ole32.dll” (ByVal pStm As Long, riid As GUID, _
pUnk As IUnknown) As Long
Declare Function CoInitialize Lib “ole32.dll” (ByVal _
pvReserved As Long) As Long
Declare Sub CoUninitialize Lib “ole32.dll” ()
” Start the background thread for this object
” using the apartment model
” Returns zero on error
Public Function StartBackgroundThreadApt(ByVal qobj _
As clsBackground)
Dim threadid As Long
Dim hnd&, res&
Dim threadparam As Long
Dim tobj As Object
Set tobj = qobj
” Proper marshaled approach
	InitializeIID
	res = CoMarshalInterThreadInterfaceInStream _
(IID_IDispatch, qobj, threadparam)
If res <> 0 Then
StartBackgroundThreadApt = 0
Exit Function
End If
hnd = CreateThread(0, 2000, AddressOf _
BackgroundFuncApt, threadparam, 0, threadid)
If hnd = 0 Then
” Return with zero (error)
Exit Function
End If
” We don”t need the thread handle
CloseHandle hnd
StartBackgroundThreadApt = threadid
End Function

Функція StartBackgroundThreadApt трохи складніша ніж її еквівалент при застосуванні підходу вільних потоків. Перша нова функція називається InitializeIID. Вона має такий код:

” Initialize the GUID structure
Private Sub InitializeIID()
Static Initialized As Boolean
If Initialized Then Exit Sub
With IID_IDispatch
.Data1 = &H20400
.Data2 = 0
.Data3 = 0
.Data4(0) = &HC0
.Data4(7) = &H46
End With
Initialized = True
End Sub

Ви бачите, нам необхідний ідентифікатор інтерфейсу – 16 байтовая структура, яка унікально визначає інтерфейс. Зокрема нам необхідний ідентифікатор інтерфейсу для інтерфейсу IDispatch (детальна інформація щодо IDispatch може бути знайдена в моїй книзі Developing ActiveX Components). Функція InitializeIID просто ініціалізує структуру IID_IDISPATCH до коректним значенням для ідентифікатора інтерфейсу IDispatch. Значення Це значення виходить за допомогою використання утиліти перегляду системного реєстру.


Чому нам потрібен цей ідентифікатор?


Тому що, щоб твердо дотримуватися угоди COM про потоки, ми повинні створити проміжний об’єкт (proxy object) для об’єкта clsBackground. Проміжний об’єкт повинен бути переданий новому потоку замість початкового об’єкта. Звернення до нового потоку на проміжному об’єкті будуть переадресовані (marshaled) в поточний потік.


CoMarshalInterThreadInterfaceInStream виконує цікаву задачу. Вона збирає всю інформацію, необхідну при створенні проміжного об’єкта, для певного інтерфейсу і завантажує її на об’єкт потоку (Stream object). У цьому прикладі ми використовуємо інтерфейс IDispatch, тому що ми знаємо, що кожен клас Visual Basic підтримує IDispatch і ми знаємо, що підтримка переадресації (marshalling) IDispatch вбудована в Windows – так що цей код буде працювати завжди. Потім ми передаємо об’єкт потоку (stream object) новому потоку. Цей об’єкт розроблений Windows, щоб бути переданим між потоками однаковим способом, так що ми можемо безпечно передавати його функції CreateThread. Інша частина функції StartBackgroundThreadApt ідентична функції StartBackgroundThreadFree.


Функція BackgroundFuncApt також складніше ніж її еквівалент при використанні моделі вільних потоків і показано нижче:

” A correctly marshaled apartment model callback.
” This is the correct approach, though slower.
Public Function BackgroundFuncApt(ByVal param As Long) As Long
Dim qobj As Object
Dim qobj2 As clsBackground
Dim res&
” This new thread is a new apartment, we must
” initialize OLE for this apartment
” (VB doesn”t seem to do it)
res = CoInitialize(0)
	” Proper apartment modeled approach
res = CoGetInterfaceAndReleaseStream(param, _
IID_IDispatch, qobj)
Set qobj2 = qobj
Do While Not qobj2.DoTheCount(10000)
Loop
qobj2.ShowAForm
” Alternatively, you can put a wait function here,
” then call the qobj function when the wait is satisfied
” All calls to CoInitialize must be balanced
CoUninitialize
End Function

Перший крок повинен ініціалізувати підсистему OLE для нового потоку. Це необхідно для переадресації (marshalling) коду, щоб працювати коректно. CoGetInterfaceAndReleaseStream створює проміжний об’єкт для об’єкта clsBackground і реалізує об’єкт потоку (stream object), який використовується для передачі даних з іншого потоку. Інтерфейс IDispatch для нового об’єкта завантажується в змінну qobj. Тепер можливо отримати інші інтерфейси – проміжний об’єкт буде коректно переадресовувати дані для кожного інтерфейсу, який може підтримувати.


Тепер Ви можете бачити, чому цикл поміщений у цю функцію замість того, щоб знаходитися безпосередньо в об’єкті. Коли Ви вперше викличте функцію qobj2.DoTheCount, то побачите, що код виконується в початковому потоці! Кожен раз, коли Ви викликаєте метод об’єкта, Ви фактично викликаєте метод проміжного об’єкта. Ваш поточний потік припиняється, запит методу переадресовується початкового потоку і викликається метод первісного об’єкта в тій же самому потоці, який створив об’єкт. Якби цикл був в об’єкті, то Ви б заморозили початковий потік.


Хорошим результатом застосування цього підходу є те, що все працює правильно. Об’єкт clsBackground може безпечно показувати форми і генерувати події. Недоліком цього підходу є, звичайно, його більш повільне виконання. Перемикання потоків і переадресація (marshalling) – відносно повільні операції. Ви фактично ніколи не захочете виконувати фонову операцію як показано тут.


Але цей підхід може надзвичайно добре працювати, якщо Ви можете поміщати фонову операцію безпосередньо в функцію BackgroundFuncApt! Наприклад: Ви могли б мати фоновий потік, що виконує фонові обчислення або операцію очікування системи. Коли вони будуть завершені, ви можете викликати метод об’єкта, який згенерує подія в клієнті. Зберігаючи кількість викликів методу, невелика щодо кількості роботи, виконуваної в фонової функції, Ви можете досягати дуже ефективних результатів.


Що, якщо Ви хочете виконати фонову операцію, яка не повинна використовувати об’єкт? Очевидно, проблеми з угодою COM про потоках зникають. Але з’являються інші проблеми. Як фоновий потік повідомить про своєму завершенні пріоритетного потоку? Як вони обмінюються даними? Як два потоки будуть синхронізовані? Все це можливо виконати за допомогою відповідних викликів API. У моїй книзі Visual Basic 5.0 Programmer “s Guide to the Win32 API є інформації щодо об’єктів синхронізації типу Подій, Mutexes, семафори і Waitable таймерів.


Ця книга також включає приклади файлів відображаються в пам’ять, які можуть бути корисні при обміні даних між процесами. Ви зможете використовувати глобальні змінні, щоб обмінюватися дані, але треба знати, що таке поведінка не гарантується Visual Basic (іншими словами, навіть якщо це зараз працює, немає ніяких гарантій, що це буде працювати в майбутньому). В цьому випадку я міг би запропонувати Вам використовувати для обміну даними методики, засновані на API. Проте, перевагою показаного тут підходу, заснованого на об’єктах, є те, що цей підхід робить проблему обміну даними між потоками тривіальної, просто робіть це через об’єкт.


Висновок


Я одного разу почув від досвідченого програміста під Windows, що OLE є найважчою технологією, якою він коли-небудь навчався. Я з цим згоден. Це дуже велика тема, і деякі частини цієї технології дуже важко зрозуміти. Visual Basic, як завжди, приховує від Вас багато складнощів.


Є сильна спокуса, щоб користуватися перевагою просунутих методів типу багатопотокового режиму, використовуючи підхід “tips and techniques”. Це спокуса заохочено деякими статтями, які інколи представляють специфічне рішення, запрошуючи Вас вирізати і вставити (cut and past) їх методики у ваші власні додатки.


Коли я писав книгу Visual Basic Programmer “s Guide to the Windows API, я виступав проти такого підходу до програмування. Я відчував, що взагалі безвідповідально включати в додаток код, який Ви не розумієте, і що реальне знання, яке так важко отримати, коштує витрачених зусиль.


Таким чином мої книги по API були розроблені, щоб забезпечити не швидкі відповіді та прості рішення, а щоб навчити використанню API до такої міри, що програмісти могли б інтелектуально правильно застосовувати навіть найбільш просунуті методи. Я застосував це той же самий підхід до моїй книзі Developing ActiveX Components, яка вимагає багато часу для обговорення принципів ActiveX, COM і об’єктно-орієнтованого програмування перед описом подробиць реалізації цієї технології.


Багато чого з моєї кар’єри на ниві Visual Basic і багато з діяльності у фірмі Desaware, засноване на навчанні Visual Basic програмістів просунутим методам. Читач, хто надихнув мене на написання цієї статті, критикуючи мене за стримування технології багатопоточності, пропустив точку.


Так, я навчаю і демонструю просунуті методи програмування – але я намагаюся ніколи не пропустити велику картинку. Просунуті методи, яким я навчаю, повинні бути несуперечливі з правилами і специфікаціями Windows. Вони повинні бути такими безпечними, наскільки це можливо. Вони повинні бути підтримуваними в кінцевому рахунку. Вони не повинні руйнуватися, коли змінюються Windows або Visual Basic.


Я можу вимагати тільки часткового успіху, так як Microsoft може змінити правила гри всякий раз, коли їм здасться, що це необхідно. Але я завжди пам’ятаю про це і пробую попереджати людей, коли я думаю, що можу проштовхнути обмеження.


Я сподіваюся, що наведене тут обговорення багатопотокового режиму показує небезпеки застосування “простих методів” без гарного розуміння основної технології.


Я не можу обіцяти, що використання apartment model версії CreateThread є абсолютно коректним, але моє розуміння проблеми і досвід показують, що це безпечно.


Можуть матись інші фактори, які я пропустив. OLE – дійсно складна річ і модулі OLE DLL і сам Visual Basic схильні до змін. Я тільки можу стверджувати, що найкраще з мого знання – код, який я тут показав, задовольняє правилам COM і що емпіричне доказ показує, що Visual Basic runtime 5 0 “s є достатньо безпечним для виконання фонового коду потоку в стандартному модулі.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*