Skip to content

8. Execution threads

8.1. Introduction

When an application is launched, it runs in an execution flow called a thread. The .NET class that models a thread is the System.Threading.Thread class and has the following definition:

Image

We will use only some of the properties and methods of this class:

CurrentThread - static property
returns the thread currently running
Name - object property
thread name
isAlive - object property
indicates whether the thread is active (true) or not (false)
Start - object method
starts the execution of a thread
Abort - object method
permanently stops a thread's execution
Sleep(n) - static method
pauses a thread for n milliseconds
Suspend() - object method
temporarily suspends the execution of a thread
Resume() - object method
resumes the execution of a suspended thread
Join() - object method
blocking operation - waits for the thread to finish before proceeding to the next instruction

Let’s look at a simple application that demonstrates the existence of a main execution thread, the one in which a class’s Main function runs:


' Using threads
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' Initialize current thread
        Dim main As Thread = Thread.CurrentThread
        ' display
        Console.Out.WriteLine(("Current thread: " + main.Name))
        ' change the name
        main.Name = "main"
        ' verification
        Console.Out.WriteLine(("Current thread: " + main.Name))
        'infinite loop
        While True
            ' display
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' temporary pause
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Screen results:

dos>thread1
Current thread:
Current thread: main
main: 06:13:55
main: 06:13:56
main: 06:13:57
main: 06:13:58
main: 06:13:59

The previous example illustrates the following points:

  • the Main function runs in a thread
  • we can access the properties of this thread via Thread.CurrentThread
  • the role of the Sleep method. Here, the thread executing Main sleeps for 1 second between each display.

8.2. Creating execution threads

It is possible to have applications where pieces of code execute "simultaneously" in different execution threads. When we say that threads run simultaneously, we are often using the term loosely. If the machine has only one processor, as is still often the case, the threads share this processor: they each have access to it, in turn, for a brief moment (a few milliseconds). This is what creates the illusion of parallel execution. The amount of time allocated to a thread depends on various factors, including its priority, which has a default value but can also be set programmatically. When a thread has the processor, it normally uses it for the entire time allotted to it. However, it can release it early:

  • by waiting for an event (wait, join, suspend)
  • by sleeping for a specified period (sleep)
  1. A thread T is first created by its constructor
Public Sub New(ByVal start As ThreadStart)

ThreadStart is of type delegate and defines the prototype of a function with no parameters:

Public Delegate Sub ThreadStart()

A typical construction is as follows:

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

The run function passed as a parameter will be executed when the thread is launched.

  1. The execution of thread T is initiated by T.Start(): the [run] function passed to the constructor of T will then be executed by thread T. The program executing the T.Start() statement does not wait for task T to finish: it immediately proceeds to the next statement. We now have two tasks running in parallel. They often need to communicate with each other to know the status of the shared work to be done. This is the problem of thread synchronization.
  1. Once launched, the thread runs autonomously. It will stop when the start function it is executing has finished its work.
  1. We can send certain signals to task T:
    1. T.Suspend() tells it to pause temporarily
    2. T.Resume() tells it to resume its work
    3. T.Abort() tells it to stop permanently
  1. You can also wait for it to finish executing using T.join(). This is a blocking instruction: the program executing it is blocked until task T has finished its work. It is a means of synchronization.

Let’s examine the following program:


' options
Option Strict On
Option Explicit On 

