Потоки в Visual Basic h2>
З
появою оператора AddressOf, частина індустрії ПО стала орієнтуватися на
авторів, що показують як з
використанням Visual Basic вирішувати раніше неможливі завдання. Інша частина швидко
охопила консультантів, які допомагають користувачам, що мають проблеми при вирішенні
таких завдань. p>
Проблема
не в Visual Basic або в технології. Проблема в тому, що більшість авторів
застосовують одне й те ж правило до
AddressOf методиками, що більшість компаній з розробки ПЗ вважають, що
якщо Ви повинні щось зробити, то Ви
зможете. Ідея про те, що застосування самої нової і останньої технології повинно, за визначенням, бути найкращим
вирішенням проблеми, широко поширена в індустрії ПЗ. p>
Ця
ідея невірна. Розгортання технології повинно керуватися перш за все
проблемою, яку необхідно вирішити
вирішити, а не технологією, яку хтось пробує Вам впарити;). p>
Дуже
погано, що з-за того, що компанії часто нехтують згадуванням про
обмеження та недоліки їх
інструментальних засобів, автори інколи бувають не в змозі звернути увагу
читачів на наслідки деяких
методик, які вони описують. І журнали і книги іноді нехтують своєю відповідальністю, щоб упевнитися, що
практика програмування, яку вони описують, є прийнятною. p>
Програмістові
дуже важливо вибрати необхідний інструмент для своєї роботи. Це - ваше завдання,
щоб розробити код, який працює
тепер не тільки на одній специфічній платформі, але також працює на різних платформах і системних конфігураціях.
Ваш код повинен бути добре документована і підтриманий іншими програмістами, які беруть участь у
проекті. Ваш код повинен слідувати правилам, продиктованими операційною системою або стандартами, які
Ви використовуєте. Відмова так робити може призвести до проблем у майбутньому, оскільки системи і
програмне забезпечення постійно удосконалюються. p>
Недавні
статті в 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. P>
Швидкий огляд багатопоточності h2>
Якщо
Ви вже добре розбираєтеся в технології багатопотокового режиму, то Ви можете
пропустити цей розділ і продовжувати
читання з розділу, названого "Що нового в Service Pack 2." p>
Кожен,
хто використовує Windows, знає, що Windows здатне робити більше ніж одну річ
одночасно. p>
Може
одночасно виконувати декілька програм, при одночасному програванні
компакт-диска, посилці факсу і пересилання
файлів. Кожен програміст знає (або повинен знати) що ЦЕНТРАЛЬНИЙ ПРОЦЕСОР комп'ютера може тільки виконувати
одну команду одночасно (проігноруємо існування багатопроцесорних машин). Як
єдиний ЦЕНТРАЛЬНИЙ ПРОЦЕСОР може виконувати безліч завдань? p>
Це
робиться швидким перемиканням між багатьма завданнями. Операційна система
містить в пам'яті всі програми, які
запущені в даний момент. Це дозволяє Центральний процесор виконувати програми по черзі. Кожного разу
відбувається перемикання між програмами, при цьому змінюється вміст внутрішніх регістрів,
включаючи покажчик команди і покажчик вершини стека. Кожна з таких "завдань" називається потоком
виконання (thread of execution). p>
В
простий багатозадачного системі, кожна програма має емеет єдиний потік.
Це означає, що ЦЕНТРАЛЬНИЙ ПРОЦЕСОР
починає виконання команд на початку програми і продовжує дотримуючись інструкцій в послідовності, визначеній
програмою до тих пір, поки програма не завершується. p>
Скажімо,
програма має п'ять команд: BCD і E, що виконуються послідовно
(ніяких переходів немає в цьому прикладі).
Коли програма має один потік, команди будуть завжди виконувати в точно тому ж самому порядку: A, B, C, D і E.
Дійсно, ЦЕНТРАЛЬНИЙ ПРОЦЕСОР може зажадати часу для виконання інших команд в інші
програмах, але вони не будуть впливати на цей додаток, якщо не є конфлікт над загальними ресурсами
системи, але це вже окрема тема для розмови. p>
Просунута
багатопотокова операційна система типу Windows дозволяє додатку виконувати більше ніж один потік одночасно. Скажімо,
команда D в нашому типовому додатку могла створити новий потік, який стартував командою B і далі
виконував послідовність команд C і E. Перший потік був би все ще A, B, C, D, E, та коли команда D
виконається, виникне новий потік, який виконає команди б B, C, E (тут команди D вже не буде,
інакше ми отримаємо ще один потік). p>
В
якому порядку будуть слідувати команди в цьому додатку? p>
Це
могло б бути: p>
Thread
1 A B C D E E p>
Thread
2 B C p>
Або
так: p>
Thread 1 A B C D E p>
Thread 2 B C E p>
Або отак: p>
Thread 1 A B C D E p>
Thread
2 B C E p>
Іншими
словами, коли Ви починаєте новий потік виконання в програмі, Ви ніколи не
можете знати точний порядок, в якому
команди в двох потоках виконуватися щодо один одного. Два потоки повністю незалежні. p>
Чому
- Це проблема? p>
Імітатор багатопоточності h2>
Розглянемо
проект MTDemo: p>
Проект
містить один модуль коду, в якому міститься два глобальних змінних: p>
'MTDemo - Multithreading Demo
program p>
'Copyright © 1997 by Desaware Inc.
All Rights Reserved p>
Option Explicit p>
Public GenericGlobalCounter As Long p>
Public
TotalIncrements As Long p>
'
Цей проект містить одну форму - frmMTDemo1, яка містить p>
'наступний код: p>
'MTDemo - Multithreading Demo
program p>
'Copyright © 1997 by Desaware Inc.
All Rights Reserved p>
Option Explicit p>
Dim State As Integer p>
'State = 0 - Idle p>
'State = 1 - Loading existing
value p>
'State = 2 - Adding 1 to existing
value p>
'State = 3 - Storing existing
value p>
'State = 4 - Extra delay p>
Dim Accumulator As Long p>
Const OtherCodeDelay = 10 p>
Private Sub Command1_Click () p>
Dim f As New frmMTDemo1 p>
f.Show p>
End Sub p>
Private Sub Form_Load () p>
Timer1.Interval = 750 + Rnd * 500 p>
End Sub p>
Private Sub Timer1_Timer () p>
Static otherdelay & p>
Select Case State p>
Case 0 p>
lblOperation = "Idle" p>
State = 1 p>
Case 1 p>
lblOperation = "Loading
Acc " p>
Accumulator =
GenericGlobalCounter p>
State = 2 p>
Case 2 p>
lblOperation =
"Incrementing" p>
Accumulator = Accumulator + 1 p>
State = 3 p>
Case 3 p>
lblOperation =
"Storing" p>
GenericGlobalCounter =
Accumulator p>
TotalIncrements = TotalIncrements +
1 p>
State = 4 p>
Case 4 p>
lblOperation = "Generic
Code " p>
If otherdelay> = OtherCodeDelay
Then p>
State = 0 p>
otherdelay = 0 p>
Else
p>
otherdelay = otherdelay + 1 p>
End If p>
End Select p>
UpdateDisplay p>
End Sub p>
Public Sub UpdateDisplay () p>
lblGlobalCounter =
Str $ (GenericGlobalCounter) p>
lblAccumulator =
Str $ (Accumulator) p>
lblVerification =
Str $ (TotalIncrements) p>
End
Sub p>
Ця
програма для моделювання багатопотокового режиму використовує таймер і простий
кінцевий автомат. Змінна State описує
п'ять команд, які ця програма виконує. State = 0 - пасивний стан. State = 1 завантажує локальну
змінну глобальної змінної GenericGlobalCounter. State = 2 збільшує на одиницю локальну змінну.
State = 3 запам'ятовує результат у змінній GenericGlobalCounter і збільшує змінну
TotalIncrements (яка вважає кількість збільшень змінної GenericGlobalCounter). State = 3
додає додаткову затримку, що представляє собою час, витрачений на виконання інших команд
в програмі. p>
Функція
UpdateDisplay оновлює три мітки на формі, які показують поточне значення
змінної GenericGlobalCounter,
локального суматора, і загальної кількості збільшень. p>
Кожен
сигнал таймера моделює цикл центральний процесор у поточному потоці. Якщо
Ви запустіть програму, то побачите, що
значення змінної GenericGlobalCounter буде завжди точно так само змінної TotalIncrements, тому що
мінлива TotalIncrements показує кількість збільшень лічильника GenericGlobalCounter потоком. p>
Але
що станеться, коли Ви натискаєте кнопку Command1 і запустіть другий примірник
форми? Ця нова форма змоделює
другий потік. p>
Час
від часу, команди вишикуються в лінію таким чином, що обидві форми завантажать
однакове значення
GenericGlobalCounter, збільшать і збережуть його. У результаті, значення лічильника
збільшиться тільки на одиницю, навіть при
те, що кожен потік думав, що він незалежно збільшує значення лічильника.
p>
Іншими
словами, мінлива була збільшена двічі, але значення збільшилося тільки на
одиницю. Якщо ви запускаєте кілька
форм, то відразу помітите, що число збільшень, що представляється змінною TotalIncrements, зростає набагато швидше, ніж
лічильник GenericGlobalCounter. p>
Що,
якщо змінна представляє Об'єктовий рахунок блокування - який стежить, коли
об'єкт повинен бути звільнений? Що, якщо
вона являє собою сигнал, який вказує, що ресурс знаходиться у використанні? p>
Така
проблема може привести до появи ресурсів, постійно недоступних в системі,
до об'єкта, який блокує в пам'яті, або
передчасно звільнено. Це може призвести до збоїв програми. p>
Цей
приклад був розроблений, щоб достатньо просто побачити проблему, але спробуйте поекспериментувати зі значенням змінної
OtherCodeDelay. Коли небезпечний код відносно невеликий в порівнянні з усією програмою, проблеми
з'являться менш часто. Хоча це і звучить обнадійливо, але істина полягає в наступному. Проблеми
Багатопотокового режиму можуть бути надзвичайно нестійкі і їх важко виявити. Це означає, що
багато-режим потребує обережного підходу до проектування програми. p>
Вирішення проблем багатопоточності h2>
Є
два відносно простих способи уникнути проблем багатопотокового режиму. p>
Уникайте
загального використання глобальних змінних. p>
Додайте
код синхронізації скрізь, де використовуються глобальні змінні. p>
Перший
підхід використовується в основному в Visual Basic. Коли Ви вмикаєте багато -
режим в Visual p>
Basic
додатки, всі глобальні змінні стануть локальними для специфічного
потоку. Це властиво способу, з
яким Visual Basic виконує apartment model threading - докладніше про це
пізніше. p>
Початковий
випуск Visual Basic 5.0 дозволяв використовувати багатопоточність тільки для компонентів,
які не мали ніяких елементів
призначеного для користувача інтерфейсу. Так було тому що вони не мали безпечного потоку управління формами.
Наприклад: коли Ви створюєте форму в Visual Basic, VB дає їй ім'я глобальної змінної (таким чином, якщо Ви
маєте форму, іменовану Form1, Ви можете безпосередньо звертатися до її методів,
використовуючи Form1.метод замість того, щоб оголосити окрему змінну форми). Цей тип глобальної
змінної може викликати проблеми багатопотокового режиму, які Ви бачили раніше. Були безсумнівно
інші проблеми всередині управління формами. p>
З
service pack 2, керування формами Visual Basic було зроблено безпечним
потоком. Це говорить про те, що кожен
потік має власну глобальну змінну для кожної форми, визначеної в
проекті. p>
Що нового в Service Pack 2 h2>
Зробивши
потік управління формами безпечним, Service pack 2 надав можливість з
допомогою Visual p>
Basic
створювати клієнтські додатки, що використовують багато-режим. p>
Додаток
повинно бути визначено як програма ActiveX Exe з установкою запуску з Sub
Main: p>
'MTDemo2 - Multithreading demo
program p>
'Copyright © 1997 by Desaware Inc.
All Rights Reserved p>
Option Explicit p>
Declare Function FindWindow Lib
"user32" Alias "FindWindowA" _ p>
(ByVal lpClassName As String, ByVal
lpWindowName As String) _ p>
As Long p>
Sub Main () p>
Dim f As frmMTDemo2 p>
'We need this because Main is
called on each new thread p>
Dim hwnd As Long p>
hwnd = FindWindow (vbNullString,
"Multithreading Demo2") p>
If hwnd = 0 Then p>
Set f = New frmMTDemo2 p>
f.Show p>
Set f = Nothing p>
End If p>
End
Sub p>
Перший
разу програма завантажує і відображає основну форму додатку. Підпрограма
Main має з'ясувати, чи є це
першим потоком програми, тому цей код виконується при старті кожного потоку. Ви не можете використовувати глобальну
змінну, щоб це з'ясувати, бо Visual Basic apartment model зберігає глобальні змінні
специфічними для одиночного потоку. У цьому прикладі використовується функція API FindWindow, щоб
перевірити, чи була завантажена основна форма прикладу. p>
Є
інші способи з'ясувати, чи є це основним потоком, включаючи
використання об'єктів синхронізації
системи - але це окрема тема для розмови. p>
Багатопотокові
режим реалізується створенням об'єкта в новому потоці. Об'єкт повинен бути
визначений, використовуючи модуль класу. У
цьому випадку, простий модуль класу визначається наступним чином: p>
'MTDemo2 - Multithreading demo
program p>
'Copyright © 1997 by Desaware Inc.
All Rights Reserved p>
Option Explicit p>
Private Sub Class_Initialize () p>
Dim f As New frmMTDemo2 p>
f.Show p>
Set
f = Nothing p>
End
Sub p>
Ми
можемо встановити змінну форми як nothing після того, як вона створена,
тому що після відображення форми вона
буде збережена. p>
'MTDemo2 - Multithreading demo
program p>
'Copyright © 1997 by Desaware Inc.
All Rights Reserved p>
Option Explicit p>
Private Sub cmdLaunch1_Click () p>
Dim c As New clsMTDemo2 p>
c.DisplayObjPtr Nothing p>
End Sub p>
Private Sub cmdLaunch2_Click () p>
Dim c As clsMTDemo2 p>
Set c =
CreateObject ( "MTDemo2.clsMTDemo2") p>
End Sub p>
Private Sub Form_Load () p>
lblThread.Caption =
Str $ (App.ThreadID) p>
End
Sub p>
Форма
відображає ідентифікатор потоку в мітці на формі. Форма містить дві командні
кнопки, один з яких використовує
оператор New, інша-використовує оператор CreateObject. p>
Якщо
Ви запустіть програму всередині середовища Visual Basic, то побачите, що форми завжди
створюються в одному і тому ж потоці. Це
відбувається, тому що середовище Visual Basic підтримує тільки одиночний потік.
Якщо Ви скомпіліруете і запустіть
програму, то побачите, що підхід, який використовує CreateObject створює і clsMTDemo2 та її форму у новому потоці. p>
Чому багатопоточність
h2>
Звідки
вся метушня щодо багатопотокового режиму, якщо він включає так багато потенційної
небезпеки? Тому що, в деяких
ситуаціях, багато-режим може значно покращувати ефективність програми. У деяких випадках
це може покращувати ефективність деяких операцій синхронізації типу очікування завершення
додатки. Це дозволяє зробити архітектуру програми більш гнучкою. Наприклад, операція Add a long у формі
MTDEMO2 з наступним кодом: p>
Private Sub cmdLongOp_Click () p>
Dim l & p>
Dim s $ p>
For l = 1 To 1000000 p>
s = Chr $ (l And & H7F) p>
Next
l p>
End
Sub p>
Запустіть
кілька екземплярів форми, використовуючи кнопку cmdLaunch1. Коли Ви натискаєте на
кнопку cmdLongOp на будь-який з форм, то
побачите, що ця дія заморожує операції на всіх інших формах. Так відбувається, тому що всі форми виконуються в
одиночному потоці - і цей потік зайнятий виконанням довгого циклу. Якщо Ви запустіть кілька
екземплярів форми кнопкою cmdLaunch2 і нажімете кнопку cmdLongOp на форму, то тільки ця форма буде
заморожена - інші форми будуть активними. Вони виконуються у власних потоках, і довгий
цикл буде виконуватися тільки у власному потоці. p>
Звичайно,
в будь-якому випадку, Ви, ймовірно, не повинні розміщувати тривалі операції такого типу
у ваших формах. p>
Далі
коротке резюме, коли важливий багато-режим: p>
Сервер
ActiveX EXE - без загальних ресурсів. p>
Коли
Ви маєте ActiveX EXE сервер, який Ви збираєтеся спільно використовувати
серед декількох додатків,
багато-режим запобігає програми від небажаних взаємодій з один одним. Якщо одне
додаток виконує довгу операцію на об'єкті в однопотоковим сервер, інші програми будуть
витіснені, тобто будуть чекати, коли звільниться сервер. Багато-режим рещает цю
проблему. Проте, є випадки, де Ви можете хотіти використовувати ActiveX EXE сервер, щоб
регулювати доступ до загальнодоступних ресурсах (shared resource). Наприклад, сервер stock
quote, описаний у моїй книзі Developing ActiveX Components. У цьому випадку сервер stock quote
виконується в одиночному потоці і що доступний для всіх додатків, що використовують сервер з
черги. p>
багато-клієнт - виконуваний як ActiveX EXE
сервер h2>
Проста
форма цього підходу продемонстрована в додатку MTDEMO2. Цей підхід використовується, коли програма підтримує
множинні вікна, які повинні виходити з однієї програми, але працювати повністю
незалежно. Інтернет-браузер - гарний приклад такого багатопотокового клієнта, де кожне
вікно виконується у власному потоці. Тут слід звернути увагу на те, що багато -
режим не повинен використовуватися як заміна для гарного подієво керованого проекту. p>
Багатопотокові сервери DLL або EXE h2>
В
архітектурі клієнт-сервер, багато-режим може збільшити ефективність,
якщо Ви маєте суміш довгих і коротких
клієнтських запитів. Будьте уважним, хоча - якщо всі ваші клієнтські запити мають подібну довжину,
багато-режим може фактично сповільнювати середній час відповіді сервера! Ніколи не
приймайте на віру той факт, що якщо ваш сервер є багатопоточність, то обов'язково його
ефективність збільшиться. p>
Угода про потоках
h2>
вірите
чи ні, але все це було введенням. Частина цього матеріалу є оглядом
матеріалу, який описаний у моїй книзі
Developing ActiveX Components, інша частина матеріалу описує нову
інформацію для service pack 2. p>
Тепер,
дозволите задавати питання, яке має відношення до багатопотоковому режиму,
що використовує COM (модель
багатокомпонентних об'єктів, на якій засновані не тільки всі Visual Basic
об'єкти, а й інші windows програми,
використовують технології OLE). p>
Дано:
p>
Багатопотокові
режим є потенційно небезпечним взагалі, і особливо спроби багатопотокового кодування додатків, які не розроблені
для підтримки багатопотокового режиму, швидше за все призведуть до фатальних помилок і збоїв системи. p>
Запитання:
p>
Як
це можливо, що Visual Basic дозволяє Вам створювати об'єкти і використовувати
їх з поодинокими і багато-середовищами
безвідносно до того, розроблені вони для одиночного або багатопотокового використання? p>
Іншими
словами - Як багатопотокові Visual Basic додатки можуть використовувати об'єкти,
які не розроблені для безпечного
виконання в многопоточной середовищі? Як можуть інші багатопотокові програми використовувати Однопотокові об'єкти
Visual Basic? p>
Коротко:
як COM підтримує потоки? p>
Якщо
Ви знаєте COM, то Ви знаєте, що COM визначає структуру угоди. Об'єкт
COM погоджується слідувати деяким
правилами так, щоб цим можна було успішно користуватися з будь-якої програми
або об'єкта, що підтримує COM. p>
Більшість
людей спочатку думає про інтерфейсній частини угоди - про методи і властивості,
які надає об'єкт. p>
Але
Ви не можете не знати того, що COM також визначає потоковість як частина
угоди. І подібно до будь-якої частини
угоди COM - якщо Ви порушуєте ці умови, то будете мати проблеми.
Visual Basic, природно, приховує від
Вас більшість механізмів COM, але щоб зрозуміти як використовувати багатопоточність в Visual Basic, Ви повинні
розібратися COM моделі потоків. p>
Модель
одиночного потоку: p>
Однопотокові
сервер - найпростіший тип реалізації сервера. І найпростіший для розуміння. p>
В
цьому випадку EXE сервер виконується в одиночному потоці. Всі об'єкти створюються в
цьому потоці. p>
Всі
виклики методів кожного об'єкта, що підтримується сервером повинні прибути в цей
потік. p>
Але
що буде, якщо клієнт виконується в іншому потоці? У тому випадку, для об'єкта
сервера повинен бути створений
проміжний об'єкт (proxy object). Цей проміжний об'єкт виконується в потоці клієнта і відображає методи і властивості
фактичного об'єкта. Коли викликається метод проміжного об'єкта, він виконує
операції, необхідні для підключення до потоку об'єкта, а потім викликає метод фактичного
об'єкта, використовуючи параметри, передані до проміжного
об'єкту. Природно, що цей підхід вимагає значного часу на виконання завдання, проте він дозволяє
виконати всі угоди. Цей процес перемикання потоків і пересилання даних від проміжного
об'єкта до фактичного об'єкту і назад називається marshalling. Ця тема обговорюється
в главі 6 моєї книги Developing ActiveX Components. p>
В
випадку DLL серверів, одиночна потокова модель вимагає, щоб всі об'єкти в
сервер створювалися і викликалися у тому
ж самому потоці що і перший об'єкт, створений сервером. p>
Модель Apartment Threading h2>
Зверніть
увагу, що модель Apartment Threading як визначено COM не вимагає, щоб
кожен потік мав власний набір
глобальних змінних. Visual Basic таким чином реалізує модель Apartment Threading. Модель Apartment
Threading декларує, що кожний об'єкт може бути створений у власному потоці, проте, як
тільки об'єкт створений, його методи і властивості можуть викликатися тільки тим же самим потоком,
що створив об'єкт. Якщо об'єкт іншого потоку захоче мати доступ до методів цього об'єкту,
то він повинен діяти через проміжний об'єкт. p>
Така
модель щодо легкою для реалізації. Якщо Ви виправили глобальні
змінні (як робить Visual Basic),
модель Apartment Threading автоматично гарантує безпеку потоку - так як кожен об'єкт справді
виконується у власному потоці, і завдяки відсутності глобальних змінних, об'єкти в різних
потоках не взаємодіють один з одним. p>
Модель вільних потоків h2>
Модель
вільних потоків (Free Threading Model) полягає в наступному .. Будь-який об'єкт
може бути створений в будь-якому потоці. Всі
методи і властивості будь-якого об'єкта можуть бути викликає в будь-який час з будь-якого потоку. Об'єкт приймає на
себе всю відповідальність за обробку будь-якої необхідної синхронізації. p>
Це
найважча у реалізації модель, так як потрібно, щоб всю синхронізацію
обробляв програміст. Фактично до
недавнього часу, технологія OLE безпосередньо не підтримувала цю модель! Однак, з тих пір
marshalling ніколи не потрібно і це найбільш ефективна модель потоків. p>
Яку
модель підтримує ваш сервер? p>
Як
програму або сама Windows дізнається, яку модель потоків використовує сервер?
Ця інформація включена в системний
реєстр (registry). Коли Visual Basic створює об'єкт, він перевіряє системний
реєстр, щоб визначити, в яких
випадках потрібно використовувати проміжний об'єкт (proxy object) і в яких - marshalling. p>
Ця
перевірка є обов'язком клієнта і необхідна для суворої підтримки
вимог багатопоточності для кожного
об'єкта, якого він створює. p>
Функція
API CreateThread p>
Тепер
давайте подивимося, як з Visual Basic може використовуватися функція API
CreateThread. Скажімо, Ви маєте клас,
що Ви хочете виполненять в іншому потоці, наприклад, щоб виконати деяку
фонову операцію. Характерний клас
такого типу міг би мати наступний код (з прикладу MTDemo 3): p>
'Class clsBackground p>
'MTDemo 3 - Multithreading example p>
'Copyright © 1997 by Desaware Inc.
All Rights Reserved p>
Option Explicit p>
Event DoneCounting () p>
Dim l As Long p>
Public Function DoTheCount (ByVal
finalval &) As Boolean p>
Dim s As String p>
If l = 0 Then p>
s $ = "In Thread" &
App.threadid p>
Call MessageBox (0, s $, "",
0) p>
End If p>
l = l + 1 p>
If l> = finalval Then p>
l = 0 p>
DoTheCount = True p>
Call MessageBox (0, "Done with
counting "," ", 0) p>
RaiseEvent DoneCounting p>
End If p>
End Function p>
Клас
розроблений так, щоб функція DoTheCount могла неодноразово викликатися з безперервного
циклу у фоновому потоці. Ми могли б
помістити цикл безпосередньо в сам об'єкт, але незабаром ви побачите, що були вагомі причини для проектування об'єкта як
показано в прикладі. При першому виклику функції DoTheCount з'являється MessageBox, в якому показано
ідентифікатор потоку, за яким ми можемо визначити потік, в якому виконується код. Замість VB команди
MessageBox використовується MessageBox API, тому що функція API, як відомо, підтримує безпечне
виконання потоків. Другий MessageBox з'являється після того, як закінчений підрахунок і Згенеровано подія,
яке вказує, що операція завершена. p>
Шпалери
потік запускається за допомогою наступного коду у формі frmMTDemo3: p>
Private Sub
cmdCreateFree_Click () p>
Set c = New clsBackground p>
StartBackgroundThreadFree c p>
End Sub p>
Функція StartBackgroundThreadFree визначена в
модулі modMTBack наступним чином: p>
Declare Function CreateThread Lib
"kernel32" _ p>
(ByVal lpSecurityAttributes As Long,
ByVal _ p>
dwStackSize As Long, ByVal
lpStartAddress As Long, _ p>
ByVal lpParameter As Long, ByVal
dwCreationFlags _ p>
As Long, lpThreadId As Long) As Long
p>
Declare Function CloseHandle Lib
"kernel32" _ p>
(ByVal hObject As Long) As Long p>
'Start the background thread for
this object p>
'using the invalid free threading
approach. p>
Public Function
StartBackgroundThreadFree _ p>
(ByVal qobj As clsBackground) p>
Dim threadid As Long p>
Dim hnd & p>
Dim threadparam As Long p>
'Free threaded approach p>
threadparam = ObjPtr (qobj) p>
hnd = CreateThread (0, 2000,
AddressOf _ p>
BackgroundFuncFree, threadparam, 0,
threadid) p>
If hnd = 0 Then p>
'Return with zero (error) p>
Exit Function p>
End If p>
'We don't need the thread
handle p>
CloseHandle hnd p>
StartBackgroundThreadFree = threadid
p>
End Function p>
Функція
CreateThread має шість параметрів: p>
lpSecurityAttributes
- Зазвичай встановлюється в нуль, щоб використовувати задані за замовчуванням атрибути захисту. p>
dwStackSize
- Розмір стека. Кожен потік має власний стек. p>
lpStartAddress
- Адреса пам'яті, де стартує потік. Він повинен бути рівний адресою функції в
стандартному модулі, отриманому при
використанні оператора AddressOf. p>
lpParameter
- Long 32 розрядний параметр, який передається функції, запускає новий
потік. p>
dwCreationFlags
- 32 біт мінлива прапорів, яка дозволяє Вам управляти запуском потоку (активний, припинений і т.д.).
Докладніше про ці прапорах можна почитати в Microsoft's online 32 bit reference. p>
lpThreadId
- Змінна, в яку завантажується унікальний ідентифікатор нового потоку. p>
Функція
повертає дескриптор потоку. p>
В
цьому випадку ми передаємо покажчик на об'єкт clsBackground, який ми будемо
використовувати в новому потоці. ObjPtr
відновлює значення покажчика інтерфейсу в змінну qobj. Після створення
потоку закривається дескриптор за допомогою
функції CloseHandle. Ця дія не завершує потік, - потік продовжує виконуватися до виходу з функції
BackgroundFuncFree. Однак, якщо ми не закрили дескриптор, то об'єкт потоку буде існувати навіть після
виходу з функції BackgroundFuncFree. Всі дескриптори p>
потоку
повинні бути закриті і при завершенні потоку система звільняє зайняті потоком
ресурси. p>
Функція
BackgroundFuncFree має наступний код: p>
'A free threaded callback. p>
'A free threaded callback. p>
'This is an invalid approach,
though it works p>
'in this case. p>
Public Function
BackgroundFuncFree (ByVal param As _ p>
IUnknown) As Long p>
Dim qobj As clsBackground p>
Dim res & p>
'Free threaded approach p>
Set qobj = param p>
Do While Not
qobj.DoTheCount (100000) p>
Loop
p>
'qobj.ShowAForm' Crashes! p>
'Thread ends on return p>
End Function p>
Параметром
цієї функції є-вказівник на інтерфейс (ByVal param As IUnknown). При
це ми можемо уникнути неприємностей,
тому що під COM кожен інтерфейс грунтується на IUnknown, так що такий тип параметра допустимо незалежно від типу
інтерфейсу, що передається функції. Ми, однак, повинні негайно визначити param як тип об'єкта,
щоб потім його використовувати. У цьому випадку qobj встановлюється як об'єкт clsBackground,
який був переданий до об'єкта StartBackgroundThreadFree. p>
Функція
потім виконує нескінченний цикл, протягом якого може виконуватися будь-яка
необхідна операція, в цьому випадку
повторний рахунок. Подібний підхід міг би використовуватися тут, щоб виконати операцію очікування, яка призупиняє
потік поки не проізойдкт системне подія (типу завершення процесу). Потік потім міг би
викликати метод класу, щоб повідомити додатку, що подія відбулася. p>
Доступ
до об'єкта qobj надзвичайно швидкий через використання підходу вільного потоку
(free threading) - ніяка переадресація
(marshalling) при цьому не використовується. p>
Зверніть
увагу на те, що якщо Ви спробуєте використовувати об'єкт clsBackground,
який показує форму, то це
приведе до збоїв програми. Зверніть також увагу на те, що подія
завершення ніколи не відбувається в
клієнтської формі. Дійсно, навіть Microsoft Systems Journal, який
описує цей підхід, містить дуже
багато попереджень про те, що при використанні цього підходу є деякі речі, які не працюють. p>
Деякі
розробники, хто пробували розгортати додатки, що застосовують цей тип багатопоточності, виявили, що їх застосування
викликають збої після оновлення до VB5 service pack 2. p>
Є
Чи це дефектом Visual Basic? p>
означає
Чи означає це, що Microsoft не забезпечила сумісність? p>
Відповідь
на обидва питання: Ні p>
Проблема
не в Microsoft або Visual Basic. p>
Проблема
полягає в тому, що вищезазначений код є сміттям. p>
Проблема
проста - Visual Basic підтримує об'єкти і в моделі одиночного потоку і в
apartment model. p>
Дозвольте
мені перефразувати це: об'єкти Visual Basic є COM об'єктами і
вони, згідно з COM угодою, будуть
правильно працювати як в моделі одиночного потоку так і в apartment model. Це
означає, що кожен об'єкт очікує, що
будь-які виклики методів будуть відбуватися в тому ж самому потоці, який створив об'єкт. p>
Приклад,
показаний вище, порушує це правило. p>
Це
порушує угоду COM. p>
Що
це означає? p>
Це
означає, що поведінка об'єкта підпорядкованої змін, так як Visual Basic
постійно модифікується. p>
Це
означає, що будь-яка спроба об'єкта звернутися до інших об'єктів або форм
може потерпіти невдачу і що причини
відмов можуть змінюватися, оскільки ці об'єкти модифікуються. p>
Це
означає, що навіть код, який зараз працює, може раптово викликала збій,
оскільки інші об'єкти додаються,
видаляються чи змінюються. p>
Це
означає, що неможливо характеризувати поведінку за або передбачити,
чи буде воно працювати чи може чи
працювати в будь-якій даному середовищі. p>
Це
означає, що неможливо передбачити, чи код працювати на будь-який даної
системі, і що його поведінка може
залежати від операційної, кількості використовуваних процесорів та інших проблем конфігурації системи. p>
Ви
бачите, що як тільки Ви порушуєте угоду COM, Ви більше не захищені тими
можливостями COM, які дозволяють
об'єктам успішно взаємодіяти один з одним і з клієнтами. p>
Цей
підхід є програмної алхімією. Це безвідповідально і жоден програміст
не повинен коли-небудь використовувати це.
Точка. p>
Назад
до функції API CreateThread p>
Тепер,
коли я показав Вам, чому підхід до використання CreateThread API, показаний
в деяких статтях, є сміттям, я
покажу Вам, як можна використовувати цю функцію API безпечно. Прийом простий - p>
Ви
повинні просто твердо притримати угоди COM про потоках. Це займе небагато
більше часу і зусиль, але практика
показала, що виходять дуже надійні результати. p>
Приклад
MTDEMO3 демонструє цей підхід у формі frmMTDemo3, що має код, який
запускає клас фону в apartment model
наступним чином: p>
Private Sub
cmdCreateApt_Click () p>
Set c = New clsBackground p>
StartBackgroundThreadApt
c p>
End
Sub p>
Поки
це виглядає дуже схоже на підхід вільних потоків. Ви створюєте екземпляр
класу і передаєте його функції, яка
запускає фоновий потік. У модулі modMTBack з'являється наступний код: p>
'Structure to hold IDispatch GUID p>
Type GUID p>
Data1 As Long p>
Data2 As Integer p>
Data3 As Integer p>
Data4 (7) As Byte p>
End Type p>
Public IID_IDispatch As GUID p>
Declare Function
CoMarshalInterThreadInterfaceInStream Lib _
p>
"ole32.dll" (riid As GUID,
ByVal pUnk As IUnknown, _ p>
ppStm As Long) As Long p>
Declare Function
CoGetInterfaceAndReleaseStream Lib _ p>
"ole32.dll" (ByVal pStm As
Long, riid As GUID, _ p>
pUnk As IUnknown) As Long p>
Declare Function CoInitialize Lib
"ole32.dll" (ByVal _ p>
pvReserved As Long) As Long p>
Declare Sub CoUninitialize Lib
"ole32.dll" () p>
'Start the background thread for
this object p>
'using the apartment model p>
'Returns zero on error p>
Public Function
StartBackgroundThreadApt (ByVal qobj _ p>
As clsBackground) p>
Dim threadid As Long p>
Dim hnd &, res & p>
Dim threadparam As Long p>
Dim tobj As Object p>
Set tobj = qobj p>
'Proper marshaled approach p>
InitializeIID p>
res =
CoMarshalInterThreadInterfaceInStream _ p>
(IID_IDispatch, qobj,
threadparam) p>
If res 0 Then p>
StartBackgroundThreadApt = 0 p>
Exit Function p>
End If p>
hnd = CreateThread (0, 2000,
AddressOf _ p>
BackgroundFuncApt, threadparam, 0,
threadid) p>
If hnd = 0 Then p>
'Return with zero (error) p>
Exit Function p>
End If p>
'We don't need the thread
handle p>
CloseHandle hnd p>
StartBackgroundThreadApt = threadid p>
End Function p>
Функція
StartBackgroundThreadApt трохи більш складна ніж її еквівалент при застосуванні
підходу вільних потоків. Перша нова
функція називається InitializeIID. Вона має наступний код: p>
'Initialize the GUID structure p>
Private Sub InitializeIID () p>
Static Initialized As Boolean p>
If Initialized Then Exit Sub p>
With IID_IDispatch p>
. Data1 = & H20400 p>
. Data2 = 0 p>
. Data3 = 0 p>
. Data4 (0) = & HC0 p>
. Data4 (7) = & H46 p>
End With p>
Initialized
= True p>
End
Sub p>
Ви
бачите, нам необхідний ідентифікатор інтерфейсу - 16 байтове структура, яка
унікально визначає інтерфейс. У
Зокрема нам необхідний ідентифікатор інтерфейсу для інтерфейсу IDispatch (детальна інформація щодо IDispatch
може бути знайдена в моїй книзі Developing ActiveX Components). Функція InitializeIID просто
ініціалізує структуру IID_IDISPATCH до коректним значенням для ідентифікатора інтерфейсу IDispatch.
Значення Це значення виходить за допомогою використання утиліти перегляду системного реєстру. p>
Чому
нам потрібен цей ідентифікатор? p>
Тому
що, щоб твердо дотримуватися угоди COM про потоки, ми повинні створити
проміжний об'єкт (proxy object) для
об'єкта clsBackground. Проміжний об'єкт
повинен бути переданий новому потоку замість
початкового об'єкта. Звернення до нового потоку на проміжному об'єкті
будуть пе?? еадресовани (marshaled) в
поточний потік. p>
CoMarshalInterThreadInterfaceInStream
виконує цікаву задачу. Вона збирає всю інформацію, необхідну при створенні проміжного
об'єкта, для певного інтерфейсу і завантажує її на об'єкт потоку (stream object). У цьому прикладі ми
використовуємо інтерфейс IDispatch, тому що ми знаємо, що кожен клас Visual Basic підтримує IDispatch і ми
знаємо, що підтримка переадресації (marshalling) IDispatch вбудована в Windows - так що цей код буде
працювати завжди. Потім ми передаємо об'єкт потоку (stream object) новому потоку. Цей об'єкт розроблений Windows,
щоб бути переданим між потоками однаковим способом, так що ми можемо безпечно
передавати його функції CreateThread. Інша частина функції StartBackgroundThreadApt ідентична функції
StartBackgroundThreadFree. p>
Функція
BackgroundFuncApt також складніше ніж її еквівалент при використанні моделі вільних
потоків і показано нижче: p>
'A correctly marshaled apartment
model callback. p>
'This is the correct approach,
though slower. p>
Public Function
BackgroundFuncApt (ByVal param As Long) As Long
p>
Dim qobj As Object p>
Dim qobj2 As clsBackground p>
Dim res & p>
'This new thread is a new
apartment, we must p>
'initialize OLE for this
apartment p>
'(VB doesn't seem to do it) p>
res = CoInitialize (0) p>
'Proper apartment modeled
approach p>
res =
CoGetInterfaceAndReleaseStream (param, _ p>
IID_IDispatch, qobj) p>
Set qobj2 = qobj p>
Do While Not
qobj2.DoTheCount (10000) p>
Loop
p>
qobj2.ShowAForm p>
'Alternatively, you can put a wait
function here, p>
'then call the qobj function when
the wait is satisfied p>
'All calls to CoInitialize must be
balanced p>
CoUninitialize
p>
End
Function p>
Перший
крок повинен ініціалізувати підсистему OLE для нового потоку. Це необхідно
для переадресації (marshalling) коду,
щоб працювати коректно. CoGetInterfaceAndReleaseStream створює проміжний об'єкт для об'єкта clsBackground
і реалізує об'єкт потоку (stream object), який використовується для передачі даних з іншого потоку. Інтерфейс
IDispatch для нового об'єкта завантажується в змінну qobj. p>
Тепер
можливо отримати інші інтерфейси - проміжний об'єкт буде коректно
переадресовувати дані для кожного
інтерфейсу, який може підтримувати. p>
Тепер
Ви можете бачити, чому цикл поміщений у цю функцію замість того, щоб
знаходитися безпосередньо в об'єкті.
Коли Ви вперше викличте функцію qobj2.DoTheCount, то побачите, що код виконується у початковому потоці! Кожного разу,
коли Ви викликаєте метод об'єкта, Ви фактично викликаєте метод проміжного об'єкта. Ваш поточний
потік припиняється, запит методу переадресовується початкового потоку і викликається метод первісного
об'єкта в тій же самому потоці, який створив об'єкт. Якби цикл був в об'єкті, то Ви б
заморозили початковий потік. p>
Хорошим
результатом застосування цього підходу є те, що все працює правильно.
Об'єкт clsBackground може безпечно
показувати форми і генерувати події. Недоліком цього підходу є, звичайно, його більш повільне
виконання. Перемикання потоків і переадресація (marshalling) - відносно повільні операції. Ви фактично
ніколи не захочете виконувати фонову операцію як показано тут. p>
Але
цей підхід може надзвичайно добре працювати, якщо Ви можете поміщати фонову
операцію