Skip to content

8. Потоки виконання

8.1. Вступ

Коли запускається програма, вона виконується у потоці виконання, який називається потоком. Клас .NET, що моделює thread, — це клас System.Threading.Thread, який має таке визначення:

Image

Ми будемо використовувати лише деякі властивості та методи цього класу:

CurrentThread - propriété statique
повертає потік, що наразі виконується
Name - propriété d'objet
ім'я потоку
isAlive - propriété d'objet
вказує, чи є потік активним (true) чи ні (false)
Start - méthode d'objet
запускає виконання потоку
Abort - méthode d'objet
остаточно зупиняє виконання потоку
Sleep(n) - méthode statique
призупиняє виконання потоку на n мілісекунд
Suspend() - méthode d'objet
тимчасово призупиняє виконання потоку
Resume() - méthode d'objet
відновлює виконання призупиненого потоку
Join() - méthode d'objet
блокуюча операція — очікує завершення потоку, щоб перейти до наступної інструкції

Розглянемо першу програму, яка демонструє наявність головного потоку виконання — того, в якому виконується функція Main класу:


' використання потоків
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' ініціалізація поточного потоку
        Dim main As Thread = Thread.CurrentThread
        ' виведення на екран
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' зміна імені
        main.Name = "main"
        ' перевірка
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' нескінченний цикл
        While True
            ' виведення на екран
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' тимчасове зупинення
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Результати на екрані:

dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59

Попередній приклад ілюструє наступні моменти:

  • функція Main успішно виконується у потоці
  • доступ до характеристик цього потоку здійснюється за допомогою Thread.CurrentThread
  • роль методу Sleep. Тут потік, що виконує Main, регулярно переходить у режим очікування на 1 секунду між двома відображеннями.

8.2. Створення потоків виконання

Можливі програми, в яких фрагменти коду виконуються «одночасно» у різних потоках виконання. Коли кажуть, що thread виконуються одночасно, це часто є неточною формулюванням. Якщо машина має лише один процесор, як це досі часто буває, то thread-и ділять між собою цей процесор: кожен з них по черзі має доступ до нього протягом короткого проміжку часу (декількох мілісекунд). Саме це створює ілюзію паралельності виконання. Часовий проміжок, відведений для одного thread, залежить від різних факторів, зокрема від його пріоритету, який має значення за замовчуванням, але також може бути встановлений програмно. Коли thread має доступ до процесора, він зазвичай використовує його протягом усього відведеного йому часу. Однак він може звільнити його достроково:

  • перейшовши в режим очікування події (wait, join, suspend)
  • перейшовши в режим сну на певний час (sleep)
  1. Потік T спочатку створюється за допомогою його конструктора
Public Sub New(ByVal start As ThreadStart)

ThreadStart є типом делегату та визначає прототип функції без параметрів:

Public Delegate Sub ThreadStart()

Типова конструкція виглядає так:

dim T as Thread=new Thread(new ThreadStart(run));

Функція run, передана як параметр, буде виконана під час запуску потоку.

  1. Виконання потоку T запускається функцією T.Start(): функція [run], передана у конструктор T, буде виконана потоком T. Програма, яка виконує інструкцію T.start(), не чекає на завершення завдання T: вона одразу переходить до наступної інструкції. Таким чином, ми маємо два завдання, що виконуються паралельно. Часто їм потрібно мати можливість спілкуватися між собою, щоб знати, на якому етапі знаходиться спільна робота, яку потрібно виконати. Це і є проблема синхронізації потоків.
  1. Після запуску потік thread виконується автономно. Він зупиниться, коли функція start, яку він виконує, завершить свою роботу.
  1. Завдачі T можна надсилати певні сигнали:
    1. T.Suspend() наказує йому тимчасово зупинитися
    2. T.Resume() — наказує йому відновити роботу
    3. T.Abort() наказує їй остаточно зупинитися
  1. Також можна дочекатися завершення її виконання за допомогою T.join(). Це блокуюча інструкція: програма, яка її виконує, блокується доти, доки завдання T не завершить свою роботу. Це один із способів синхронізації.

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


' параметри
Option Strict On
Option Explicit On 

' простори імен
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' ініціалізація поточного потоку
        Dim main As Thread = Thread.CurrentThread
        ' присвоєння імені потоку
        main.Name = "main"

        ' створення потоків виконання
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' створення потоку i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' присвоєння імені потоку
            tâches(i).Name = "tache_" & i
            ' запуск виконання потоку i
            tâches(i).Start()
        Next i
        ' кінець функції main
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub

    Public Sub affiche()
        ' виведення повідомлення про початок виконання
        Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
        ' перехід у режим очікування на 1 с
        Thread.Sleep(1000)
        ' виведення повідомлення про завершення виконання
        Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