' namespaces
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' Initialize current thread
        Dim main As Thread = Thread.CurrentThread
        ' Set a name for the thread
        main.Name = "main"

        Creating execution threads
        Dim tasks(4) As Thread
        Dim i As Integer
        For i = 0 To tasks.Length - 1
            ' Create thread i
            tasks(i) = New Thread(New ThreadStart(AddressOf display))
            ' Set the thread name
            tasks(i).Name = "task_" & i
            ' Start thread i
            tasks(i).Start()
        Next i
        ' end of main
        Console.Out.WriteLine(("End of thread " + main.Name))
    End Sub

    Public Sub display()
        ' Display start of execution
        Console.Out.WriteLine(("Start of execution of the display method in thread " + Thread.CurrentThread.Name + ": " + DateTime.Now.ToString("hh:mm:ss")))
        ' Sleep for 1 second
        Thread.Sleep(1000)
        ' Display end of execution
        Console.Out.WriteLine(("End of method execution displayed in thread " + Thread.CurrentThread.Name + ": " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

The main thread, the one that executes the Main function, creates 5 other threads responsible for executing the static method display. The results are as follows:

dos>thread2
End of main thread
Start of execution of the display method in the task_0 thread: 05:27:53
Start of execution of the affiche method in the tache_1 thread: 05:27:53
Start of execution of the affiche method in the tache_2 thread: 05:27:53
Start of method execution in thread task_3: 05:27:53
Start of method execution displayed in thread task_4: 05:27:53
End of method execution displayed in thread task_0: 05:27:54
End of method execution displayed in thread task_1: 05:27:54
End of method execution displayed in thread task_2: 05:27:54
End of method execution displayed in thread task_3: 05:27:54
End of method execution displayed in the tache_4 thread: 05:27:54

These results are very informative:

  • First, we see that starting a thread’s execution is not blocking. The Main method started the execution of 5 threads in parallel and finished executing before them. The
            ' we start the execution of thread i
            tasks(i).Start()

starts the execution of the thread tasks[i], but once this is done, execution immediately continues with the next statement without waiting for the thread to finish.

  • All created threads must execute the display method. The order of execution is unpredictable. Even though in the example, the order of execution appears to follow the order of execution requests, no general conclusions can be drawn from this. The operating system here has 6 threads and one processor. It will allocate the processor to these 6 threads according to its own rules.
  • The results show an effect of the Sleep method. In the example, thread 0 is the first to execute the display method. The start-of-execution message is displayed, then it executes the Sleep method, which suspends it for 1 second. It then loses the processor, which becomes available to another thread. The example shows that thread 1 will obtain it. Thread 1 will follow the same path, as will the other threads. When the 1-second sleep period for thread 0 ends, its execution can resume. The system grants it the processor, and it can complete the execution of the display method.

Let’s modify our program to end the Main method with the following instructions:

        ' end of main
        Console.Out.WriteLine(("end of thread " + main.Name))
        Environment.Exit(0)

Running the new program yields:

end of the main thread

The threads created by the Main function are not executed. It is the statement

        Environment.Exit(0)

that does this: it terminates all threads in the application, not just the Main thread. The solution to this problem is for the Main method to wait for the threads it created to finish executing before terminating itself. This can be done using the Join method of the Thread class:


        ' wait for all threads to finish executing
        For i = 0 To tasks.Length - 1
            ' wait for thread i to finish executing
            tasks(i).Join()
        Next i        'for
        ' end of main
        Console.Out.WriteLine(("End of thread " + main.Name))
        Environment.Exit(0)

This produces the following results:

Start of method execution displayed in the tache_1 thread: 05:34:48
Start of method execution displayed in the tache_2 thread: 05:34:48
Start of method execution displayed in the tache_3 thread: 05:34:48
Start of method execution displayed in the tache_4 thread: 05:34:48
Start of method execution displayed in thread task_0: 05:34:48
End of method execution displayed in thread task_2: 05:34:50
End of method execution displayed in the tache_1 thread: 05:34:50
End of method execution displayed in thread task_3: 05:34:50
End of method execution displayed in thread task_0: 05:34:50
End of method execution displayed in the tache_4 thread: 05:34:50
End of the main thread

8.3. Benefits of threads

Now that we have highlighted the existence of a default thread—the one that executes the Main method—and we know how to create others, let’s consider the benefits of threads for us and why we are presenting them here. There is a type of application that lends itself well to the use of threads: client-server applications on the Internet. In such an application, a server located on machine S1 responds to requests from clients located on remote machines C1, C2, ..., Cn.

We use Internet applications that follow this pattern every day: web services, email, forum browsing, file transfers... In the diagram above, server S1 must serve clients C1, C2, ..., Cn simultaneously. If we take the example of an FTP (File Transfer Protocol) server that delivers files to its clients, we know that a file transfer can sometimes take several hours. It is, of course, out of the question for a single client to monopolize the server for such a long period. What is usually done is that the server creates as many execution threads as there are clients. Each thread is then responsible for handling a specific client. Since the processor is cyclically shared among all active threads on the machine, the server spends a little time with each client, thereby ensuring the concurrency of the service.

8.4. Access to Shared Resources

In the client-server example mentioned above, each thread serves a client largely independently. Nevertheless, threads may need to cooperate to provide the requested service to their client, particularly when accessing shared resources. The diagram above resembles the counters of a large government office, such as a post office, where an agent at each counter serves a customer. Suppose that from time to time these agents need to make photocopies of documents brought in by their customers and that there is only one photocopier. Two agents cannot use the photocopier at the same time. If agent i finds the photocopier in use by agent j, they will have to wait. This situation is called access to a shared resource, and in computer science, it is quite tricky to manage. Consider the following example:

  • an application will generate n threads, where n is passed as a parameter
  • the shared resource is a counter that must be incremented by each generated thread
  • at the end of the application, the counter’s value is displayed. We should therefore get n.

The program is as follows:


' 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])
        ' usage instructions
        Const syntax As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' Check number of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntax)
            ' exit
            Environment.Exit(1)
        End If
        ' Check argument quality
        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("Invalid number of threads (between 1 and " & nbMaxThreads & ")")
            ' end
            Environment.Exit(2)
        End Try
        ' Create and spawn threads
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf increment))
            ' naming
            threads(i).Name = "task_" & i
            ' launch
            threads(i).Start()
        Next i
        ' wait for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' display counter
        Console.Out.WriteLine(("Number of threads generated: " & cptrThreads))
    End Sub

    Public Shared Sub increment()
        ' Increases the thread counter
        ' read counter
        Dim value As Integer = cptrThreads
        ' followed by
        Console.Out.WriteLine(("At " + DateTime.Now.ToString("hh:mm:ss") & ", thread " & Thread.CurrentThread.Name & " read the counter value: " & cptrThreads))
        ' wait
        Thread.Sleep(1000)
        ' increment counter
        cptrThreads = value + 1
        ' monitoring
        Console.Out.WriteLine(("At " & DateTime.Now.ToString("hh:mm:ss") & ", thread " & Thread.CurrentThread.Name & " wrote the counter value: " & cptrThreads))
    End Sub
