Skip to content

8. 执行线程

8.1. 简介

当应用程序启动时,它会在称为线程的执行流中运行。在 .NET 中,表示线程的类是 System.Threading.Thread 类,其定义如下:

Image

我们将仅使用该类中的部分属性和方法:

CurrentThread - 静态属性
返回当前正在运行的线程
Name - 对象属性
线程名称
isAlive - 对象属性
表示线程是否处于活动状态(true)或非活动状态(false)
Start - 对象方法
启动线程的执行
中止 - 对象方法
永久停止线程的执行
Sleep(n) - 静态方法
使线程暂停 n 毫秒
Suspend() - 对象方法
暂时暂停线程的执行
Resume() - 对象方法
恢复已暂停线程的执行
Join() - 对象方法
阻塞操作 - 等待线程完成后才继续执行下一条指令

让我们看一个简单的应用程序,它演示了主执行线程的存在,即类中的 Main 函数运行的那个线程:


' use of threads
Imports System
Imports System.Threading
 
Public Module thread1
    Public Sub Main()
        ' init current thread
        Dim main As Thread = Thread.CurrentThread
        ' display
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' we change the name
        main.Name = "main"
        ' check
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' infinite loop
        While True
            ' display
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' temporary shutdown
            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. 创建执行线程

在某些应用程序中,代码片段可能在不同的执行线程中“同时”运行。当我们说线程同时运行时,通常只是泛泛而谈。如果机器只有一个处理器(这种情况至今仍很常见),则线程会共享该处理器:它们轮流在短暂的一瞬间(几毫秒)访问处理器。这正是产生并行执行错觉的原因。 分配给线程的时间长短取决于多种因素,包括其优先级——该优先级有默认值,但也可通过编程设置。当线程获得处理器时,通常会使用其分配的全部时间。不过,它也可以提前释放处理器:

  • 通过等待某个事件(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() 启动:此时,传递给 T 构造函数的 [run] 函数将由线程 T 执行。执行 T.Start() 语句的程序不会等待任务 T 完成:它会立即继续执行下一条语句。现在,我们有两个任务正在并行运行。它们通常需要相互通信,以了解待处理的共享工作的状态。这就是线程同步的问题。
  1. 线程一旦启动,便会自主运行。当其正在执行的启动函数完成工作后,线程即停止运行。
  1. 我们可以向任务 T 发送特定信号:
    1. T.Suspend() 命令其暂时暂停
    2. T.Resume() 指示其恢复工作
    3. T.Abort() 指示其永久停止
  1. 您还可以使用 T.join() 等待其执行完成。这是一条阻塞指令:执行该指令的程序将被阻塞,直到任务 T 完成工作。这是一种同步手段。

让我们来分析以下程序:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports System.Threading
 
Module thread2
    Public Sub Main()
        ' init Current thread
        Dim main As Thread = Thread.CurrentThread
        ' name the Thread
        main.Name = "main"
 
        ' creation of execution threads
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' create thread i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' set the thread name
            tâches(i).Name = "tache_" & i
            ' start execution of thread i
            tâches(i).Start()
        Next i
        ' end of hand
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub
 
    Public Sub affiche()
        ' display start of execution
        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")))
        ' sleep for 1 s
        Thread.Sleep(1000)
        ' display end of run
        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 个线程,负责执行静态方法 display。结果如下:

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个线程的执行,并在它们完成执行之前就结束了自身。该
            ' on lance l'exécution du thread i
            tâches(i).Start()

会启动线程 tasks[i] 的执行,但一旦启动,程序会立即继续执行下一条语句,而不会等待该线程完成。

  • 所有创建的线程都必须执行 display 方法。执行顺序是不可预测的。尽管在示例中,执行顺序似乎遵循了执行请求的顺序,但无法由此得出普遍结论。此处的操作系统拥有 6 个线程和一个处理器。它将根据自身的规则将处理器分配给这 6 个线程。
  • 结果显示了 Sleep 方法的影响。在示例中,线程 0 最先执行 display 方法。它先显示“开始执行”消息,随后执行 Sleep 方法,该方法使其暂停 1 秒。随后它失去处理器,处理器被释放给另一个线程。 示例表明线程 1 将获得处理器。线程 1 将遵循相同的执行路径,其他线程亦是如此。当线程 0 的 1 秒睡眠期结束时,其执行即可恢复。系统将处理器分配给它,它便能完成 display 方法的执行。

让我们修改程序,在 Main 方法结尾添加以下指令:

        ' fin de main
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

运行新程序后输出:

fin du thread main

Main 函数创建的线程并未被执行。这是因为该语句

        Environment.Exit(0)

语句实现了这一效果:它终止了应用程序中的所有线程,而不仅仅是主线程。解决此问题的办法是让 Main 方法在终止自身之前,等待其创建的线程执行完毕。这可以通过使用 Thread 类的 Join 方法来实现:


        ' on attend la fin d'exécution de tous les threads
        For i = 0 To tâches.Length - 1
            ' attente de la fin d'exécution du thread i
            tâches(i).Join()
        Next i        'for
        ' fin de main
        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 上客户端的请求。

我们每天都在使用遵循这种模式的互联网应用程序:Web 服务、电子邮件、论坛浏览、文件传输……在上图中,服务器 S1 必须同时为客户端 C1、C2、……、Cn 提供服务。以向客户端传输文件的 FTP(文件传输协议)服务器为例,我们知道文件传输有时可能需要数小时。 当然,单个客户端独占服务器如此长时间是绝无可能的。通常的做法是,服务器创建与客户端数量相等的执行线程。每个线程随后负责处理特定的客户端。由于处理器在机器上的所有活动线程之间循环共享,服务器在每个客户端上花费的时间都很短,从而确保了服务的并发性。

8.4. 共享资源的访问

在上述客户端-服务器示例中,每个线程基本上独立地为一个客户端提供服务。尽管如此,线程可能需要协作才能向其客户端提供所请求的服务,特别是在访问共享资源时。上图类似于大型政府机构(如邮局)的柜台,每个柜台的职员为一名客户服务。假设这些职员不时需要复印客户带来的文件,而复印机只有一台。 两个工作人员无法同时使用复印机。如果工作人员 i 发现复印机正被工作人员 j 使用,则必须等待。这种情况被称为访问共享资源,在计算机科学中,其管理相当棘手。考虑以下示例:

  • 一个应用程序将生成 n 个线程,其中 n 作为参数传入
  • 共享资源是一个计数器,每个生成的线程都必须将其递增
  • 在应用程序结束时,将显示计数器的值。因此,我们应得到 n。

程序代码如下:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread3
    ' class variables
    Private Shared cptrThreads As Integer = 0
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' error
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' end
            Environment.Exit(2)
        End Try
        ' thread creation and generation
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        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

我们不再赘述线程创建部分,因为我们已经讲过了。相反,让我们关注 Increment 方法,每个线程都使用该方法来递增静态计数器 cptrThreads

  1. 计数器被读取
  2. 线程暂停 1 秒。因此它会失去 CPU 控制权
  3. 计数器被递增

步骤 2 仅用于强制线程让出处理器。处理器将被分配给另一个线程。实际上,无法保证线程在读取计数器值与将其递增之间不会被中断。 在读取计数器值与写入加1后的值之间,存在失去CPU控制的风险。事实上,递增操作在处理器层面涉及多个基本指令,这些指令可能被中断。因此,步骤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 秒,从而将 CPU 让出
  • 随后第二个线程获得 CPU 控制权,并读取计数器值。由于前一个线程尚未将其递增,该值仍为 0。该线程也暂停 1 秒。
  • 在1秒内,所有5个线程都有时间运行并读取到值0。
  • 当它们依次唤醒时,会将读取到的0加1,并将1写入计数器,这一结果由主程序(Main)确认。

问题出在哪里?第二个线程读取了错误的值,因为第一个线程在完成其任务(即更新窗口中的计数器)之前就被中断了。这引出了程序中关键资源和关键区段的概念:

  • 关键资源是指一次只能由一个线程持有的资源。在此,关键资源即为计数器。
  • 程序中的临界区是指线程执行流中访问临界资源的一段指令序列。我们必须确保在此临界区期间,只有该线程能够访问该资源。

8.5. 对共享资源的独占访问

在我们的示例中,临界区是位于读取计数器与写入其新值之间的代码段:


        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1

要执行这段代码,必须确保线程处于独占状态。该线程可能会被中断,但在中断期间,其他任何线程都不得执行这段相同的代码。 .NET 平台提供了多种工具来确保代码的关键部分仅由单个线程进入。我们将使用 Mutex 类:

Image

在此,我们将仅使用以下构造函数和方法:

public Mutex()
创建一个同步对象 M
public bool WaitOne()
执行 M.WaitOne() 操作的线程 T1 请求对同步对象 M 的所有权。如果互斥锁 M 未被任何线程持有(初始情况),则将其“授予”请求它的线程 T1。 如果稍后线程 T2 执行相同的操作,它将被阻塞。实际上,一个互斥锁只能属于一个线程。当线程 T1 释放其持有的互斥锁 M 时,该互斥锁将被释放。因此,多个线程在等待互斥锁 M 时可能会被阻塞。
public void
ReleaseMutex()
执行 M.ReleaseMutex() 操作的线程 T1 会放弃对互斥锁 M 的持有权。当线程 T1 失去处理器时,系统可以将其分配给正在等待互斥锁 M 的某个线程。只有一个线程会依次获得它;其余等待 M 的线程仍处于阻塞状态

互斥锁 M 管理对共享资源 R 的访问。线程通过 M.WaitOne() 请求资源 R,并通过 M.ReleaseMutex() 释放它。必须由单个线程在特定时间点执行的代码关键段即为共享资源。关键段执行的同步可通过以下方式实现:

M.WaitOne()
' le thread est seul à entrer ici
' section critique
....
M.ReleaseMutex()

其中 M 是一个互斥锁对象。当然,绝不能忘记释放不再需要的互斥锁,以便其他线程能够进入临界区;否则,等待一个永远不会被释放的互斥锁的线程将永远无法获得处理器访问权限。此外,必须避免出现两个线程相互等待的死锁情况。考虑以下按顺序发生的操作:

  • 线程 T1 获取互斥锁 M1 的控制权以访问共享资源 R1
  • 线程 T2 获取互斥锁 M2 以访问共享资源 R2
  • 线程 T1 请求互斥锁 M2,被阻塞
  • 线程 T2 请求互斥锁 M1,被阻塞。

在此,线程 T1 和 T2 处于相互等待的状态。当线程需要两个共享资源时,就会出现这种情况:一个是受互斥锁 M1 控制的资源 R1,另一个是受互斥锁 M2 控制的资源 R2。一种可能的解决方案是使用单个互斥锁 M 同时获取这两个资源。然而,如果这会导致对一个昂贵资源的长时间锁定,那么这种方法并不总是可行的。 另一种解决方案是:持有 M1 但无法获取 M2 的线程应释放 M1 以避免死锁。若将上述内容应用到前面的示例中,我们的应用程序将变为如下形式:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread4
    ' class variables
    Private Shared cptrThreads As Integer = 0    ' thread counter
    Private Shared autorisation As Mutex
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        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
 
        ' initialize access authorization to a critical section
        autorisation = New Mutex
 
        ' thread creation and generation
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' we request permission to enter the critical secton
        autorisation.WaitOne()
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' access authorization is returned
        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

此类事件类似于布尔值,但避免了主动或半主动等待。因此,如果写入访问由布尔值 *canWrite* 控制,生产者在写入前将执行如下代码:

while(peutEcrire==false)        ' attente active

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

在第一种方法中,线程不必要地占用了处理器资源。在第二种方法中,它每隔 100 毫秒检查一次 canWrite 布尔变量的状态。AutoResetEvent 类可以进一步优化:当线程等待的事件发生时,它会请求被唤醒:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' thread waits for peutEcrire event to change to true

该操作

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

canWrite 布尔值初始化为 false。该操作

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

由一个线程执行时,如果布尔值 canWrite* 为 true,则该线程继续执行;否则,该线程将被阻塞,直到该值变为 true。另一个线程将使用 canWrite.Set() 操作将其设置为 true,或使用 canWrite.Reset()* 操作将其设置为 false。

生产者-消费者程序如下:


' use of reader and writer threads
' illustrates the simultaneous use of shared resources and synchronization
 
' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class lececr
 
    ' class variables
    Private Shared data(5) As Integer    ' resource shared between reader and writer threads
    Private Shared lecteur As Mutex    ' synchronization variable to read the table
    Private Shared écrivain As Mutex    ' synchronization variable to write to the table
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' a random number generator
    Private Shared peutLire As AutoResetEvent    ' indicates that you can read the contents of data
    Private Shared peutEcrire As AutoResetEvent
 
    Public Shared Sub Main(ByVal args() As [String])
 
        ' number of threads to generate
        Const nbThreads As Integer = 3
 
        ' flag initialization
        peutLire = New AutoResetEvent(False)        ' cannot be read yet
        peutEcrire = New AutoResetEvent(True)        ' we can already write
 
        ' initialization of synchronization variables
        lecteur = New Mutex         ' synchronizes drives
        écrivain = New Mutex         ' synchronizes writers
 
        ' creation of reader threads
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' launch
            lecteurs(i).Start()
        Next i
 
        ' creating writer threads
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' creation
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' launch
            écrivains(i).Start()
        Next i
 
        'end of hand
        Console.Out.WriteLine("fin de Main...")
    End Sub
 
    ' read the contents of the table
    Public Shared Sub lire()
        ' review section
        lecteur.WaitOne()        ' a single reader can pass
        peutLire.WaitOne()        ' you must be able to read
 
        ' table reading
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i
 
        ' we can no longer read
        peutLire.Reset()
        ' we can write
        peutEcrire.Set()
        ' end of critical section
        lecteur.ReleaseMutex()
    End Sub
 
    ' write in the table
    Public Shared Sub écrire()
        ' review section
        ' only one writer can pass
        écrivain.WaitOne()
        ' we have to wait for write authorization
        peutEcrire.WaitOne()
 
        ' writing table
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            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
 
        ' we can no longer write
        peutEcrire.Reset()
        ' you can read
        peutLire.Set()
        'end of critical section
        é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

可以注意到以下几点:

  • 尽管在读取关键区会失去CPU,但确实每次只有一个读取者
  • 确实每次只有一个写入者,尽管它在写入临界区会失去CPU
  • 读取者仅在表中存在可读取内容时才进行读取
  • 写入者仅在数组已完全读取完毕时才进行写入