![]() |
![]() |
![]() |
Совместная
работа с данными по мере их
создания
В многопоточных
приложениях часто встречается
ситуация, когда потоки не только
работают с общими данными, но и
ожидают их появления (то есть поток
1 должен создать данные, прежде чем
поток 2 сможет их использовать).
Поскольку данные являются общими,
доступ к ним необходимо
синхронизировать. Также необходимо
предусмотреть средства для
оповещения ожидающих потоков о
появлении готовых данных.
Подобная
ситуация обычно называется проблемой
«поставщик/потребитель». Поток
пытается обратиться к данным,
которых еще нет, поэтому он должен
передать управление другому потоку,
создающему нужные данные. Проблема
решается кодом следующего вида:
Не
пытайтесь решить эту проблему
постоянной активизацией потока 1 с
проверкой состояния условной
переменной, значение которой>устанавливается
потоком 2. Такое решение серьезно
повлияет на быстродействие вашей
программы, поскольку в большинстве
случаев поток 1 будет
активизироваться без всяких причин;
а поток 2 будет переходить в
ожидание так часто, что у него не
останется времени на создание
данных.
Связи «поставщик/потребитель»
встречаются очень часто, поэтому в
библиотеках классов
многопоточного программирования
для таких ситуаций создаются
специальные примитивы. В .NET эти
примитивы называются Wait и Pulse-PulseAl 1
и являются частью класса Monitor.
Рисунок 10.8 поясняет ситуацию,
которую мы собираемся
запрограммировать. В программе
организуются три очереди потоков:
очередь ожидания, очередь
блокировки и очередь выполнения.
Планировщик потоков не выделяет
процессорное время потокам,
находящимся в очереди ожидания.
Чтобы потоку выделялось время, он
должен переместиться в очередь
выполнения. В результате работа
приложения организуется гораздо
эффективнее, чем при обычном опросе
условной переменной.
На псевдокоде
идиома потребителя данных
формулируется так:
' Вход в синхронизированный блок следующего вида
While нет
данных
Перейти в очередь ожидания
Loop
Если данные есть, обработать их.
Покинуть
синхронизированный блок
Сразу же после
выполнения команды Wait поток
приостанавливается, блокировка
снимается, и поток переходит в
очередь ожидания. При снятии
блокировки поток, находящийся в
очереди выполнения, получает
возможность работать. Со временем
один или несколько заблокированных
потоков создадут данные,
необходимые для работы потока,
находящегося в очереди ожидания.
Поскольку проверка данных
осуществляется в цикле, переход к
использованию данных (после цикла)
происходит лишь при наличии данных,
готовых к обработке.
На псевдокоде
идиома поставщика данных выглядит
так:
' Вход в синхронизированный блок вида
While данные
НЕ нужны
Перейти в очередь ожидания
Else
Произвести данные
После появления готовых данных вызвать Pulse-PulseAll.
чтобы
переместить один или несколько
потоков из очереди блокировки в
очередь выполнения. Покинуть
синхронизированный блок (и
вернуться в очередь выполнения)
Предположим,
наша программа моделирует семью с
одним родителем, который
зарабатывает деньги, и ребенком,
который эти деньги тратит. Когда
деньги кончаются, ребенку
приходится ждать прихода новой
суммы. Программная реализация этой
модели выглядит так:
1 Option Strict On
2 Imports System.Threading
3 Module Modulel
4 Sub Main()
5 Dim theFamily As
New Family()
6 theFamily.StartltsLife()
7 End Sub
8 End fjodule
9
10 Public Class
Family
11 Private mMoney As
Integer
12 Private mWeek As
Integer = 1
13 Public Sub
StartltsLife()
14 Dim aThreadStart
As New ThreadStarUAddressOf Me.Produce)
15 Dim bThreadStart
As New ThreadStarUAddressOf Me.Consume)
16 Dim aThread As
New Thread(aThreadStart)
17 Dim bThread As
New Thread(bThreadStart)
18 aThread.Name =
"Produce"
19 aThread.Start()
20 bThread.Name =
"Consume"
21 bThread. Start()
22 End Sub
23 Public Property
TheWeek() As Integer
24 Get
25 Return mweek
26 End Get
27 Set(ByVal Value
As Integer)
28 mweek - Value
29 End Set
30 End Property
31 Public Property
OurMoney() As Integer
32 Get
33 Return mMoney
34 End Get
35 Set(ByVal Value
As Integer)
36 mMoney =Value
37 End Set
38 End Property
39 Public Sub
Produce()
40 Thread.Sleep(500)
41 Do
42 Monitor.Enter(Me)
43 Do While Me.OurMoney
> 0
44 Monitor.Wait(Me)
45 Loop
46 Me.OurMoney =1000
47 Monitor.PulseAll(Me)
48 Monitor.Exit(Me)
49 Loop
50 End Sub
51 Public Sub
Consume()
52 MsgBox("Am
in consume thread")
53 Do
54 Monitor.Enter(Me)
55 Do While Me.OurMoney
= 0
56 Monitor.Wait(Me)
57 Loop
58 Console.WriteLine("Dear parent I just spent all your " & _
money in week "
& TheWeek)
59 TheWeek += 1
60 If TheWeek = 21 *52
Then System.Environment.Exit(0)
61 Me.OurMoney =0
62 Monitor.PulseAll(Me)
63 Monitor.Exit(Me)
64 Loop
65 End Sub
66 End Class
Метод StartltsLife (строки
13-22) осуществляет подготовку к
запуску потоков Produce и Consume. Самое
главное происходит в потоках Produce (строки
39-50) и Consume (строки 51-65). Процедура Sub
Produce проверяет наличие денег, и если
деньги есть, переходит в очередь
ожидания. В противном случае
родитель генерирует деньги (строка
46) и оповещает объекты в очереди
ожидания об изменении ситуации.
Учтите, что вызов Pulse-Pulse All вступает
в силу лишь при снятии блокировки
командой Monitor.Exit. И наоборот,
процедура Sub Consume проверяет наличие
денег, и если денег нет — оповещает
об этом ожидающего родителя. Строка
60 просто завершает программу по
прошествии 21 условного года; вызов
System. Environment.Exit(0) является .NET-аналогом
команды End (команда End тоже
поддерживается, но в отличие от System.
Environment. Exit она не позволяет вернуть
код завершения операционной
системе).
Потоки,
переведенные в очередь ожидания,
должны быть освобождены другими
час-тями вашей программы. Именно по
этой причине мы предпочитаем
использовать PulseAll вместо Pulse.
Поскольку заранее неизвестно,
какой именно поток будет
активизирован при вызове Pulse 1 ,
при относительно небольшом
количестве потоков в очереди с
таким же успехом можно вызвать PulseAll.
![]() |
![]() |
![]() |