Головний потік, який виконує функцію Main, створює ще 5 потоків, що відповідають за виконання статичного методу affiche. Результати такі:

dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54

Ці результати є дуже показовими:

  • по-перше, видно, що запуск виконання потоку не є блокуючим. Метод Main запустив виконання 5 потоків паралельно і завершив своє виконання раніше за них. Операція
             ' запускаємо виконання потоку i
            tâches(i).Start()

запускає виконання потоку tâches[i], але після цього виконання негайно продовжується з наступною інструкцією, не чекаючи на завершення виконання потоку.

  • Усі створені потоки повинні виконати метод affiche. Порядок виконання є непередбачуваним. Навіть якщо в цьому прикладі порядок виконання, здається, відповідає порядку запитів на виконання, з цього не можна робити загальних висновків. Операційна система в даному випадку має 6 потоків і один процесор. Вона розподілятиме ресурси процесора між цими 6 потоками згідно з власними правилами.
  • У результатах ми бачимо наслідок виконання методу Sleep. У наведеному прикладі саме потік 0 першим виконує метод affiche. З’являється повідомлення про початок виконання, після чого він виконує метод Sleep, який призупиняє його на 1 секунду. Тоді він втрачає доступ до процесора, який стає доступним для іншого потоку. Приклад показує, що його отримає потік 1. Потік 1 пройде той самий шлях, що й інші потоки. Коли секунда очікування потоку 0 закінчиться, його виконання може поновитися. Система надає йому процесор, і він може завершити виконання методу affiche.

Змінимо нашу програму, щоб завершити метод Main за допомогою таких інструкцій:

         ' завершення обробки
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Виконання нової програми дає:

fin du thread main

Потоки, створені функцією Main, не виконуються. Це інструкція

        Environment.Exit(0)

призводить до цього: вона видаляє всі потоки програми, а не лише потік Main. Вирішити цю проблему можна так: метод Main повинен дочекатися завершення виконання потоків, які він створив, перш ніж завершити свою роботу. Це можна зробити за допомогою методу Join класу Thread:


         ' очікування завершення виконання всіх потоків
        For i = 0 To tâches.Length - 1
            ' очікування завершення виконання потоку i
            tâches(i).Join()
        Next i        'for
         ' завершення головного блоку
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

У результаті отримуємо такі результати:

Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main

8.3. Значення потоків

Тепер, коли ми виявили наявність потоку за замовчуванням, який виконує метод Main, і знаємо, як створювати інші, зупинимося на тому, у чому для нас полягає користь потоків і чому ми їх тут розглядаємо. Існує тип додатків, які добре підходять для використання потоків, — це клієнт-серверні додатки в Інтернеті. У такому додатку сервер, розташований на машині S1, відповідає на запити клієнтів, розташованих на віддалених машинах C1, C2, ..., Cn.

Ми щодня користуємося інтернет-додатками, що відповідають цій схемі: веб-сервіси, електронна пошта, перегляд форумів, передача файлів... У наведеній вище схемі сервер S1 повинен одночасно обслуговувати клієнтів Ci. Якщо взяти за приклад сервер FTP (File Transfer Protocol), який передає файли своїм клієнтам, то відомо, що передача файлу іноді може тривати кілька годин. Звісно, не може бути й мови про те, щоб один клієнт монополізував сервер на такий тривалий час. Зазвичай сервер створює стільки потоків виконання, скільки є клієнтів. Кожен потік відповідає за обслуговування конкретного клієнта. Оскільки процесор циклічно розподіляється між усіма активними потоками машини, сервер приділяє трохи часу кожному клієнту, забезпечуючи таким чином одночасність обслуговування.

8.4. Доступ до спільних ресурсів