End Class

We won’t dwell on the thread creation part, which we’ve already covered. Instead, let’s focus on the Increment method, used by each thread to increment the static counter cptrThreads.

  1. The counter is read
  2. the thread pauses for 1 second. It therefore loses the CPU
  3. the counter is incremented

Step 2 is only there to force the thread to lose the processor. The processor will be given to another thread. In practice, there is no guarantee that a thread will not be interrupted between the moment it reads the counter and the moment it increments it. There is a risk of losing the CPU between the moment the counter value is read and the moment its value, incremented by 1, is written. Indeed, the increment operation will involve several basic instructions at the processor level that can be interrupted. Step 2, the one-second sleep, is therefore only there to account for this risk. The results obtained are as follows:

dos>thread3 5
At 05:44:34, the tache_0 thread read the counter value: 0
At 05:44:34, the tache_1 thread read the counter value: 0
At 05:44:34, the tache_2 thread read the counter value: 0
At 05:44:34, the tache_3 thread read the counter value: 0
At 05:44:34, the tache_4 thread read the counter value: 0
At 05:44:35, the tache_0 thread wrote the counter value: 1
At 05:44:35, the tache_1 thread wrote the counter value: 1
At 05:44:35, the tache_2 thread wrote the counter value: 1
At 05:44:35, the tache_3 thread wrote the counter value: 1
At 05:44:35, the tache_4 thread wrote the counter value: 1
Number of threads generated: 1

