Иллюстрированный самоучитель по VB.NET

       

Совместная работа с данными по мере их создания


Совместная работа с данными по мере их создания

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

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

  • Поток 1 (потребитель) активизируется, входите синхронизированный метод, ищет данные, не находит их и переходит в состояние ожидания. Предварителъно он должен снять блокировку, чтобы не мешать работе потока- поставщика.

  • Поток 2 (поставщик) входит в синхронизированный метод, освобожденный потоком 1, создает данные для потока 1 и каким-то образом оповещает поток 1 о наличии данных. Затем он снимает блокировку, чтобы поток 1 смог обработать новые данные.

    Не пытайтесь решить эту проблему постоянной активизацией потока 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.


    Содержание раздела