У згаданому вище прикладі «клієнт-сервер» кожен потік обслуговує клієнта практично незалежно. Проте потоки можуть бути змушені співпрацювати, щоб надати клієнту запитувану послугу, зокрема для доступу до спільних ресурсів. Наведена вище схема нагадує вікна великої адміністративної установи, наприклад поштового відділення, де за кожним вікном співробітник обслуговує клієнта. Припустимо, що час від часу цим співробітникам доводиться робити ксерокопії документів, принесених їхніми клієнтами, а ксерокс є лише один. Два співробітники не можуть користуватися копіювальним апаратом одночасно. Якщо співробітник i виявляє, що копіювальний апарат зайнятий співробітником j, йому доведеться чекати. Таку ситуацію називають доступом до спільного ресурсу, і в інформатиці її досить складно організувати. Розглянемо такий приклад:

  • додаток генерує n потоків, де n передається як параметр
  • спільним ресурсом є лічильник, який повинен інкрементуватися кожним згенерованим потоком
  • Після завершення роботи програми на екрані відображається значення лічильника. Отже, слід знайти n.

Програма виглядає так:


' параметри
Option Explicit On 
Option Strict On

' використання потоків
Imports System
Imports System.Threading

Public Class thread3
    ' змінні класу
    Private Shared cptrThreads As Integer = 0

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' інструкція з експлуатації
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' перевірка кількості аргументів
        If args.Length <> 1 Then
            ' помилка
            Console.Error.WriteLine(syntaxe)
            ' зупинка
            Environment.Exit(1)
        End If
        ' перевірка якості аргументу
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' помилка
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' кінець
            Environment.Exit(2)
        End Try
        ' створення та генерація потоків
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' створення
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' іменування
            threads(i).Name = "tache_" & i
            ' запуск
            threads(i).Start()
        Next i
        ' очікування завершення потоків
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' affichage compteur
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' збільшує лічильник потоків
        ' зчитування лічильника
        Dim valeur As Integer = cptrThreads
        ' відстеження
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' очікування
        Thread.Sleep(1000)
        ' збільшення лічильника
        cptrThreads = valeur + 1
        ' відстеження
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
    End Sub
End Class

Ми не будемо зупинятися на частині, що стосується створення потоків, яку ми вже розглядали. Звернемо увагу на метод incrémente, який використовується кожним потоком для інкрементування статичного лічильника cptrThreads.

  1. лічильник зчитується
  2. потік зупиняється на 1 с. Таким чином, він втрачає доступ до процесора
  3. лічильник збільшується

Крок 2 існує лише для того, щоб змусити потік втратити доступ до процесора. Доступ до процесора буде передано іншому потоку. На практиці немає жодної гарантії, що потік не буде перерваний у проміжку часу між моментом зчитування значення лічильника та моментом його інкрементування. Існує ризик втрати доступу до процесора між моментом зчитування значення лічильника та моментом запису його значення, збільшеного на 1. Дійсно, операція збільшення буде складатися з декількох елементарних інструкцій на рівні процесора, які можуть бути перервані. Отже, крок 2 — пауза на одну секунду — призначений лише для того, щоб систематизувати цей ризик. Отримані результати такі:

dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1

З цих результатів добре видно, що відбувається:

  • перший потік зчитує лічильник. Він виявляє значення 0.
  • він зупиняється на 1 с, тобто звільняє процесор
  • потім другий потік отримує доступ до процесора і також зчитує значення лічильника. Воно все ще дорівнює 0, оскільки попередній потік ще не збільшив його. Він також зупиняється на 1 с.
  • За 1 с усі 5 потоків встигають пройти і зчитати значення 0.
  • Коли вони по черзі «прокинуться», вони збільшать значення 0, яке прочитали, і запишуть у лічильник значення 1, що підтверджує головна програма (Main).

У чому ж полягає проблема? Другий потік прочитав неправильне значення через те, що перший потік був перерваний до завершення своєї роботи, яка полягала в оновленні лічильника у вікні. Це підводить нас до поняття критичного ресурсу та критичної ділянки програми:

  • критичний ресурс — це ресурс, який одночасно може утримувати лише один потік. У даному випадку критичним ресурсом є лічильник.
  • Критична секція програми — це послідовність інструкцій у потоці виконання потоку, під час якої він отримує доступ до критичного ресурсу. Необхідно забезпечити, щоб під час цієї критичної секції тільки він мав доступ до ресурсу.

8.5. Виключний доступ до спільного ресурсу

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


         ' зчитування лічильника
        Dim valeur As Integer = cptrThreads
        ' очікування
        Thread.Sleep(1000)
        ' інкремент лічильника
        cptrThreads = valeur + 1

Щоб виконати цей код, потрібно гарантувати, що потік працює у відокремленому режимі. Його можна перервати, але під час цієї перерви інший потік не повинен мати можливості виконувати цей самий код. Платформа .NET пропонує кілька інструментів для забезпечення одночасного доступу до критичних ділянок коду. Ми будемо використовувати клас Mutex:

Image

