Совместная работа с данными по мере их создания
Совместная работа с данными по мере их создания
В многопоточных приложениях часто встречается ситуация, когда потоки не только работают с общими данными, но и ожидают их появления (то есть поток 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.