Looking at these results, it’s clear what’s happening:

  • A first thread reads the counter. It finds 0.
  • It pauses for 1 second, thus yielding the CPU
  • A second thread then takes the CPU and also reads the counter value. It is still 0 since the previous thread has not yet incremented it. It also pauses for 1 second.
  • In 1 second, all 5 threads have time to run and read the value 0.
  • When they wake up one after another, they will increment the value 0 they read and write the value 1 to the counter, which is confirmed by the main program (Main).

Where does the problem come from? The second thread read an incorrect value because the first thread was interrupted before it finished its task, which was to update the counter in the window. This brings us to the concept of critical resources and critical sections in a program:

  • A critical resource is a resource that can be held by only one thread at a time. Here, the critical resource is the counter.
  • A critical section of a program is a sequence of instructions in a thread’s execution flow during which it accesses a critical resource. We must ensure that during this critical section, it is the only one with access to the resource.

8.5. Exclusive access to a shared resource

In our example, the critical section is the code located between reading the counter and writing its new value:


        ' read counter
        Dim value As Integer = cptrThreads
        ' wait
        Thread.Sleep(1000)
        ' increment counter
        cptrThreads = value + 1

To execute this code, a thread must be guaranteed to be alone. It may be interrupted, but during that interruption, no other thread must be able to execute this same code. The .NET platform offers several tools to ensure single-threaded entry into critical sections of code. We will use the Mutex class:

Image

Here, we will use only the following constructors and methods:

public Mutex()
creates a synchronization object M
public bool WaitOne()
Thread T1, which executes the M.WaitOne() operation, requests ownership of the synchronization object M. If the Mutex M is not held by any thread (the initial case), it is "given" to thread T1, which requested it. If, a little later, thread T2 performs the same operation, it will be blocked. In fact, a mutex can belong to only one thread. It will be released when thread T1 releases the mutex M it holds. Several threads can thus be blocked while waiting for the mutex M.
public void
ReleaseMutex()
The thread T1 that performs the operation M.ReleaseMutex() relinquishes ownership of the mutex M. When thread T1 loses the processor, the system can assign it to one of the threads waiting for the mutex M. Only one will obtain it in turn; the others waiting for M remain blocked

A Mutex M manages access to a shared resource R. A thread requests resource R via M.WaitOne() and releases it via M.ReleaseMutex(). A critical section of code that must be executed by only one thread at a time is a shared resource. Synchronization of the critical section’s execution can be achieved as follows:

M.WaitOne()
' Only the thread enters here
' critical section
....
M.ReleaseMutex()

where M is a Mutex object. Of course, you must never forget to release a Mutex that is no longer needed so that another thread can enter the critical section; otherwise, threads waiting for a Mutex that is never released will never gain access to the processor. Furthermore, one must avoid a deadlock situation in which two threads wait for each other. Consider the following actions occurring in sequence:

  • a thread T1 acquires ownership of a Mutex M1 to access a shared resource R1
  • a thread T2 acquires a Mutex M2 to access a shared resource R2
  • Thread T1 requests Mutex M2. It is blocked.
  • Thread T2 requests Mutex M1. It is blocked.

Here, threads T1 and T2 are waiting for each other. This situation occurs when threads require two shared resources: resource R1 controlled by Mutex M1 and resource R2 controlled by Mutex M2. One possible solution is to acquire both resources simultaneously using a single Mutex M. However, this is not always feasible if, for example, it results in a prolonged lock on a costly resource. Another solution is for a thread holding M1 that cannot obtain M2 to release M1 to avoid deadlock. If we apply what we just saw to the previous example, our application becomes as follows:


' 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 permission As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' usage instructions
        Const syntax As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' Check number of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntax)
            ' exit
            Environment.Exit(1)
        End If
        ' Check argument quality
        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 permission to a critical section
        authorization = New Mutex

        ' creation and generation of threads
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf increment))
            ' naming
            threads(i).Name = "task_" & i
            ' launch
            threads(i).Start()
        Next i
        ' wait for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' display counter
        Console.Out.WriteLine(("Number of threads generated: " & cptrThreads))
    End Sub

    Public Shared Sub increment()
        ' Increases the thread counter
        ' request permission to enter the critical section
        authorization.WaitOne()
        ' read counter
        Dim value As Integer = cptrThreads
        ' monitoring
        Console.Out.WriteLine(("At " & DateTime.Now.ToString("hh:mm:ss") & ", thread " & Thread.CurrentThread.Name & " read the counter value: " & cptrThreads))
        ' wait
        Thread.Sleep(1000)
        ' increment counter
        cptrThreads = value + 1
        ' monitoring
        Console.Out.WriteLine(("At " & DateTime.Now.ToString("hh:mm:ss") & ", thread " & Thread.CurrentThread.Name & " wrote the counter value: " & cptrThreads))
        ' release the access permission
        authorization.ReleaseMutex()
    End Sub
End Class

The results obtained are as expected:

dos>thread4 5
At 05:51:10, the thread tache_0 read the counter value: 0
At 05:51:11, the tache_0 thread wrote the counter value: 1
At 05:51:11, the tache_1 thread read the counter value: 1
At 05:51:12, the tache_1 thread wrote the counter value: 2
At 05:51:12, the tache_2 thread read the counter value: 2
At 05:51:13, the tache_2 thread wrote the counter value: 3
At 05:51:13, the tache_3 thread read the counter value: 3
At 05:51:14, the thread task_3 wrote the counter value: 4
At 05:51:14, the tache_4 thread read the counter value: 4
At 05:51:15, the tache_4 thread wrote the counter value: 5
Number of threads generated: 5

8.6. Event-based synchronization

Consider the following situation, sometimes referred to as a producer-consumer scenario.

  1. We have an array into which some processes deposit data (the producers) and others read it (the consumers).
  2. The producers are equal to one another but exclusive: only one producer at a time can deposit data into the array.
  3. The consumers are equal to one another but mutually exclusive: only one reader at a time can read the data stored in the array.
  4. A consumer can only read data from the table once a producer has written it there, and a producer can only write new data to the table once the existing data has been consumed.

In this explanation, we can distinguish two shared resources:

    1. the writable table
    2. the read-only array

Access to these two shared resources can be controlled by mutexes, as seen previously, one for each resource. Once a consumer has obtained the read-only array, it must verify that there is indeed data in it. An event will be used to notify it of this. Similarly, a producer that has obtained the write-only array must wait until a consumer has emptied it. An event will be used here as well.

The events used will be of the AutoResetEvent class:

Image

This type of event is analogous to a boolean but avoids active or semi-active waits. Thus, if write access is controlled by a boolean *canWrite*, a producer will execute code like the following before writing:

while(canWrite == false)        ' active wait

or

while(canWrite == false) ' semi-active wait
    Thread.Sleep(100)                ' wait for 100 ms
end while

In the first method, the thread unnecessarily ties up the processor. In the second, it checks the state of the canWrite boolean every 100 ms. The AutoResetEvent class allows for further improvement: the thread will request to be woken up when the event it is waiting for occurs:

AutoEvent canWrite = new AutoResetEvent(false)        ' canWrite = false;
....
canWrite.WaitOne() ' the thread waits for the canWrite event to become true

The operation

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

initializes the canWrite boolean to false. The operation

canWrite.WaitOne() ' the thread waits for the canWrite event to become true

executed by a thread causes that thread to proceed if the boolean canWrite* is true; otherwise, it is blocked until it becomes true. Another thread will set it to true using the canWrite.Set() operation or to false using the canWrite.Reset()* operation.