Тут ми використовуватимемо лише такі конструктори та методи:

public Mutex()
створює об’єкт синхронізації M
public bool WaitOne()
Потік T1, який виконує операцію M.WaitOne(), запитує право володіння об’єктом синхронізації M. Якщо м’ютекс M не утримується жодним потоком (що має місце на початку), його «передають» потоку T1, який його запросив. Якщо трохи пізніше потік T2 виконає ту саму операцію, він буде заблокований. Адже м’ютекс може належати лише одному потоку. Блокування буде знято, коли потік T1 звільнить м’ютекс M, який він утримує. Таким чином, кілька потоків можуть бути заблоковані в очікуванні м’ютекса M.
public void
ReleaseMutex()
Пот T1, який виконує операцію M.ReleaseMutex(), відмовляється від володіння м'ютексом M.Lorsque; потік T1 втратить доступ до процесора, а система зможе передати його одному з потоків, що очікують на м'ютекс M. Лише один з них отримає його у свою чергу, а інші, що очікують на M, залишаться заблокованими

Мутекс M керує доступом до спільного ресурсу R. Потік запитує ресурс R за допомогою M.WaitOne() і повертає його за допомогою M.ReleaseMutex(). Критична ділянка коду, яка повинна виконуватися лише одним потоком одночасно, є спільним ресурсом. Синхронізацію виконання критичного фрагмента можна здійснити таким чином:

M.WaitOne()
' тільки цей потік може тут входити
' критична ділянка
....
M.ReleaseMutex()

де M — це об’єкт Mutex. Звісно, ніколи не слід забувати звільняти Mutex, що став непотрібним, щоб інший потік міг увійти в критичну секцію; інакше потоки, що очікують на м’ютекс, який ніколи не звільняється, ніколи не отримають доступу до процесора. Крім того, слід уникати ситуації взаємного блокування (deadlock), в якій два потоки очікують один на одного. Розглянемо такі дії, що відбуваються послідовно в часі:

  • потік T1 отримує володіння м'ютексом M1, щоб отримати доступ до спільного ресурсу R1
  • потік T2 отримує володіння м'ютексом M2 для доступу до спільного ресурсу R2
  • потік T1 запитує м'ютекс M2. Він заблокований.
  • Потік T2 запитує м'ютекс M1. Він заблокований.

У цьому випадку потоки T1 та T2 очікують один на одного. Така ситуація виникає, коли потоки потребують двох спільних ресурсів: ресурсу R1, що контролюється м'ютексом M1, та ресурсу R2, що контролюється м'ютексом M2. Одним із можливих рішень є одночасне запитування обох ресурсів за допомогою єдиного мьютекса M. Однак це не завжди можливо, якщо, наприклад, це призведе до тривалого блокування ресурсу, використання якого є ресурсоємним. Інший варіант полягає в тому, що потік, який володіє M1 і не може отримати M2, звільняє M1, щоб уникнути взаємної блокування. Якщо застосувати на практиці те, що ми щойно розглянули на попередньому прикладі, наш код виглядатиме так:


' опції
Option Explicit On 
Option Strict On

' використання потоків
Imports System
Imports System.Threading

Public Class thread4
    ' змінні класу
    Private Shared cptrThreads As Integer = 0    ' compteur de threads
    Private Shared autorisation As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' інструкція з експлуатації
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' перевірка кількості аргументів
        If args.Length <> 1 Then
            ' помилка
            Console.Error.WriteLine(syntaxe)
            ' зупинка
            Environment.Exit(1)
        End If
        ' перевірка якості аргументу
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
        End Try

        ' ініціалізація дозволу на доступ до критичного сегмента
        autorisation = New Mutex

        ' створення та генерація потоків
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' створення
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' присвоєння імені
            threads(i).Name = "tache_" & i
            ' запуск
            threads(i).Start()
        Next i
        ' очікування завершення потоків
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' виведення лічильника
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' збільшує лічильник потоків
        ' запит на дозвіл увійти в критичну секцію
        autorisation.WaitOne()
        ' зчитування лічильника
        Dim valeur As Integer = cptrThreads
        ' відстеження
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' очікування
        Thread.Sleep(1000)
        ' збільшення лічильника
        cptrThreads = valeur + 1
        ' моніторинг
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' надання дозволу на доступ
        autorisation.ReleaseMutex()
    End Sub
End Class

Отримані результати відповідають очікуваним:

dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5

8.6. Синхронізація за подіями

Розглянемо таку ситуацію, яку іноді називають ситуацією «виробників-споживачів».

  1. Ми маємо масив, у який одні процеси записують дані (виробники), а інші — зчитують їх (споживачі).
  2. Виробники є рівноправними між собою, але взаємовиключними: одночасно лише один виробник може записувати свої дані в масив.
  3. Споживачі між собою рівноправні, але взаємовиключні: одночасно лише один споживач може зчитувати дані, розміщені в масиві.
  4. Споживач може зчитувати дані з таблиці лише тоді, коли виробник записав їх туди, а виробник може записувати нові дані в таблицю лише тоді, коли ті, що там є, вже використані.

У цьому викладі можна виділити два спільні ресурси:

    1. таблицю для запису
    2. масив для читання

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

Використовувані події будуть частиною класу AutoResetEvent:

Image

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

while(peutEcrire==false)        ' attente active

або

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

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

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

Операція

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

ініціалізує булеве значення peutEcrire до false. Операція

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

, що виконується потоком, призводить до того, що цей потік переходить далі, якщо булеве значення peutEcrire є істинним, або ж блокується доти, доки воно не стане істинним. Інший потік встановить для нього значення «true» за допомогою операції peutEcrire.Set() або «false» за допомогою операції peutEcrire.Reset().

Програма «виробник-споживач» виглядає так:


' використання потоків читання та запису
' ілюструє одночасне використання спільних ресурсів та синхронізації

' параметри
Option Explicit On 
Option Strict On

' використання потоків
Imports System
Imports System.Threading

Public Class lececr

    ' змінні класу
    Private Shared data(5) As Integer    ' ressource partagée entre threads lecteur et threads écrivain
    Private Shared lecteur As Mutex    ' variable de synchronisation pour lire le tableau
    Private Shared écrivain As Mutex    ' variable de synchronisation pour écrire dans le tableau
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' un générateur de nombres aléatoires
    Private Shared peutLire As AutoResetEvent    ' signale qu'on peut lire le contenu de data
    Private Shared peutEcrire As AutoResetEvent

    Public Shared Sub Main(ByVal args() As [String])

        ' кількість потоків, що генеруються
        Const nbThreads As Integer = 3

        ' ініціалізація прапорців
        peutLire = New AutoResetEvent(False)        ' on ne peut pas encore lire
        peutEcrire = New AutoResetEvent(True)        ' on peut déjà écrire

        ' ініціалізація змінних синхронізації
        lecteur = New Mutex         ' synchronise les lecteurs
        écrivain = New Mutex         ' synchronise les écrivains

        ' створення потоків-читачів
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' створення
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' запуск
            lecteurs(i).Start()
        Next i

        ' створення потоків запису
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' створення
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' запуск
            écrivains(i).Start()
        Next i

        'завершення обробки
        Console.Out.WriteLine("fin de Main...")
    End Sub

    ' читання вмісту масиву
    Public Shared Sub lire()
        ' критичний розділ
        lecteur.WaitOne()        ' un seul lecteur peut passer
        peutLire.WaitOne()        ' on doit pouvoir lire

        ' читання таблиці
        Dim i As Integer
        For i = 0 To data.Length - 1
            'очікування 1 с
            Thread.Sleep(1000)
            ' відображення
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' неможливо читати
        peutLire.Reset()
        ' можна записувати
        peutEcrire.Set()
        ' кінець критичного сегмента
        lecteur.ReleaseMutex()
    End Sub

    ' запис у таблицю
    Public Shared Sub écrire()
        ' критичний розділ
        ' може пройти лише один письменник
        écrivain.WaitOne()
        ' треба чекати дозволу на запис
        peutEcrire.WaitOne()

        ' запис у таблицю
        Dim i As Integer
        For i = 0 To data.Length - 1
            'очікування 1 с
            Thread.Sleep(1000)
            ' виведення на екран
            data(i) = objRandom.Next(0, 1000)
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
        Next i

        ' запис більше неможливий
        peutEcrire.Reset()
        ' можна читати
        peutLire.Set()
        'кінець критичного розділу
        écrivain.ReleaseMutex()
    End Sub
End Class

Виконання дає такі результати:

dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C

Можна відзначити наступне:

  • одночасно працює лише один зчитувач, хоча він і втрачає доступ до процесора в критичній секції lire
  • одночасно працює лише один записувач, хоча він і втрачає доступ до процесора в критичній секції écrire
  • читач зчитує дані лише тоді, коли в масиві є що зчитувати
  • записувач записує лише тоді, коли масив повністю прочитано