The producer-consumer program is as follows:


' 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 threads and writer threads
    Private Shared reader As Mutex    ' synchronization variable for reading the array
    Private Shared writer As Mutex    ' synchronization variable for writing to the array
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' a random number generator
    Private Shared canRead As AutoResetEvent    ' indicates that the contents of data can be read
    Private Shared canWrite As AutoResetEvent

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

        ' the number of threads to generate
        Const nbThreads As Integer = 3

        ' initialization of flags
        canRead = New AutoResetEvent(False)        ' cannot read yet
        canWrite = New AutoResetEvent(True)        ' can already write

        ' initialize synchronization variables
        reader = New Mutex         ' synchronizes readers
        writer = New Mutex         ' synchronizes the writers

        ' Create reader threads
        Dim readers(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            readers(i) = New Thread(New ThreadStart(AddressOf read))
            readers(i).Name = "reader_" & i
            ' launch
            readers(i).Start()
        Next i

        ' create writer threads
        Dim writers(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' creation
            writers(i) = New Thread(New ThreadStart(AddressOf write))
            writers(i).Name = "writer_" & i
            ' launch
            writers(i).Start()
        Next i

        'End of Main
        Console.Out.WriteLine("End of Main...")
    End Sub

    ' read the contents of the array
    Public Shared Sub Read()
        ' critical section
        reader.WaitOne()        ' only one reader can proceed
        canRead.WaitOne()        ' must be able to read

        ' read array
        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") & " : Thread " & Thread.CurrentThread.Name & " read the number " & data(i)))
        Next i

        ' can no longer be read
        canRead.reset()
        ' can write
        canWrite.Set()
        ' end of critical section
        reader.ReleaseMutex()
    End Sub

    ' write to the array
    Public Shared Sub write()
        ' critical section
        ' only one writer can enter
        writer.WaitOne()
        ' must wait for write permission
        canWrite.WaitOne()

        ' write to array
        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") & " : The writer " & Thread.CurrentThread.Name & " wrote the number " & data(i)))
        Next i

        ' cannot write anymore
        canWrite.Reset()
        ' can read
        canRead.Set()
        'end of critical section
        writer.ReleaseMutex()
    End Sub
End Class

The execution yields the following results:

dos>lececr
End of Main...
05:56:56: Writer writer_0 wrote the number 459
05:56:57: Writer writer_0 wrote the number 955
05:56:58: Writer writer_0 wrote the number 212
05:56:59: Writer writer_0 wrote the number 297
05:57:00: The writer écrivain_0 wrote the number 37
05:57:01: Writer writer_0 wrote the number 623
05:57:02: Reader reader_0 read the number 459
05:57:03: Reader reader_0 read the number 955
05:57:04: Reader reader_0 read the number 212
05:57:05: Reader reader_0 read the number 297
05:57:06: Reader reader_0 read the number 37
05:57:07: Reader reader_0 read the number 623
05:57:08: Writer writer_1 wrote the number 549
05:57:09: Writer writer_1 wrote the number 34
05:57:10: Writer writer_1 wrote the number 781
05:57:11: Writer writer_1 wrote the number 555
05:57:12: The writer writer_1 wrote the number 812
05:57:13: The writer writer_1 wrote the number 406
05:57:14: Reader reader_1 read the number 549
05:57:15: Reader reader_1 read the number 34
05:57:16: Reader reader_1 read the number 781
05:57:17: Reader reader_1 read the number 555
05:57:18: Reader reader_1 read the number 812
05:57:19: Reader reader_1 read the number 406
05:57:20: Writer writer_2 wrote the number 442
05:57:21: Writer writer_2 wrote the number 83
^C

The following points can be noted:

  • there is indeed only one reader at a time, even though it loses the CPU in the read critical section
  • there is indeed only one writer at a time, even though it loses the CPU in the critical write section
  • A reader only reads when there is something to read in the table
  • A writer writes only when the array has been fully read