 This guide is an ongoing work. If you have any queries or suggestions concerning style, subject matter, or presentation of this guide, then do not hesitate to email the author at martin@pergolesi.demon.co.uk.

The latest on line copy of this document can be found on Martin Harvey's website, either as a set of HTML pages, or as a zip file.
In the event of slow downloads from this site, a zipped copy is also kept on the Borland CodeCentral archive.
If you want to find out what the author does when not writing programming tutorials, why not visit the rest of his website?

This document last updated on Saturday September 16 2000 at 9:00 PM BST.

Multithreading - The Delphi Way.

Martin Harvey.

Version 1.1

Table of Contents.

Introduction.
Dedications.
Recommended Reading.
Navigation hints.
Change history.
Credits.

Chapter 1. What are threads? Why use them?
Chapter 2. Creating a thread in Delphi.
Chapter 3. Basic synchronization.
Chapter 4. Simple thread destruction.
Chapter 5. More thread destruction. Deadlock.
Chapter 6. More synchronization: Critical sections and mutexes.
Chapter 7. Mutex programming guidelines. Concurrency control.
Chapter 8. Delphi thread safe classes and Priorities.
Chapter 9. Semaphores. Data flow scheduling. The producer - consumer relationship.
Chapter 10. I/O and data flow: from blocking to asynchronous and back.
Chapter 11. Synchronizers and Events.
Chapter 12. More Win32 synchronization facilities.
Chapter 13. Using threads in conjunction with the BDE, Exceptions and DLLs.
Chapter 14. A real world problem, and its solution.

Introduction.

This guide is intended for anyone who is interested in improving performance and responsiveness in their Delphi applications by using threads. It covers a range of topics from absolute beginner to intermediate level, and some of the real world examples raise issues bordering on the advanced. It assumes that the reader has a reasonable knowledge of Object Pascal programming, including simple object orientation, and a working understanding of event based programming.

Dedications.

Dedicated to three members of the Computer Science department at the University of Cambridge: Dr Jean Bacon, Dr Simon Crosby, and Dr Arthur Norman.

Many thanks to Jean as a tutor for making a complicated subject seem simple, for providing excellent reference material, and for lifting a corner of the veil around a hitherto mysterious subject. She also deserves thanks as a director of studies, for explaining the Computer science timetable to me. It took me three years to figure it out for myself!

Many thanks to Simon as a tutor, for showing me that although modern operating systems may be fiendishly complicated, the principles underlying them are simple. He also deserves thanks for taking on a student with unconventional ideas about final year project material, and for providing much useful advice on my project dissertation.

Arthur Norman never taught me a thing about multithreading. He did however teach me many other things, which helped me when writing the more complicated parts of this guide:

  • There is no limit to the eccentricity of university lecturers.
  • Although most people prefer simplicity, there is a certain perverse enjoyment to be had doing things the complicated way, especially if you're cynical.
He also deserves a mention for some of the best quotes ever to fall from a computer science lecturers lips:
  • "There is something in the lecture course which may not have been visible so far, which is reality ..."
  • "The theoreticians have proven that this is unsolvable, but there's three of us, and we're smart ..."
  • "People who don't use computers are more sociable, reasonable, and ... less twisted."
  • "[If complexity theory lives up to its title] if that proves to be the case, I will be the winner, as not many of you will attempt the exam questions."
He even has his own fan page.

Recommended reading:

Title: Concurrent Systems: An integrated approach to Operating Systems, Database, and Distributed Systems.
Author: Jean Bacon.
Publisher: Addison-Wesley
ISBN: 0-201-41677-8

The author welcomes suggestions for other useful titles.

Navigation hints.

The narrative and diagrams in this guide are all contained in single HTML pages, one for each chapter. The source code examples appear in pop up windows. You will need a javascript enabled browser to view these. To facilitate viewing of the narrative and source in parallel, the reader may find it useful to tile the various web browser windows. This can be achieved by right clicking on the task bar, and selecting "Tile Windows Vertically".

Change history.

Version 1.1:
  • Corrected some spelling and punctuation errors in the prose, and rewrote some unclear explanations. Chapters 1-9 and 12 modified.
  • Added change history and other credits to contents page.
  • Renamed Chapter 12.
  • Added Chapter 14.

Credits.

Many thanks to the following people for reviewing, suggesting, correcting and generally improving this guide:
  • Tim Frost
  • Conor Boyd
  • Alan Lloyd
  • Bruce Roberts
  • Bjørge Sæther
  • Dr John Stockton
  • Craig Stuntz
  • Jim Vaught

© Martin Harvey 2000.


Chapter 1. What are threads? Why use them?

In this chapter:

  • History.
  • Definitions.
  • An Example.
  • Time slicing.
  • Why use threads?

History.

In the early days of computing, all programming was essentially single threaded. You created your program by punching holes into cards or tape, submitted your deck of cards to the local computing centre, and after a few days, you received another deck of cards, containing, if you were lucky, the required results. All processing was batch, not time critical, first come first served, and when your program was running, it had exclusive use of the computer's time.

Things have moved on. The concept of multiple threads of execution first appeared with time sharing systems, where more than one person could be logged into a central mainframe computer at once. It was important to ensure that the processing time of the machine was fairly divided between all users, and the operating systems of the time made use of the "process" and "thread" concepts. Desktop computers have seen a similar progression. Early DOS and Windows systems were single tasking. Your program ran exclusively on the machine, or not at all. With increasingly sophisticated applications, and increasing demands on personal computers, especially with respect to high performance in the graphics and networking areas, multiprocess and multithread operating systems are now commonplace. Multithreading on PC's has mainly been driven by the need for better performance and usability

Definitions.

The first concept to define is that of the process. Most Windows 95, 98 and NT users have a good intuitive idea of what a process is. They see it as a program which runs on the machine, co-existing and sharing CPU, disk and memory resources with other programs. Programmers know a process to be an invocation of executable code, such that that code has a unique existence, and the instructions executed by that process are executed in an ordered manner. On the whole, processes execute in isolation. The resources they use (memory, disk, I/O, CPU time) are virtualised, such that every process has its own set of virtual resources, untouched by other processes. The operating system provides this virtualisation. Processes execute modules of code. These may be disjoint; in the sense that, the executable modules of code comprising Windows Explorer and Microsoft Word are disjoint. However, they may also be shared, as in the case of DLL's. The code for a DLL is typically being executed in the context of many different processes, often simultaneously. The execution of instructions is on the whole not ordered between processes: Microsoft word does not stop opening a document just because the print spooler is currently sending something to the printer! Of course, where different processes interact, the programmer must impose an ordering, a central problem which will be covered later.

Our next concept is that of the thread. Threads were developed when it became clear that it was desirable to have applications which performed sets of actions in a more loosely time ordered fashion, possibly performing several sets of actions at once. In situations where some actions would cause a considerable delay in one thread of execution (e.g.. waiting for the user to do something), it was often desirable to have the program still perform other actions concurrently (e.g. background spell checking, or processing incoming network messages). However, the overhead of creating a whole new process for each concurrent action, and then having the processes communicate was often far too much of an overhead.

An Example.

If one needs to look for a good example of multithreading, then Windows Explorer (i.e. the Windows Shell) is an excellent example. Double click on "My Computer", and click through a few sub folders, creating new windows as you go. Now invoke a lengthy copy operation on one of those windows. The progress bar pops up, and that particular window does not respond to user input. However, all the other windows are perfectly usable. Obviously, several things are going on at once, but only one copy of explorer.exe is running. This is the essence of multithreading.

Time slicing.

In most systems that support multithreading, there may be many users making simultaneous requests on the computer system. Normally the number of physical processors in the system is fewer than the number of threads that might be run in parallel. Most systems support time slicing, also known as pre-emptive multitasking, in order to get around this problem. In a system that is time sliced, threads run for a short while, and are then pre-empted; that is, a hardware timer fires which causes the operating system to re-evaluate which threads should be run, potentially stopping execution on currently running threads, and running other threads which have not be executed recently. This allows even single processor machines to run multiple threads. On PC's a timeslice tends to be about about fifty five milliseconds long.

Why use threads?

Threads should not alter the semantics of a program. They simply change the timing of operations. As a result, they are almost always used as an elegant solution to performance related problems. Here are some examples of situations where you might use threads:
  • Doing lengthy processing: When a windows application is calculating it cannot process any more messages. As a result, the display cannot be updated.
  • Doing background processing: Some tasks may not be time critical, but need to execute continuously.
  • Doing I/O work: I/O to disk or to network can have unpredictable delays. Threads allow you to ensure that I/O latency does not delay unrelated parts of your application.
All of these examples have one thing in common: In the program, some operations incur a potentially large delay or CPU hogging, but this delay or CPU usage is unacceptable for other operations; they need to be serviced now. Of course there are other miscellaneous benefits, and here they are:
  • Making use of multiprocessor systems: You can't expect one application with only one thread to make use of two or more processors! Chapter 3 explains this in more detail.
  • Efficient time sharing: Using thread and process priorities, you can ensure that everyone gets a fair allocation of CPU time.
The wise use of threads turns slow, clunky, not very responsive programs into crisply responsive, efficient, fast programs, and can radically simplify various performance and usability problems.

[Contents] [Next]

© Martin Harvey 2000.


Chapter 2. Creating a thread in Delphi.

In this chapter:

  • A diagrammatic interlude.
  • Our first non-VCL thread.
  • What exactly does this program do?
  • Issues, problems and surprises.
  • Start-up issues.
  • Communication issues.
  • Termination issues.

A diagrammatic interlude.

Before looking into the details of creating a thread, and getting it to execute code independently of the main application thread, it is necessary to introduce a new sort of diagram illustrating the dynamics of thread execution. This will aid us considerably when we start designing and creating multithreaded programs. Consider this simple application.

The application has one thread of execution: the main VCL thread. The progress of this thread can be illustrated with a diagram showing the status of the thread in the application over time. The progress of the thread is represented by a line, and time flows down the page. I have included a key in this diagram, which applies to all subsequent thread diagrams.

Note that this diagram does not indicate much about the algorithms executing. Instead it illustrates the ordering of events over time, and the state of the threads in the application between those times. The actual distance between different points in the diagram is not particularly important, but the vertical ordering of those points is. There are several pieces of information to be gleaned from this diagram.

  • The thread in this application is not continually executing. There may be long periods of time where it receives no external stimulus, and is not carrying out any calculations or operations at all. The memory and resources occupied by the application exist, and the window is still on the screen, but none of its code is executing on the CPU.
  • The application is started, and the main thread executes. Once it has created the main window, it has no more work to do, and it drops into a piece of code in the VCL known as the application message loop that queries the operating system for more messages. If there are no more messages to be processed, the operating system suspends the thread, and the thread is now suspended.
  • At some later point, the user clicks on the button to display the text message. The operating system wakes up (or resumes) the main thread, and gives it a message indicating that a button has been clicked. The main thread is now active again.
  • This resume - suspend process occurs again several times. I have illustrated a wait for user confirmation to close the message box, and a wait for the close button to be clicked. In practice, many other messages might be received.

Our first non-VCL thread.

Although the Win32 API provides comprehensive multithreading support, when it comes to the creation and destruction of threads, the VCL has a useful class, TThread, which abstracts away many of the technicalities of creating a thread, provides some useful simplifications, and tries to prevent the programmer from falling into some of the more unpleasant traps that this new discipline provides. I recommend its use. The Delphi help files provide reasonable guidance when creating a thread class, so I won't mention much about the sequence of menu actions required to create a thread apart from suggesting that the reader select File | New... and then choose Thread Object.

This particular example consists of a program which calculates whether a particular number is prime or not. It contains two units, one with a conventional form, and one with a thread object. It more or less works, but has some decidedly unpleasant quirks, which illustrate some of the basic problems which multithreaded programmers face. We will discuss ways of circumventing these problems later on. Here is the source of the form, and here is the source of the thread object.

What exactly does this program do?

Each time the "Spawn" button is clicked, the program creates a new thread object, initializes some fields in the object, and then sets the thread on its way. Depending on the size of the input number, the thread grinds away calculating whether the number is prime, and once it has finished the calculation, the thread displays a message box, indicating whether the number is prime. These threads are concurrent, whether you have a uniprocessor or multiprocessor machine; from the point of view of the user, they execute simultaneously. In addition, this program does not limit the number of threads created. As a result, you can demonstrate that there is true concurrency in the following manner:
  • Since I have commented out an exit statement in the prime number determination routine, the time taken for the thread to run is roughly proportional to the size of its input. I have found that with an argument of about 2^24, the thread takes about 10-20 seconds to complete. Find a value that produces a similar delay for your machine.
  • Run the program, enter in your large number, and click the button.
  • Immediately enter in a small number (say 42) and click the button again. You will notice that the result for the small number is produced before the result for the large number, even though we started the large number first. The diagram below illustrates the situation.

Issues, problems and surprises with our first threads.

At this point the issue of synchronization rears its ugly head. Once the main thread has called Resume on a "worker" thread, the main program thread cannot assume anything about the state of the worker thread and vice versa. It is entirely possible that the worker thread might complete execution before the progress of the main VCL thread has moved on by one statement. In fact, for fairly small inputs that take less than 1/20th of a second to calculate, it is fairly likely. Similarly, the worker thread cannot assume anything about the state of progress of the main thread. It's all up to the mercy of the Win32 scheduler. There are three basic "fun factors" that one encounters: Start-up issues, Communication issues, and Termination issues.

Start-up issues.

Delphi makes dealing with thread start-up easy. Before getting a child thread to execute, one often wants to set up some initial state in the thread. By creating the thread suspended (an argument to the constructor) one can ensure that none of the code in the thread executes until the thread is resumed. This means that the main VCL thread can safely read and modify data in the TThread object, with the guarantee that it will be updated and valid by the time the child thread gets round to executing.

In the case of this program, the "FreeOnTerminate" and "TestNumber" properties of the thread are set before the thread starts executing. If this was not the case, then the behaviour of the thread would be undefined. If you don't want to create threads suspended, then you simply move the start-up problems into the next category: communication issues.

Communication issues.

These occur when you have two threads which are both running, and you need to communicate between them in any way, shape or form. This program dodges the issue, by simply not having any communication at all between separate threads. Suffice to say at this point, that unless you protect all operations on shared data (for an adequate definition of "protect"), your program is quite likely to be non-deterministic. If you do not have adequate synchronization or concurrency control, the following are no-no's:
  • Accessing any form of shared resource between two threads.
  • Playing with thread unsafe parts of the VCL in a non-VCL thread.
  • Attempting to do graphics operations in a separate thread.
Even doing things as simple as having two threads accessing a shared integer variable can result in complete disaster, and unsynchronized access to shared resources or VCL calls will result in many hours of fraught debugging, considerable confusion, and eventual consignment to the nearest mental hospital. Until you have learnt the appropriate techniques in later chapters,dDon't do it.

The good news? You can do all three of the above if you use the correct mechanisms for controlling concurrency, and it's not even that hard! We will be looking at a simple way of solving communication issues via the VCL in the next chapter, and more elegant (but complicated) methods later on.

Termination issues.

Threads, just like any other Delphi object, involve memory and resource allocation, so it should come as no surprise to learn that it is important to handle thread termination gracefully, something that this example program fails to do. There are two possible approaches to the deallocation problem.

The first is to let the thread handle the problem itself. This is mainly used for threads that either a) Communicate the results of thread execution back to the main VCL thread before termination. or b) Do not contain any information useful to other threads at termination time. In these cases, the programmer can set the "FreeOnTerminate" flag in the thread object, and it will dispose of itself when it has finished.

The second is to have the main VCL thread read data from the worker thread object when it has finished, and then dispose of the worker thread. This is covered later in Chapter 4.

I have side-stepped the issue of communicating results back to the main thread by having the child thread present the answer to the user via a call to "ShowMessage". This does not involve communication with the main VCL thread, and the ShowMessage call happens to be thread safe (by and large), so the VCL stays happy. As a result of this, I can use the first approach to Thread deallocation, and let the thread dispose of itself. Despite this, the example program illustrates one nasty feature of having threads clean up after themselves:

As you can see, there are two things that can happen. The first is that we try to exit the program whilst the thread is still active and calculating. The second is that we try to exit the program whilst it is suspended. The first case is fairly benign: the application quits without consulting the thread. The Delphi and Windows cleanup code make this fairly clean. The second is not so pretty since the thread is suspended somewhere in the bowels of the Win32 messaging subsystem. As it turns out, Delphi appears to do a good clean-up job under both circumstances. However, it is less than immaculate programming style to force-quit a thread without reference to what it is currently doing. For example, the worker threads might be in the process of writing to a file. If the user quits the program before they have finished, then the file is corrupted. This is why it is a good idea to have child or worker threads perform a co-ordinated exit with the main VCL thread even if no data transfer is required: a clean thread and process termination is possible. Chapter 4 discusses solutions to this problem.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 3. Basic synchronisation.

Chapter 3. Basic synchronization.

In this chapter:

  • What data is shared between threads?
  • Atomicity when accessing shared data.
  • Additional VCL problems.
  • Fun with multiprocessor machines.
  • The Delphi solution: TThread.Synchronize.
  • How does this work? What does Synchronize do?
  • Synchronizing to non-VCL threads.

What data is shared between threads?

First of all, it is worth know exactly what state is stored on a per process and per thread basis. Each thread has its own program counter and processor state. This means that threads progress independently through the code. Each thread also has its own stack, so local variables are inherently local to each thread, and no synchronization issues exist for these variables. Global data in the program can be freely shared between threads, and thus synchronization problems may exist with these variables. Of course, if a variable is globally accessible, but only one thread uses it, then there is no problem. The same situation holds for memory allocated on the heap (normally objects): in principle, any thread could access a particular object, but if the program is written so that only one thread has a pointer to a particular object, then only one thread can access it, and no concurrency problem exists.

Delphi provides a threadvar keyword. This allows "global" variables to be declared where a copy of the variable is created for each thread. This feature is not used much because it is often more convenient to put such variables inside a TThread class, hence creating one variable instance for each TThread descendant created.

Atomicity when accessing shared data.

In order to understand how to make threads work together, it is necessary to understand the concept of atomicity. An action or sequence of actions is atomic if the action or sequence is indivisible. When a thread performs an atomic action, it means that all other threads view the action as either having not started, or as having completed. It is not possible for one thread to catch another "in the act". If no synchronization is performed between threads, then just about all operations are non-atomic. Let's take a simple example. Consider this fragment of code. What could be simpler? Unfortunately, even this trivial piece of code can cause problems if two separate threads use it to increment the shared variable A. This single pascal statement breaks down into three operations at the assembler level.

Read A from memory into a processor register.
Add 1 to processor register.
Write contents of processor register to A in memory.

Even on a single processor machine, the execution of this code by multiple threads may cause problems. The reason it does so is because of scheduling operations. When only one processor exists, only one thread actually executes at one time, but the Win32 scheduler switches between them at about 18 times per second. The scheduler may stop one thread running and start another thread at any time: the scheduling is pre-emptive. The operating system does not wait for permission before suspending one thread and starting another: the switch may happen at any time. Since the switch can occur between any two processor instructions, it may occur at an inconvenient point in the middle of a function, and even half way through the execution of one particular program statement. Let's imagine that two threads are executing the example code on a uniprocessor machine (X and Y). In a nice case, the program may be running, and the scheduling operations may miss this critical point, giving the expected results: A is incremented by two.
Instructions executed by Thread X | Instructions executed by Thread Y | Value of variable A in memory
<Other Instructions> | Thread Suspended | 1
Read A from memory into a processor register. | Thread Suspended | 1
Add 1 to processor register. | Thread Suspended | 1
Write contents of processor register (2) to A in memory. | Thread Suspended | 2
<Other Instructions> | Thread Suspended | 2
THREAD SWITCH | THREAD SWITCH | 2
Thread Suspended | <Other Instructions> | 2
Thread Suspended | Read A from memory into a processor register. | 2
Thread Suspended | Add 1 to processor register. | 2
Thread Suspended | Write contents of processor register to A (3) in memory. | 3
Thread Suspended | <Other Instructions> | 3

However, this is by no means guaranteed, and is up to blind chance. Murphy's law being what it is, the following situation may occur:
Instructions executed by Thread X | Instructions executed by Thread Y | Value of variable A in memory
<Other Instructions> | Thread Suspended | 1
Read A from memory into a processor register. | Thread Suspended | 1
Add 1 to processor register. | Thread Suspended | 1
THREAD SWITCH | THREAD SWITCH | 1
Thread Suspended | <Other Instructions> | 1
Thread Suspended | Read A from memory into a processor register. | 1
Thread Suspended | Add 1 to processor register. | 1
Thread Suspended | Write contents of processor register (2)to A in memory. | 2
THREAD SWITCH | THREAD SWITCH | 2
Write contents of processor register (2) to A in memory. | Thread Suspended | 2
<Other Instructions> | Thread Suspended | 2

In this case, A is not incremented by two but by only one. Oh dear! If A happens to be the position of a progress bar, then perhaps this isn't such a problem, but if it's anything more important, like a count of the number of items in a list, then one is likely to run into problems. If the shared variable happens to be a pointer then one can expect all sorts of unpleasant results. This is known as a race condition.

Additional VCL problems.

The VCL contains no protection against these conflicts. This means that thread switches may occur when one or more threads are executing VCL code. A lot of the VCL is sufficiently well contained that this is not a problem. Unfortunately, components, and in particular, children of TControl contain various mechanisms which do not take kindly to thread switches. A thread switch at the wrong time can wreak complete havoc, corrupting reference counts for shared handles, destroying not only data, but also links between components.

Even when threads are not executing VCL code, lack of synchronization can still cause further problems: It is not enough to ensure that the main VCL thread is dormant before another thread dives in and modifies something. Some code in the VCL may execute which (for instance) pops up a dialog box, or invokes a disk write, suspending the main thread. If another thread modifies shared data, it may appear to the main thread that some global data has magically changed as a result of the call to display a dialog or write to a file. This is obviously not acceptable and means that either only one thread can execute VCL code, or a mechanism must be found to ensure that separate threads do not interfere with each other.

Fun with multiprocessor machines.

Luckily for the application writer, the problem is not made any more complex for machines with more than one processor. The synchronization methods that Delphi and Windows provide work equally well whatever the number of processors. Implementors of the Windows operating systems have had to write extra code to cope with multiprocessing: Windows NT 4 informs the user at bootup whether it is using the uniprocessor or multiprocessor kernel. However, for the application writer, this is all hidden. You do not need to worry about how many processors the machine has any more than you have to worry about which chipset the motherboard uses.

The Delphi solution: TThread.Synchronize.

Delphi provides a solution which is ideal for beginners to thread writing. It is simple and overcomes all the problems mentioned so far. TThread has a method called Synchronize. This method takes as a parameter another parameterless method which you want to be executed. You are then guaranteed that the code in the parameterless method will be executed as a result of the synchronize call, and will not conflict with the VCL thread. As far as the non-VCL thread that calls synchronize is concerned, it appears that all the code in the parameterless method happens at the moment synchronize is called.

Hmm. Sound confusing? Quite possibly. I'll illustrate this with an example. We will modify our prime number program, so that instead of showing a message box, it indicates whether a number is prime or not by adding some text to a memo in the main form. First of all, we'll add a new memo (ResultsMemo) to our main form, like this. Now we can do the real work. We add another method (UpdateResults) to our thread which displays the results on the memo, and instead of calling ShowMessage, we call Synchronize, passing this method as a parameter. The declaration of the thread and the modified parts now look like this. Note that UpdateResults accesses both the main form, and a result string. From the viewpoint of the main VCL thread, the main form appears to be modified in response to an event. From the viewpoint of the prime calculation thread, the result string is accessed during the call to Synchronize.

How does this work? What does Synchronize do?

Code which is invoked when synchronize is called can perform anything that the main VCL thread might do. In addition, it can also modify data associated with its own thread object, safe in the knowledge that the execution of its own thread is at a particular point (the call to synchronize). What actually happens is rather elegant, and best illustrated by another diagram.
When synchronize is called, the prime calculation thread is suspended. At this point, the main VCL thread may be suspended in the idle state, it may be suspended temporarily on I/O or other operations, or it may be executing. If it is not suspended in a totally idle state (main application message loop), then the prime calculation thread keeps waiting. Once the main thread becomes idle, the parameterless function passed to synchronize executes in the context of the main VCL thread. In our case, the parameterless function is called UpdateResults, and plays around with a memo. This ensures that no conflicts will occur with the main VCL thread, and in essence, the processing of this code is much like the processing of any Delphi code which occurs in response to the application being sent a message. No conflicts occur with the thread that called synchronize because it is suspended at a known safe point (somewhere in the code for TThread.Synchronise).

Once this "processing by proxy" completes, the main VCL thread is free to go about its normal work, and the thread that called synchronize is resumed, and returns from the function call. Thus a call to Synchronize appears to be another message to the main VCL thread, and a function call to the Prime calculation thread. The threads are at known locations, and do not execute concurrently. No race conditions occur. Problem solved.

Synchronizing to non-VCL threads.

My previous example shows how a single thread can be made to interact with the main VCL thread. In effect it borrows time from the main VCL thread to do this. This doesn't work arbitrarily between threads. If you have two non-VCL threads, X and Y, you can't call synchronize in X alone and then modify data stored in Y. It is necessary to call synchronize from both threads when reading or writing the shared data. In effect, this means that the data is modified by the main VCL thread, and all the other threads synchronize to the main VCL thread every time they need to access this data. This is workable, but inefficient, especially if the main thread is busy: every time the two threads need to communicate, they have to wait for a third thread to become idle. Later on, we shall see how to control concurrency between the threads and have them communicate directly.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 4. Simple thread destruction.

In this chapter:

  • Issues with thread completion, termination and destruction.
  • Premature thread termination.
  • The OnTerminate Event.
  • Controlled Thread Termination - Approach 1.

Issues with thread completion, termination and destruction.

Back in Chapter 2, an outline was given of some of the problems inherent in thread termination. There are two main issues:
  • Exiting the thread cleanly, and clearing up the resources allocated.
  • Obtaining results from the thread when it has finished.
These points are closely interrelated. If a thread does not have to communicate any information back to the main VCL thread when it has finished, or if one uses the techniques described in the previous chapter to communicate results just before the thread object terminates, then there is no need for the main VCL thread to participate in any thread clean-up. In these cases, one can set FreeOnTerminate to true for the thread, and let the thread perform its own memory deallocation. Remember that if one does this, the user can force quit the program, resulting in termination of all the threads in it, with possibly unforeseen consequences. If the thread only writes to memory, or communicates with other parts of the application, then this is not a problem. If it writes to a file or a shared system resource, then this is not acceptable.

If a thread has to exchange information with the VCL after termination, then a mechanism has to be found for synchronizing the VCL thread with a worker thread, and the main VCL thread has to perform the cleanup (you have to write code to free the thread). Two mechanisms will be outlined later.

There is one more point to be borne in mind:

  • Terminating a thread before its course of execution has run to completion.
This may actually occur quite often. Some threads, especially those that process I/O, execute in a permanent loop: the program might always receive more data, and the thread always has to be prepared to process it until the program terminates.

So, addressing these points in reverse order...

Premature thread termination.

In some situations, one thread may need to notify another thread that it should terminate. This commonly occurs if a thread is executing a lengthy operation, and the user decides to quit the application, or the operation must be aborted. TThread provides a simple mechanism to support this in the form of the Terminate method, and the Terminated property. When a thread is created, its terminated property is set to false. Whenever a thread's terminate method is called, the terminated property for that thread is set to true. It is thus the responsibility of all threads to periodically check whether they they have been terminated, and if they have, to gracefully quit execution. Note that no large scale synchronization happens around this process; When one thread sets the terminated property of another, it cannot assume that the other thread has read the value of its terminated property and started termination. The Terminated property is simply a flag, saying "please quit as soon as possible". The diagram below illustrates this situation.
When designing thread objects, some thought has to be given to reading the terminated property when required. If your thread blocks, as a result of any of the synchronization mechanisms discussed later, you may have to override the terminate method to unblock your thread. In particular, you will remember to have to call the inherited terminate method before unblocking your thread if you want the next test for terminate to return true. More on this anon. As an example, here is a small modification to the prime number calculation thread of the previous chapter to ensure that it checks for the terminated property. I have assumed that it is acceptable for the thread to return an incorrect result when the terminated property is set.

The OnTerminate Event.

The OnTerminate event occurs when a thread has truly finished execution. It does not happen when terminate method of a thread is called. This event is potentially quite useful, in that it is executed in the context of the main VCL thread, just like methods passed to synchronize. Thus, if one wishes to perform some VCL operations with a thread that automatically frees itself upon termination, then this is the place to do it. Most programmers new to threads will find this to be the most useful way of getting a non VCL thread to transfer its data back to the VCL, with a minimum of fuss, and without requiring any explicit synchronization calls.
As you can see from the diagram above, OnTerminate works in much the same way as Synchronize, and it is almost identical in semantics to putting a call to Synchronize at the end of a thread. The main use for this is that by using a flag, such as "AppCanQuit" or a reference count of running threads, in the main VCL thread, a simple mechanisms can be provided to ensure that the main VCL thread exits only when all other threads have quit. There are synchronization subtleties involved here, especially if a programmer were to put a call to Application.Terminate into the OnTerminate event of a thread, but these will be dealt with later.

Controlled Thread Termination - Approach 1.

In this example we will take the prime number program code in Chapter 3, and modify it so that the user cannot inadvertently close the application when worker threads are executing. This turns out to be simple. In fact, we need not modify the thread code at all. We simply add a reference count field to the main form, increment it when creating threads, set the OnTerminate event of the threads to point to a handler in the main form which decrements the reference count, and when the user requests that the application be closed, we raise a warning dialog box if necessary.

The example shows how simple this approach is: all the code concerned with keeping track of the number of threads in execution all occurs in the main VCL thread, and the code is essentially event driven, just like any other Delphi application. In the next chapter we will consider a potentially more complicated approach, which has benefits when using more advanced synchronization mechanisms.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 5. More thread destruction. Deadlock.

In this chapter:

  • The WaitFor method.
  • Controlled Thread Termination - Approach 2.
  • A quick intro to message passing and lazy notification.
  • WaitFor may result in a long delay.
  • Have you spotted the bug?
  • Avoiding this particular manifestation of deadlock.

The WaitFor method.

OnTerminate, discussed in the previous chapter, is useful if you are using threads in a "fire and forget" manner, with automatic destruction. What if you have to ensure that, at a certain point in the execution of the VCL thread, all other threads have terminated? The solution to this is the WaitFor method. This method is useful if:
  • The main VCL thread needs to access the worker thread object after its execution has finished, and read or modify data contained in the thread.
  • Force terminating threads upon program closure is not a viable option.
Quite simply, when thread A calls the WaitFor method of thread B, thread A is suspended until thread B has finished executing. When thread A resumes, it can be sure that results from thread B can be read, and that the thread object representing B can be destroyed. Typically, this occurs at program termination, where the main VCL thread will call Terminate on all the non VCL threads, and then WaitFor all the non-VCL threads before quitting.

Controlled Thread Termination - Approach 2.

In this example we will modify the prime number program code so that only one thread executes at a time, and the program waits for the thread to complete before exiting. Although, in this program, it is not strictly necessary for the main thread to wait for other threads to terminate, this is a useful exercise and demonstrates several properties of WaitFor that are not always desirable. It also illustrates a couple of fairly subtle bugs which might slip past thread programming novices. Firstly the code for the main form. As you can see, there are several differences from the previous example:
  • We have a "magic number" declared at the top of the unit. This is an arbitrary message number, and its value is not important provided it is the only message in the application with this number.
  • Instead of having a reference count of threads, we maintain an explicit reference to one thread and one thread only, pointed to by the FThread member variable of the main form.
  • We only want one thread to be executing at any one time since we only have one member variable pointing to a worker thread. Consequently, the thread creation code checks that we do not have a thread currently executing before creating another.
  • The thread creation code does not set the FreeOnTerminate property to true. Instead, the main VCL thread will free the worker thread later.
  • The main form has a message handler defined which waits for the worker thread to complete, and then frees it.
  • Likewise, the code executed when the user wishes to close the form waits for the worker thread to complete and frees it.
Having noted those points, here is the worker thread code.Again, there are some small differences from the code as presented in Chapter 3:
  • The IsPrime function now tests for thread termination requests, resulting in a quick exit if the terminated property has been set.
  • The Execute function tests for abnormal termination.
  • If there is no abnormal termination, then it uses synchronize to display the results, and posts a message to the main form, requesting that the main form clean it up.

A quick intro to message passing and lazy notification

In the normal case of affairs, the thread is executed, runs its course, uses synchronize to display the results, and then posts a message to the main form. This message posting is asynchronous: the main form picks up this message at some point in the future. PostMessage does not suspend the worker thread, it carries on through to completion. This is a very useful property: we can't use synchronize to tell the main form to free the thread, because we would then "return" from the Synchronize call to a non-existent thread. Instead, it simply acts as a notification, a gentle reminder to the main form that it should free the thread as soon as possible.

At some later point, the main program thread receives the message, and executes the handler. This handler checks that a thread exists, and if it does, it waits for it to complete. This step is necessary because although it is likely that the worker thread has run to completion (there aren't many statements after the PostMessage), it is not guaranteed. Once this wait has completed, the main thread can then clean up the worker thread.

The diagram below illustrates this first case. For the sake of simplicity, the details of the Synchronize operation have been omitted from the diagram. In addition, the call to PostMessage has been displayed as occurring some time before the worker thread completes in order to illustrate the functioning of the WaitFor operation.

Later chapters will cover the advantages of posting messages in more detail. Suffice to say at this point that this technique is useful when interfacing with the VCL thread.

In the abnormal case of affairs, the user tries to quit the application, and confirms that he would like an immediate exit. The main thread sets the terminated property of the worker thread, which will hopefully ensure a reasonably swift termination, and it then waits for it to complete. Upon completion, the cleanup proceeds as before. The diagram below illustrates this second case.

Many readers may at this point be perfectly happy with the situation. However, trouble lurks in the wings, and as is often the case when considering multi-thread synchronization, the devil is in the detail.

WaitFor may result in a long delay.

The benefit of WaitFor is also its biggest drawback: it suspends the main thread in a state where it cannot receive messages. This means that the application cannot perform any of the operations normally associated with message processing: the application will not redraw, resize or respond to external stimuli when it is waiting. As far as the user is concerned, it appears that the application has hung. This is not much of a problem in the case of normal thread termination; by making the call to PostMessage the very last operation in the worker thread, we ensure that the main thread will not have to wait long. However, in the case of an abnormal thread termination, the amount of time the main thread spends in this state is entirely dependent on how often the worker thread checks the terminate property. The source for PrimeThread contains a line marked "Line A". If the "and not terminated" is removed, then you can experiment with quitting the app during the execution of a long thread.

There are some advanced methods of removing this dilemma involving the Win32 message wait functions, and an explanation of this method can be found by visiting http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.html. On the whole, it is simpler to write threads which check the Terminated property on a regular basis. If this is not possible, then it is often worthwhile displaying some form of warning to the user about the potential unresponsiveness of the application (a la Microsoft Exchange.)

Have you spotted the bug? WaitFor and Synchronize: An introduction to deadlock.

The delay that WaitFor incurs is a truly minor problem when compared with the other vice it has. In applications that use both Synchronize and WaitFor, it is entirely possible to make the application deadlock. Deadlock is a phenomenon whereby no algorithmic error occurs in the application, but the entire application is stopped, dead in the water. In the general case, deadlock occurs when threads wait for each other in a cyclical manner. Thread A may be waiting for thread B to complete some operation, whilst thread C waits for thread D, etc. etc. At the end of the line, thread D might be waiting for Thread A to complete some operation. Unfortunately, thread A can't complete the operation because it is suspended. This is the computing equivalent of the "A: You Go First ... B: No you ... A: No, I insist!" problem that besets motorists when ownership of the right of way is not clear. This behaviour is documented in the VCL help files.

In this particular case, deadlock can occur between the two threads if the calculation thread calls Synchronize shortly before the main thread calls WaitFor. If this occurs, then the calculation thread will be waiting for the main thread to return to the message loop, whilst the main thread is waiting for the calculation thread to complete. Deadlock will result. It is also possible for the main VCL thread to call WaitFor shortly before the worker thread calls Synchronize. Given a simplistic implementation, this would also result in deadlock. Luckily, the VCL implementors managed to trap this error case, which results in an exception being raised in the worker thread, thus breaking the deadlock, and quitting the thread.

The programming of the example, as it stands, makes this fairly unlikely. The worker thread only calls Synchronize if it reads the Terminated property as false shortly before finishing execution. The main application thread sets the terminated property shortly before calling WaitFor. Thus for deadlock to occur, the worker thread would have to read the terminated property as false, execute a synchronize, and then control would have to be transferred to the main thread exactly at the point where the user has confirmed an application force quit.

Despite the fact that in this case deadlock is unlikely, events of this sort are clearly race conditions. It all depends on the exact timing of events, which will vary from run to run and from machine to machine. 99.9% of the time, a forced closure will work, and one time out of a thousand, everything might lock up: exactly the sort of problem that needs to be avoided at all costs. The reader may remember that I previously mentioned that no large scale synchronization occurs when reading or writing the terminated property. This property means that it is not possible to use the terminated property to avoid this problem, as the previous diagram makes clear.

The interested reader may wish to duplicate this deadlock problem. This can be made relatively likely by performing the following modifications to the source code:

  • Remove the "and not terminated" at Line A
  • Replace the "not terminated" at line B with "true".
  • Remove the comment on Line C.
Deadlock can then be provoked by running a thread whose execution takes about 20 seconds, and force quitting the application shortly after the thread is created. The reader may also wish to adjust the length of time that the main application thread sleeps for in order to achieve the "correct" ordering of events:
  • The user starts a calculation thread.
  • The user then tries to quit and says "Yes, I'd like to quit despite the fact threads are running".
  • The main application thread goes to sleep (Line C)
  • The calculation thread eventually gets to the end of execution and calls Synchronize. (Aided by modifications to lines A and B).
  • The main application thread wakes up from sleep and calls WaitFor.

Avoiding this particular manifestation of deadlock.

The best method of avoiding this form of deadlock is not to use WaitFor and Synchronize in the same application. WaitFor can be avoided by using the OnTerminate event, as previously discussed. As luck would have it in this example, the results that the thread returns are sufficiently simple that we can avoid using Synchronize in an almost trivial manner. By using WaitFor, the main thread can now legally access properties of the worker thread after termination, and all that is needed is a "result" variable to hold the text string produced by the worker thread. The modifications required are:
  • The removal of the "DisplayResults" method of the thread.
  • The addition of an appropriate property to the worker thread.
  • The modification of the message handler in the main form
Here are the relevant changes. This just about concludes the discussion of mechanisms for synchronization common to all 32 bit Delphi versions. I have not yet discussed two methods: TThread.Suspend and TThread.Resume. These are discussed in Chapter 10. Further chapters explore the facilities offered by the Win32 API, and later versions of Delphi. I would suggest that, once the reader has got to grips with the Delphi threading basics, he takes the time to study these more advanced mechanisms, since they allow a good deal more flexibility than the native Delphi mechanisms, and allow the programmer to co-ordinate threads in a more elegant and efficient manner, as well as reducing the possibility of writing code that leads to deadlocks.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 6. More synchronisation: Critical sections and mutexes.

Chapter 6. More synchronization: Critical sections and mutexes.

In this chapter:

  • Synchronize limitations.
  • Critical Sections.
  • So what does this all mean to the Delphi programmer?
  • Points of interest.
  • Can data be lost or frozen in the buffer?
  • What about out of date messages?
  • Flow Control issues and list inefficiencies.
  • Mutexes.

Synchronize limitations.

Synchronize has several drawbacks which make it unsuitable for anything except very simple multithreaded applications.
  • Synchronize is only useful when attempting to communicate between a worker thread and the main VCL thread.
  • Synchronize insists that the worker thread wait until the main VCL thread is completely idle even when this is not strictly necessary.
  • If applications make frequent use of Synchronize, the main VCL thread becomes the bottleneck, and no real performance gains are realized.
  • If synchronize is used to communicate indirectly between two worker threads, both threads can be suspended waiting for the main VCL thread.
  • Synchronize can cause deadlock if the main VCL thread waits for any other threads.
On the plus side, Synchronize does have one advantage over most other synchronization mechanisms:
  • Just about any code can be passed to Synchronize, including thread unsafe VCL code.
It is important to remember why threads are being used in an application. The overriding reason for most Delphi programmers will be that they want their application to remain responsive, whilst performing operations that either take a long time, or use blocking data transfers or I/O. This often means that the main application thread should be performing short, event based routines, and handling user interface updates. It's good at responding to user input, and displaying user output. The other threads in the application will be performing the "grunt work". In the light of this philosophy, one often finds that most code executing in worker threads does not use parts of the VCL which are not thread safe. Worker threads may perform operations on files, or databases, but they rarely use descendants of TControl. Seen in this light, Synchronize is a case of overkill.

Many threads only need to communicate with the VCL in simple ways, such as transferring a stream of data, or executing a database query and returning a data structure as the result of this query. Referring back to Chapter 3, we note that we only need to maintain atomicity when modifying shared data. To take a simple example, we may have a stream which is written to by a worker thread, and read periodically by the main VCL thread. Do we need to ensure that the VCL thread is never executing at the same time as a worker thread? Of course not! All we need to ensure is that only one thread modifies this shared resource at once, thus eliminating race conditions, and making operations on the shared resource atomic. This property is known as mutual exclusion. There are many synchronization primitives which can be used to enforce this property. The simplest of these is known as the Mutex. Win32 provides a mutex primitive, and a close relation of it, the Critical Section. Some versions of Delphi contain a class which encapsulates the Win32 critical section calls. This class will not be discussed here, since this functionality is not common to all 32 bit versions of Delphi. The users of such a class should have little difficulty using the corresponding methods in the class to achieve the same effects as those discussed here.

Critical Sections.

The Critical Section is a primitive which allows us to enforce mutual exclusion. The Win32 API supports several operations upon it:
  • InitializeCriticalSection.
  • DeleteCriticalSection.
  • EnterCriticalSection.
  • LeaveCriticalSection.
  • TryEnterCriticalSection (Windows NT only).
The InitializeCriticalSection and DeleteCriticalSection operations can be considered to be very much the same as heap creation and destruction of objects. On the whole, it is sensible to keep critical section creation and destruction to one particular thread, normally the longest lived. Obviously, all the threads that wish to synchronize access using this primitive must have a handle or pointer to this primitive. This may be direct via a shared variable, or indirect, perhaps because the critical section is embedded in a thread safe class to which both threads have access.

Once the critical section object is created it can be used to control access to a shared resource. The two main operations are EnterCriticalSection and LeaveCriticalSection. In a great deal of the standard literature on the topic of synchronization, these operations are also known as WAIT and SIGNAL, or LOCK and UNLOCK respectively. These alternative terms are also used for other synchronization primitives, and have roughly equivalent meanings. By default, when the critical section is created, none of the threads in the application have ownership of it. To gain ownership, a thread makes a call to EnterCriticalSection, and if the critical section is not owned, then this thread gains ownership. The thread then typically performs operations upon a shared resource (the critical piece of code, illustrated by a double line), and once it has finished, it releases ownership via a call to LeaveCriticalSection.

The important property that critical sections have is that only one thread may have ownership at any one time. If a thread tries to enter a critical section when another thread is already inside the critical section, then it will be suspended, and only reawoken when the other thread has left the critical section. This provides us with the required mutual exclusion around a shared resource. More than one thread can be suspended waiting for ownership at one time, so critical sections can be used for synchronization between more than two threads. By way of an example, here's what happens if four threads try to gain access to the same critical section at roughly similar times.
As the diagram makes clear, only one thread is ever executing critical code at once, so there are no race conditions or atomicity problems.

So what does this all mean to the Delphi programmer?

This means that, provided one is not performing VCL operations, but only doing simple data transfers, the Delphi thread programmer is free of the burdens of TThread.Synchronise.
  • The VCL thread need not be idle before the worker thread modifies shared resources, it only needs to be out of the critical section.
  • Critical sections neither know nor care about whether a thread is the main VCL thread, or an instance of a TThread object, so one can use critical sections between any two threads.
  • The thread programmer can now (almost) safely use WaitFor, removing the deadlock problem.
My last point is qualified because it is still possible to produce deadlock in exactly the same manner as before. All one has to do is to call WaitFor in the main thread when currently inside a critical section. As we shall see later on, suspending threads for any large amount of time whilst in critical sections is normally a bad idea. Now that the theory has been adequately explained, I'll present another example. This one is a slightly more elegant and interesting prime number program. When started, it tries to find prime numbers starting at 2, and works upwards. Every time it finds a prime number, it updates a shared data structure (a string list), and informs the main thread that it has added more data to the string list. Here is the code for the main form.

It is fairly similar to previous examples with respect to thread creation, but there are a few extra members of the main form which have to be set up. StringSection is the critical section which controls access to resources shared between threads. FStringBuf is a string list which acts as a buffer between the main form and the worker thread. The worker thread sends results to the main form by adding them to this string list, which is the only shared resource in this program. Finally we have a boolean variable, FStringSectInit. This variable serves as a check, ensuring that the required synchronization objects are in fact created before being used. The shared resources are created when we start a worker thread, and destroyed shortly after we are sure that the worker thread has quit. Note that since the string list acting as a buffer is dynamically allocated, we must use WaitFor at thread destruction time to ensure that the worker thread has stopped using the buffer before freeing it.

We can use WaitFor in this program with no worries about deadlock, because we can prove that there is never a situation where both threads are waiting for each other. The line of reasoning required to prove this is simple.

  1. The worker thread only waits when trying to gain access to the critical section.
  2. The main program thread only waits when waiting for the worker thread to finish.
  3. The main program does not wait when it has ownership of the critical section.
  4. If the worker thread is waiting for the critical section, the main program will release the critical section before it ever waits for the worker thread.
Here is the code for the worker thread. The worker thread iterates through successive positive integers, trying to find one that is prime. When it finds one, it gets ownership of the critical section, modifies the buffer, leaves the critical section, and then posts a message to the main form, indicating that there is data in the buffer.

Points of interest.

This example is more complicated than previous examples, because we have an arbitrarily large buffer between two threads, and as a result, there are various problems that have to be considered and avoided, as well as some features of the code that deal with unusual situations. These points can be summarized:
  • Can data be lost or "frozen" in the buffer?
  • What about "out of date" messages?
  • Flow control issues.
  • Inefficiencies in the string list, static vs dynamic sizing.

Can data be lost or "frozen" in the buffer?

The worker thread indicates to the main program thread that there is data to be processed in the buffer by posting it a message. It is important to note when using windows messaging in this manner that there is nothing inherent in the thread synchronization which ties up a particular message with a particular update to the buffer. Luckily, in this case, the rules of cause and effect work in our favour: When the buffer is updated, a message is sent after the update. This means that the main program thread always receives buffer update messages after a buffer update. Hence, it is impossible for data to remain in the buffer for an indeterminate amount of time. If data is currently in the buffer, then the worker thread and the main thread are somewhere in the process of sending or receiving a buffer update message. Note that if the worker thread posted a message just before updating the buffer, it might be possible for the main thread to process that message, reading the buffer before the worker thread had updated the buffer with the most recent result, meaning that the most recent result could remain stuck in the buffer for some time.

What about "out of date" messages?

The laws of cause and effect worked well in the previous case, but unfortunately, the converse problem also holds. If the main thread is busy updating for a long time, it is possible for messages to build up in its queue, such that we receive updates a long time after the worker thread sent those messages. In most situations, this does not pose a problem. However, the one particular case that needs to be considered is the case where the user has stopped the worker thread, either directly by pressing the "stop" button, or indirectly by closing the program. In this case, it is entirely possible for the main VCL thread to terminate the worker thread, remove all synchronization and buffer objects, and then subsequently receive messages which have been sitting in a queue for some time. In the example shown, I have checked for this problem by ensuring that the critical section and buffer objects still exist before processing the message (The line of code commented Not necessarily the case!). This approach tends to be sufficient for most applications.

Flow Control issues and list inefficiencies.

Back in Chapter 2, I stated that once a thread had been created, no implicit synchronization existed between them. This has been evident in early examples, as demonstrated by the problems that thread switching can cause, a manifestation of a positional synchronization problem. The same problem exists for rate synchronization. There is nothing in the above example that guarantees that the worker thread will produce results sufficiently slowly for the main VCL thread to be able to keep up when displaying them. In fact, if the program is executed such that the worker thread starts out by looking for small prime numbers, it is quite likely that, given equal shares of CPU time, the worker thread will outpace the VCL thread by quite a large margin. This problem is solved by some means of flow control.

Flow control is the name given to the process by which the speed of execution of several threads is balanced so that the rate of input into a buffer and the rate of output are smoothly balanced. The example above is particularly simple, but it occurs in many other cases. Almost every I/O or data transfer mechanism between threads or processes incorporates flow control in some manner. In simple cases, this may simply involve only allowing one outstanding piece of data in transit, and suspending either the producer (the thread that puts data into the buffer) or the consumer (the thread that takes it out). In more complex cases, the threads may be executing on different machines, and the "buffer" may be a composite of internal buffers on those machines, and the buffering capabilities of the network in between them. A large part of the TCP protocol is that of managing flow control. Every time you download a web page, the TCP protocol arbitrates between the two machines, ensuring that whatever the relative CPU or disk speed, all data transfers occur at a rate that both machines can handle[1]. In the case of our example above, a fairly crude attempt at flow control has been made. The thread priority of the worker thread has been set so that the scheduler will select the main VCL thread in preference to the worker thread whenever both have work to do. Under Win32 schedulers, this removes the problem, but it is not really a cast iron guarantee.

Another issue which relates to flow control is that in the case of the example above, the size of the buffer is unbounded. Firstly, this creates an inefficiency problem, in that the main thread has to perform a large number of memory moves when removing the first member of a large string list, and secondly, this means that with the flow control mentioned above, the buffer might grow without limit. Try removing the statement that sets the thread priority. You will find that the worker thread generates results faster than the VCL thread can process them, which makes the string list large. This then slows down the VCL thread even more (since the string removal operations take longer on a larger list), and the problem gets even worse. What you will eventually find is that the list gets large enough to fill up main memory, the machine starts thrashing, and everything grinds to a halt. So much to a halt in fact, that when testing the example, I was unable to get Delphi to respond to my request to quit the app, and had to resort to using the Windows NT task manager to close the process!

Simple though this program seemed at first sight, it has raised a large number of potential gremlins. More robust solutions to these problems are discussed in the second part of this guide.

Mutexes.

The reader might be forgiven for thinking that I had spent so long explaining critical sections that I had altogether forgotten about mutexes. Rest assured that this is not the case, it is merely that mutexes present no new conceptual challenges. A mutex works in exactly the same way as a critical section. The only difference in Win32 implementations is that the critical section is limited to being used within only one process. If you have a single program that uses several threads, then the critical section is lightweight, and adequate for your needs. However, when writing a DLL, it is often possible that several different processes will be using the DLL at the same time. In this case, you must use mutexes instead of critical sections. Although the Win32 API provides a more comprehensive range of functions for dealing with Mutexes and other synchronization objects than will be explained here, the following functions are analogous to those listed for critical sections above:
  • CreateMutex / OpenMutex
  • CloseHandle
  • WaitForSingleObject(Ex)
  • ReleaseMutex
These functions are well documented in the Win32 API help files, and will be discussed in more detail later on.

[1] The TCP protocol also performs many other weird and wonderful functions, such as coping with lost data, and optimizing window sizes so that the flow of data is not only matched to the two end point machines but also the network in between, whilst minimizing latency and maximizing throughput. It also contains back-off algorithms to ensure that several TCP connections can share one physical connection without any of them monopolizing the physical resource.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 7. Mutex programming guidelines. Concurrency control.

In this chapter:

  • Time to introduce a little style
  • Deadlock due to mutex ordering.
  • Avoiding thread deadlock by letting a wait time out.
  • Avoiding thread deadlock by imposing an ordering on mutex acquisition.
  • Out of the frying pan and into the fire!
  • Avoiding thread deadlock the "slacker way" by letting Win32 do it for you
  • Atomicity of composite operations - optimistic versus pessimistic concurrency control.
  • Optimistic concurrency control.
  • Pessimistic concurrency control.
  • Avoiding holes in the locking scheme.
  • Confused yet? You can throw in the towel!

Time to introduce a little style?

Most of the examples presented so far in this tutorial have been fairly rough and ready. When designing reusable components, or the framework for a large multithreaded application, a "fly-by-night" approach is not appropriate. The application writer or component designer needs to construct classes that have thread safety built in, that is, classes which assume that they may be accessed from different threads, and contain appropriate internal mechanisms to ensure that data is kept consistent. In order to do this, the component designer needs to be aware of some problems which crop up when using mutexes in increasingly complicated applications. If you are attempting to write thread safe classes for the first time, don't be put off by the apparent complexity of some of the issues in this chapter. Quite often a simplistic solution can be adopted, which avoids many of the issues mentioned in this chapter, at the cost of some efficiency. Note that text which mentions "mutexes" from here on applies equally well to critical sections; I omit mentioning the latter in every case merely for brevity.

Deadlock due to mutex ordering.

If a program contains more than one mutex, then it is surprisingly easy to deadlock a program with sloppy synchronization code. The most common situation is where a cyclic dependency exists with the order in which mutexes are acquired. This is often known in the academic literature as the dining philosophers problem. As we have seen before the criteria for deadlock is that all the threads are waiting for another thread to release a synchronization object. The simplest example of this is between two threads, one of which acquires mutex A before acquiring mutex B, and the other of which acquires mutex B before acquiring mutex A.
Of course, it is entirely possible to deadlock a program in more subtle ways with a cyclic chain of dependencies, such as the one illustrated below with four threads, and four mutexes, A to D.
Obviously, situations like this are not acceptable in most applications. There are several ways of removing this problem, and a couple of techniques which alleviate dependency problems of this sort, making deadlock removal that much easier.

Avoiding thread deadlock by letting a wait time out.

The Win32 functions dealing with mutexes do not require that a thread wait for ever to acquire a mutex object. The WaitForSingleObject function lets one specify a time that the thread is prepared to wait for. After this time has expired, the thread will be unblocked, and the call will return an error code indicating that the wait timed out. When using mutexes to enforce access around a critical region of code, one does not normally expect the thread to have to wait very long, and a time-out set in terms of a few seconds may well be adequate. If your thread uses this method, then it must of course cope with failure in a sensible way, perhaps by retrying, or giving up. Of course, users of critical sections do not have this luxury, since the critical section wait functions wait for ever.

Avoiding thread deadlock by imposing an ordering on mutex acquisition.

Although it's a good idea to be able to cope with failure to acquire a mutex, it makes good sense to ensure that deadlock situations do not happen in the first place. Since this sort of deadlock is caused by cyclic dependencies, it can be eliminated by imposing an ordering on the acquisition of mutexes. This ordering is very simple. Lets say that we have a program with mutexes M1, M2, M3, ... Mn, where one or more of these mutexes may be acquired by the threads in a program.
  • Deadlock will not occur provided that for some arbitrary mutex Mx, threads only try to acquire mutex Mx if they do not currently have ownership of any mutexes of "higher priority", that is M(x+1) ... Mn.
Sound a bit abstract? Let's take a fairly simple concrete example. In this part of the chapter I refer to "locking" and "unlocking" objects. This terminology seems appropriate when a mutex is associated with a piece of data, where atomic access to that data is required. One should note that this effectively means that each thread gains ownership (acquires) the mutex before accessing an object, and gives up ownership of (releases) the mutex after accessing it: the operations are identical to those previously discussed, the only change is in terminology, which at this juncture, is more appropriate to an OO model. In this sense, Object.Lock can be considered completely equivalent to EnterCriticalSection(Object.CriticalSection) or perhaps WaitForSingleObject(Object.Mutex,INFINITE).
We have a list data structure which is accessed by several threads. Hanging off the list are some objects, each one of which has its own mutex. For the moment, we will assume that the list structure is static, does not change, and can thus freely be read by threads without any locking. Threads operating on this data structure want to do one of several things:
  • Read an item by locking it, reading the data, and then unlocking it.
  • Write to an item by locking it, writing the data and then unlocking it.
  • Compare two items, by looking them up in the list, locking both items, and then performing the comparison.
Simple pseudocode for these functions, ignoring typecasts, exception handling, and other such non-central issues, might look something like this.

Let's imagine for a moment that a thread is told to compare items X and Y in the list. If the thread always locks X then Y, then a deadlock may occur if one thread is told to compare items 1 and 2, and another thread is told to compare items 2 and 1. One simple solution is to always lock the lowest numbered item first, or to sort the input indexes, perform the locking, and adjust the results of the comparison appropriately. However, a more interesting situation is where an object contains details of another object that comparison is required with. In this situation, a thread may lock the first object, get the index of the second object in the list, find that it is lower on down the list, lock it, and then proceed. All very easy. The problem occurs when the second object is higher up in the list than the first object. We cannot lock it immediately, since to do so would invite deadlock. What we must do is unlock the first object, lock the second object, and then relock the first object. This ensures that deadlock will not occur. Here is an example indirect comparison routine representative of this approach.

Out of the frying pan and into the fire!

Although this avoids deadlock, it opens another can of worms. In the delay between unlocking and relocking the first object, we cannot be sure that another thread has not modified the first object behind our back. This is because we have performed a composite operation: the entire operation is no longer atomic. Solutions to this are discussed further down the page.

Avoiding thread deadlock the "slacker way" by letting Win32 do it for you.

Mindful of the mental gymnastics that these problems can present, those lovely OS designers at Microsoft have provided another way of solving this problem via another Win32 synchronization function: WaitForMultipleObjects(Ex). This function lets the programmer wait for and acquire multiple synchronization objects (including Mutexes) at once. In particular, it lets a thread wait until one or all of a set of objects are signalled (in the case of mutexes this is equivalent to "not owned"), and then acquire ownership of the signalled objects. This has the great advantage that if two threads wait for mutexes A and B, it does not matter which order they specify them in the set of objects to wait for, either none of the objects will be acquired, or they will all be acquired atomically, so it is impossible to cause deadlock in this manner.

This approach also has several disadvantages. The first disadvantage is that since all the synchronization objects have to be signalled before any of them are acquired, it is possible that a thread waiting for a large number of objects will not acquire ownership for a long time if other threads are acquiring the same synchronization objects on a one by one basis. For example, in the diagram below, the leftmost thread performs a wait for mutexes A, B and C, whilst the other three threads acquire each mutex individually. In the worst case, the thread waiting for multiple objects might never get ownership at all

The second disadvantage is that it is still possible to fall into the deadlock trap, this time not with single mutexes, but with sets of several mutexes at once! It should be plain to see that the following diagram illustrates a situation that will result in deadlock as surely as the example presented at the beginning of this chapter.

The third disadvantage which this approach has, in common with the "time out" method of deadlock avoidance is that it's not possible to use these functions if you are using critical sections; the EnterCriticalSection function does not allow you to specify a timeout value, nor does it return an error code.

Atomicity of composite operations - optimistic versus pessimistic concurrency control.

When thinking about mutex ordering earlier, we encountered a situation where it was necessary to unlock and subsequently relock an object in order to respect mutex ordering. This means that several operations have occurred on an object, and the lock on that object has been released in between operations.

Optimistic concurrency control.

One way of dealing with the problem is to assume that this sort of thread interference is unlikely to occur, and to simply cross check for the problem and return an error if it does happen. This is often a valid way of dealing with the problem in complex situations where the "loading" of a data structure by various threads is not too high. In the case presented earlier, we can trivially check for this by keeping a local copy of the data, and checking that it is still valid after relocking both objects in the required order. Here is the modified routine.

With more complicated data structures, one may sometimes resort to globally unique ID's or version stamps on pieces of data. On a personal note, I recall working with a group of other students on a final year university project where this approach worked very well: a sequence number was incremented whenever a piece of data was changed (in this case the data consisted of appointments in a multi-user diary). The data was locked during reading, subsequently displayed to the user, and if the user edited the data, the number was compared with that obtained by the user on the last read, and the update abandoned if the numbers did not match.

Pessimistic concurrency control.

We might take a slightly different approach to this problem by considering that the list itself is likely to be modified and thus requires its own lock. All operations which read from or write to the list, including lookups, must lock the list first. This then provides an alternative solution to the problem of cleanly locking several objects in the list. Let's review the operations we will want to perform again, with an eye on the slightly modified locking scheme.

A thread may want to read and modify the contents of an object hanging off the list, but without modifying the existence of the object or its placement in the list. This operation takes a long time, and we don't want to impede other threads who may want to deal with other objects, so the thread modifying the object must perform the following operations.

  • Lock the list.
  • Look up the object in the list.
  • Lock the object.
  • Unlock the list.
  • Perform the operation on the object.
  • Unlock the object.
This is great because even if the read or write operation on the object takes a long time, it will not have the list locked for that time, and thus will not delay other threads who may want to modify other objects.

A thread may delete an object by performing this algorithm:

  • Lock the list.
  • Lock the object.
  • Remove the object from the list.
  • Unlock the list.
  • Delete the object (subject to possible restrictions on deleting a mutex that is locked).
Note that it is possible to unlock the list before finally deleting the object, since we have removed the object from the list, and we know that no other operations on the object or the list are in progress (having both locks).

Here comes the interesting part. A thread can compare two objects by performing an algorithm simpler than ne one mentioned in the previous section:

  • Lock the list.
  • Look up the first object.
  • Lock the first object.
  • Look up the second object.
  • Lock the second object.
  • Unlock the list.
  • Perform the comparison.
  • Unlock both objects (in any order).
Note that in the comparison operation, I haven't stated any restrictions on the order in which the locks on objects are acquired. Does this create a deadlock? The algorithms presented do not need the criteria for avoiding deadlock presented at the start of this chapter, yet deadlock never occurs. It never occurs because when a thread locks an object mutex, it already has ownership of the list mutex, and thus provided it locks several objects without releasing the list mutexes, the composite locking of several objects appears atomic. As a result of this, we can qualify the deadlock criteria above:
  • Deadlock will not occur provided that for some arbitrary mutex Mx, threads only try to acquire mutex Mx if they do not currently have ownership of any mutexes of "higher priority", that is M(x+1) ... Mn.
  • Additionally deadlock will not occur if Mutexes are acquired out of order (breaking the above criteria), and for any group of mutexes involved in an out of order acquisition, all sets of locking operations on those mutexes are made atomic, normally by enclosing the locking operations inside a critical section (obtained by locking another mutex).

Avoiding "holes" in the locking scheme.

It is worth noting at this point that the example above is typical of locking code that is very sensitive to ordering. Above all this should indicate that when devising non-trivial locking schemes, great care must be taken in the ordering of events.

If you can ensure that your program will only run under Windows NT (or 2K), then the Windows API does in fact provide a solution to the composite operation problem when unlocking and relocking objects. The SignalObjectAndWait API call allows you to atomically signal (or release) one synchronization object, and wait for another. By keeping these two operations atomic, you can transfer a locked state from one object to another, whilst also ensuring that no other threads have modified the state of the objects during the transfer. This means that optimistic concurrency control is not necessary in such situations.

Confused yet? You can throw in the towel!

If you've managed to keep reading up to this point, then congratulations, you've achieved a basic understanding of the problems which give multithreaded application writers considerable food for thought. It is useful to stress that complicated locking schemes on internal data structures are often only necessary for high performance systems. Small desktop applications can often get away with less complicated approaches. There are a couple of ways of "throwing in the towel".
  • Don't worry about efficiency, and lock the lot.
  • Throw all the data into the BDE.
Locking all shared data is often fine if one is willing to sacrifice some efficiency. Most users prefer a program that runs slightly slowly to one that crashes at unpredictable intervals due to a subtle error in the locking scheme. If one has large amounts of data that need to be persistent in some manner, then putting all the data into the BDE is another approach. All (half way decent) database engines are inherently thread safe, which means that you can access your data with no problems from separate threads. If you do use a database engine, then you may have to learn a little about transaction management, i.e.. reservation, and the usage of prepare, commit and rollback semantics, but take to heart that this is just the transaction based approach to solving concurrency problems, and simply a different side to the same coin; most of the hard coding and head scratching has been done for you. Using the BDE with threads is covered later.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 8. Delphi thread safe classes and Priorities.

In this chapter:

  • Why write thread safe classes?
  • Types of thread safe classes.
  • Thread safe encapsulations or derivatives of existing classes.
  • Data flow management classes.
  • Monitors.
  • Interlock classes.
  • Thread support in the VCL.
  • TThreadList
  • TSychroObject
  • TCriticalSection
  • TEvent and TSimpleEvent
  • TMultiReadExclusiveWriteSynchroniser.
  • Guidelines for thread safe class writers.
  • Priority management.
  • What's in a priority? How Win32 does it.
  • What priority should I make my thread?

Why write thread safe classes?

Simple Delphi applications written by newcomers to thread writing tend to include their synchronization as part of the application logic. As the previous chapter has demonstrated, it is incredibly easy to make subtle errors in synchronization logic, and designing a separate synchronization scheme for each application is a lot of work. A relatively small number of synchronization mechanisms are used time and again: Almost all I/O bound threads communicate data via shared buffers, and the use of lists and queues with inbuilt synchronization in I/O situations is very common. These factors indicate that there is much to be gained by building a library of thread safe objects and data structures: the problems involved in thread communication are hard, but a small number of stock solutions covers almost all cases.

Sometimes it is necessary to write a thread safe class because no other approach is permissible. Code in DLL's which accesses system unique data must contain thread synchronization, even if the DLL contains no thread objects. Given that most Delphi programmers will use the facilities in the language (classes) to allow for modular development and re-use of code, these DLL's will contain classes, and these classes must be thread safe. Some may be fairly simple, perhaps instances of common buffer classes as described earlier. However, it is just as likely that some of these classes may implement resource locking or other synchronization mechanisms in a totally unique way to solve a particular problem.

Types of thread safe classes.

Classes come in many different shapes and sizes, programmers with reasonable Delphi experience will be aware that the class concept is used in many different ways. Some classes are used primarily as data structures, others as wrappers to simplify a complex underlying structure. Sometimes families of co-operating classes are used to provide flexibility when achieving an overall goal, as is well demonstrated in the Delphi streaming mechanism. When it comes to thread safe classes, a similar diversity is present. In some cases the classifications can get a little blurred, but nonetheless, four distinct types of thread safe class can be discerned.

Thread safe encapsulations or derivatives of existing classes.

These are the simplest type of multithreaded class. Typically the class being enhanced has a fairly limited functionality and is self contained. In the simplest case, making the class thread safe may simply consist of adding a mutex, and two extra member functions, Lock and Unlock. Alternatively, the functions manipulating data in the class may perform the locking and unlocking operations automatically. Which approach is used depends to a large extent on the number of possible operations on the object, and the likelihood that the programmer is going to use manual locking functions to enforce atomicity of composite operations.

Data flow management classes.

These are a slight enhancement to the above type, and tend to consist of buffer classes: lists, stacks and queues. In addition to maintaining atomicity, these classes may perform automatic flow control on the threads operating on the buffer. This often consists of suspending threads which try to read from an empty buffer, or write to a full one. Implementing these classes is discussed more fully in chapter 10. A range of operations may be supported by the one class: at one end of the scale, completely non-blocking operations will be provided, and on the other end, all operations may be blocking if they cannot successfully complete. A middle ground is often provided where operations are asynchronous, but provide call-back or messaging notifications when a previously failed operation is likely to succeed. The Win32 sockets API is a good example of a data flow interface which implements all of the above options as far as thread flow control is concerned.

Monitors.

Monitors are a logical step onwards from data flow management classes. They typically allow concurrent access to data which requires more complex synchronization and locking than a simple thread safe encapsulation of an existing Delphi class. Database engines fall into the higher end of this category: typically a complicated locking and transaction management scheme is provided to allow a large degree of concurrency when accessing shared data, with a minimal performance loss due to thread conflicts. Database engines are a slightly special case in that they use transaction management to allow a fine degree of control over composite operations, and they also provide guarantees about the persistency of operations that run to completion. Another good example of a monitor is that of a filing system. The Win32 filing systems allow multiple threads to access multiple files which may be opened by several different processes in several different modes at once. A large part of a good filing system consists of handle management and locking schemes which provide optimal performance, whilst ensuring that atomicity and persistence of operations is preserved. In layman's speak: "Everyone can have their fingers in the filing system, but it guarantees that no operations will conflict, and once an operation has completed, it is guaranteed to be persistently held on disk". In particular, the NTFS filing system is "log based", so it is guaranteed to be consistent, even in the event of a power failure or operating system crash.

Interlock classes.

Interlock classes are unique in this classification, because they do not contain any data. Some locking mechanisms are useful in that the code that comprises the locking system can be easily separated from the code that manipulates shared data. The best example of this is the Delphi "Multiple reader single writer interlock" class, which enables shared read and atomic write operations on a resource. The operation of this will be examined below, and the internal workings of the class will be examined in a later chapter.

Thread support in the VCL.

In Delphi 2, no classes were provided to assist the multi-threaded programmer, all synchronization was done on a strictly "roll your own" basis. Since then, the state of the VCL has improved in this respect. I will discuss those classes found in Delphi 4, since that happens to be the version available to me. Delphi 3 and 2 users may find that some of these classes are not present, and Delphi 5(+) users may find various extensions to these classes. In this chapter I present a brief introduction to these classes and their usage. Note that on the whole, many of the inbuilt Delphi classes are not terribly useful: they offer very little added value over the mechanisms available in the Win32 API.

TThreadList

As has been mentioned before, lists, stacks and queues are very common when implementing communication between threads. The TThreadList class performs the most basic sort of synchronization required between threads. In addition to all the methods presented in TList, two extra methods are provided: Lock and Unlock. The usage of these should be fairly obvious to readers who have managed to work through the previous couple of chapters: The list is locked before being manipulated, and unlocked afterwards. If a thread performs multiple operations on the list that are required to be atomic, then the list must remain locked. The list does not perform any implicit synchronization on objects which are owned by a particular list. The programmer may wish to devise extra locking mechanisms to provide this ability, or alternatively, use the lock on the list to cover all operations on data structures owned by the list.

TSychroObject

This class provides a couple of virtual methods, Acquire and Release which are used in all the basic Delphi synchronization classes, given the underlying reality that most simple synchronization objects have a concept of ownership as previously discussed. The critical section and event classes are derived from this class.

TCriticalSection

This class needs no detailed explanation. I suspect its inclusion in Delphi is simply for the use of those Delphi programmers with a phobia of the Win32 API. It is worth noting that it provides four methods: Acquire, Release, Enter and Leave. The latter two do nothing but call the former two, just in case an application programmer prefers one set of nomenclature over another.

TEvent and TSimpleEvent

Events are a slightly different way of looking at synchronization. Instead of enforcing mutual exclusion they are used to make a variable number of threads wait upon an occurrence, and then release one or all of those threads when that occurrence occurs. TSimpleEvent is a particular case of an event, which specifies various defaults most likely to be used in Delphi applications. Events are closely related to semaphores, and are discussed in a later chapter.

TMultiReadExclusiveWriteSynchroniser.

This synchronization object is useful in situations where a large number of threads may need to read a shared resource, but that resource is written to relatively infrequently. In these situations, it is often not necessary to completely lock a resource. In earlier chapters I stated that any unsychronized use of shared resources was likely to lead to thread conflicts. While this is true, it does not necessarily follow that a complete mutual exclusion is always required. A complete mutual exclusion insists that only one thread is performing any operation at any one time. We can relax this if we realize that there are two main types of thread conflict:
  • Write after Read
  • Write after Write
Write after Read conflicts occur when one thread writes to a part of a resource after another thread has read that value, and assumed it to be valid. This is the type of conflict illustrated in chapter three. The other type of conflict occurs when two threads write to the shared resource, one after the other without the later thread being aware of the earlier write. This results in the first write being obliterated. Of course, some operations are perfectly legal: Read after Read, and Read after Write. These two operations occur all the time in singly threaded programs! This seems to indicate that we can slightly relax the criteria for data consistency. The minimum criteria in general are:
  • Several threads may read at once.
  • Only one thread can be writing at once.
  • If a thread is writing, then no threads can be reading.
The MREW synchronizer enforces these criteria by providing four functions: BeginRead, BeginWrite, EndRead and EndWrite. By calling these before and after writes, the appropriate synchronization is achieved. As far as the application programmer is concerned, he can view it as a much like a critical section, with the exception that threads acquire it for reading or writing.

Guidelines for thread safe class writers.

Although later chapters cover the details of thread safe class writing, and the various benefits and pitfalls that can be made when designing thread safe classes, it seems worthwhile to include a few simple tips which go a long way.
  • Who does the locking?
  • Be economical when locking resources.
  • Allow for failure.
The responsibility for locking a thread safe class can either be given to the class writer, or the class user. If a class provides only simple functionality, it is normally best to give this responsibility to the class user. They are likely to use several instances of such classes, and by giving them responsibility for locking, one ensures that unexpected deadlocks do not occur, and one also gives them the choice concerning how much locking is performed, in order to maximize either simplicity or efficiency. For more complicated classes, such as monitors, it is normal for the class (or set of classes) to assume responsibility, thus hiding the complexities of object locking from the end user of the class.

On the whole, resources should be locked as little as reasonably possible, and resource locking should be fine grained. Although simplistic locking schemes reduce the chances of a subtle bug being inserted into the code, they can to a large extent negate the performance benefits of using threads in the first place. Of course, there is nothing wrong with starting simple, but if performance problems occur, then the locking scheme should be examined in more detail.

Nothing ever works perfectly all the time. If using Win32 API calls, allow for failure. If you are the sort of programmer who is happy checking myriad error codes, then this is a workable approach. Alternatively, you may wish to write a wrapper class, which encapsulates the Win32 synchronization object capability whilst raising exceptions when errors occur. In any case, always think carefully about the use of try ... finally blocks to ensure that in the event of failure, the synchronization objects are left in a sensible state.

Priority management.

All threads are created equal, but some are more equal than others. The scheduler has to divide CPU time between all the threads running on the machine at any moment. In order to do this, it needs to have some idea about how much CPU usage each thread is likely to use, and how important it is that a particular thread is executed when it is able to run. Most threads behave in one of two ways: their execution time is either CPU bound or I/O bound.

CPU bound threads tend to perform long number crunching operations in the background. They will soak up all the CPU resources available to them, and will rarely be suspended in order to wait for I/O or communications with other threads. Quite often, their execution time is not critical. For example, a thread in a computer graphics program may perform a long image manipulation operation (blurring or rotating the image), which might take a few seconds or even minutes. On the time scale of processor cycles, this thread never needs to be run urgently, since the user is not bothered whether the operation takes twelve or thirteen seconds to execute, and no other threads in the system are urgently waiting for a result from this thread.

At the other end of the time scale we have I/O bound threads. These normally do not take up large amounts of CPU, and may consist of relatively small amounts of processing. They are quite often suspended (blocked) on I/O, and when they receive input, they typically run for a short time to process that particular input, and are almost immediately suspended again when no more input is available. An example of this is the thread that processes mouse movement operations and updates the mouse cursor. Every time the mouse is moved, the thread spends a very small fraction of a second updating the cursor, and is then suspended. Threads of this sort tend to be much more time critical: they are not run for long periods of time, but when they are run, it is fairly critical that they are run immediately. In most GUI systems, it is unacceptable to make the mouse cursor unresponsive, even for fairly short periods of time, and hence the mouse update thread is fairly time critical. WinNT users will notice that even when the computer is working very hard on CPU intensive operations, the mouse cursor still reacts immediately. All pre-emptive multitasking operating systems, Win32 included, provide support for these concepts by allowing the programmer to assign "priorities" to threads. Typically, threads with higher priorities tend to be I/O bound, and threads with lower priorities tend to be CPU bound. The Win32 implementation of thread priorities is slightly different from (for example) the UNIX implementation, so the details discussed here are specific to Win32 only.

What's in a priority? How Win32 does it.

Most operating systems assign a priority to threads to determine which thread should receive how much CPU attention. In Win32, the actual priority of each thread is calculated on the fly, from a number of factors, some of which can be directly set by the programmer, and some of which can not. These factors are the process Priority Class, the thread Priority Level, which together are used to calculate the thread Base Priority, and the Priority Boost in effect for that thread. The process priority class is set on a per running process basis. For almost all Delphi applications, this will be the normal priority class, with the exception of screen savers, which can be set to the idle priority class. On the whole, the Delphi programmer will not need to modify the priority class of the running process. The priority level of each thread can then be set within the class assigned for the process. This tends to be much more useful, and the Delphi programmer can use the SetThreadPriority API call to change the priority level of a thread. The allowed values for this call are: THREAD_PRIORITY_HIGHEST, THREAD_PRIORITY_ABOVE_NORMAL, THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_BELOW_NORMAL, THREAD_PRIORITY_LOWEST and THREAD_PRIORITY_IDLE. Since the actual thread base priority is calculated as a result of both thread priority level and process priority class, threads with priority level above normal in a process with normal priority class will have a greater base priority than threads with above normal priority level in a process with below normal priority class. Once the base priority for a thread has been calculated, this level stays fixed for the duration of the thread, or until its priority level (or the class of the owning process) is changed. However, the actual priority used from one second to the next in the scheduler varies slightly from this value, as a result of Priority Boost.

Priority Boost is a mechanism that the scheduler uses to try and take account of the behaviour of the threads at run time. Few threads will be totally CPU or I/O bound during their entire lifetime, and the scheduler will boost the priority of threads that block without using an entire time slice. In addition, threads that own window handles which are currently foreground windows are also given a slight boost to try and improve user responsiveness.

What priority should I make my thread?

With a basic understanding of priorities, we can now attempt to assign useful priority levels to the threads in our application. Bear in mind that by default, the VCL thread executes at normal priority level. Generally, most Delphi applications are focussed on providing as much user responsiveness as possible, so one rarely needs to increase the priority of a thread above normal - doing so will delay operations such as window repainting whenever the thread is executing. Most threads that deal with I/O or data transfer in Delphi applications can be left at normal priority, since the scheduler will boost the thread if need be, and if the thread turns out to be a bit of a CPU hog, then it will not be boosted, thus resulting in reasonable speed operations in the main VCL thread. Conversely, lowering priorities can be very useful. If you lower the priority of a thread performing CPU intensive background processing, the machine appears to the user to be very much more responsive than it would if that thread were left at the normal priority. Typically, the user is then far more likely to tolerate a slight delay in the completion of the low priority thread: they can get on with other tasks, and the machine and application still remain as responsive as normal.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 9. Semaphores. Data flow scheduling. The producer - consumer relationship.

In this chapter:

  • Semaphores.
  • What about counts above one? "Not so critical" sections
  • A new use for semaphores: Data flow scheduling and Flow control
  • The bounded buffer.
  • A Delphi implementation of the bounded buffer.
  • Creation: Initializing the semaphore counts correctly.
  • Operation: correct wait values
  • Destruction: Cleaning up.
  • Destruction: The subtleties continue.
  • Access to synchronization handles must be synchronized!
  • Win32 handle management
  • A solution
  • Using the bounded buffer: an example.
  • A few final points...

Semaphores.

A Semaphore is another type of synchronization primitive, which is slightly more general than the mutex. Used in the most simple manner possible it can be made to operate in exactly the same way as a mutex. In the general case, it allows a program to implement more advanced synchronization behaviour.

First, let's reconsider the behaviour of mutexes. A mutex can either be signalled or non-signalled. If it is signalled, then a wait operation on the mutex does not block. If it is non-signalled, a wait operation on that mutex blocks. If the mutex is non-signalled, then it is owned by a particular thread, and hence only one thread can own the mutex at any one time.

Semaphores can be made to act in precisely the same manner. Instead of having the concept of ownership, a semaphore has a count. When that count is greater than 0, the semaphore is signalled, and wait operations on it do not block. When the count is 0, then the semaphore is not signalled, and wait operations on it will block. A mutex is essentially a special case of a semaphore whose count is only 0 or 1. Similarly, semaphores can be thought of as fancy mutexes which can have more than one owner at once. The functions in the Win32 API dealing with semaphores are very similar to those for dealing with mutexes.

  • CreateSemaphore. This function is similar to CreateMutex. Instead of a flag indicating whether the thread creating the mutex requests initial ownership, this function takes an argument specifying the initial count. Creating the mutex with initial ownership is similar to creating a semaphore with a count of 0: in both cases, any other threads waiting on the object will block. Likewise, creating the mutex without initial ownership is similar to creating a semaphore with a count of 1: in both cases, one and only one other thread will not be blocked when it waits for the synchronization object.
  • Wait Functions. The wait functions are identical in both cases. With mutexes, a successful wait gives a thread ownership of the mutex. With semaphores, a successful wait decreases the count of the semaphore, or if the count is 0, it blocks the calling thread.
  • ReleaseSemaphore. This is similar to ReleaseMutex, but instead of the thread releasing ownership of the object, ReleaseSemaphore takes an extra integer as an argument to specify how much the count should be increased by. ReleaseSemaphore either increases the count in the semaphore, or wakes up the appropriate number of threads blocked on that semaphore or both.
The following table shows how code using mutexes can be converted into code using semaphores, and the equivalencies between the two.
Mutexes. | Semaphores.
MyMutex := CreateMutex(nil,FALSE,<name>); | MySemaphore := CreateSemaphore(nil,1,1,<name>);
MyMutex := CreateMutex(nil,TRUE,<name>); | MySemaphore := CreateSemaphore(nil,0,1,<name>);
WaitForSingleObject(MyMutex,INFINITE); | WaitForSingleObject(MySemaphore,INFINITE);
ReleaseMutex(MyMutex); | ReleaseSemaphore(MySemaphore,1);
CloseHandle(MyMutex); | CloseHandle(MySemaphore);

As a simple example, here are the modifications required to the code presented in chapter 6, in order to get the program to use semaphores instead of critical sections.

What about counts above one? "Not so critical" sections.

Allowing semaphores to have counts greater than one is somewhat analogous to allowing mutexes to have more than one owner. Thus semaphores allow critical sections to be created which allow a certain number of threads into one particular region of code, or access to a particular object. This is mostly useful in situations where a shared resource consists of a number of buffers or a number of threads, which can be utilized by other threads in the system. Let's take a concrete example, and assume that up to three threads can be present in a particular region of code. A semaphore is created with initial and maximum counts of 3, assuming that no threads are present in the critical region. The execution of five threads trying to access the shared resource might look something like this:

This particular application of semaphores is probably not particularly useful to Delphi programmers mainly because there are so few statically sized structures at the application level. However, it finds considerably more utility inside the OS, where handles, or resources such a filing system buffers are likely to be statically allocated at boot time.

A new use for semaphores: Data flow scheduling and Flow control.

In Chapter 6, the need for flow control when passing data between threads was outlined. Again, in Chapter 8, this subject was alluded to when discussing monitors. This chapter outlines an example situation where flow control is often required: A bounded buffer with a single producer thread putting items into the buffer, and a single consumer thread taking items out.

The bounded buffer.

The bounded buffer is representative of a simple shared data structure which provides flow control as well as shared data. The buffer considered here will be a simple queue: First In, First Out. It will be implemented as a cyclic buffer, that is, it contains a fixed number of entries and has a couple of pointers "get" and "put" to indicated where data should be inserted and removed in the buffer. There are typically four operations allowed on the buffer:
  • Create Buffer. The buffer and any associated synchronization mechanisms are created and initialized.
  • Put Item. This attempts to put an item into the buffer in a thread safe manner. If this is not possible because the buffer is full, then the thread attempting to put an item into the buffer is blocked (suspended) until the buffer is in a state which allows more data to be added.
  • Get Item. This attempts to get an item out of the buffer in a thread safe manner. If this is not possible because the buffer is empty, then the thread attempting to get an item is blocked until the buffer is in a state which allows more data to be removed.
  • Destroy Buffer. This unblocks all threads waiting in the buffer, and destroys the buffer.
Obviously, mutexes will be required when manipulating shared data. However, we can use semaphores to perform the required blocking operations when the buffer is full or empty, eliminating the need for range checking, or even keeping a count of how many items there are in the buffer. In order to do this a small change in mindset is required. Instead of waiting for a semaphore and then releasing it when performing operations related to the buffer, we use the count on a pair of semaphores to keep track of how many entries in the buffer are empty or full. Let us call those semaphores "EntriesFree" and "EntriesUsed".

Normally, two threads interact with the buffer. The producer (or writer) thread attempts to put entries into the buffer, and the consumer (reader) thread attempts to take them out, as represented in the following diagram. A third thread (possibly the VCL thread) may intervene in order to create and destroy the buffer.

As you can see, the reader and writer threads execute in a loop. The writer thread produces an item, and attempts to put it into the buffer. First the thread executes a wait on the EntriesFree semaphore. If the count on EntriesFree is zero, then the thread will be blocked, since the buffer is full and no data can be added to the buffer. Once it gets past this potential wait, it adds an item to the buffer, and then signals EntriesUsed, thus incrementing the count on Entries used, and if necessary, waking up the consumer thread. Likewise, the consumer thread will block if the count of EntriesUsed is 0, but when it gets round to removing an item, it will increment the count on EntriesFree, allowing the producer thread to add another item.

By blocking the appropriate thread whenever the buffer becomes full or empty, this stops one or other of the threads "running away". Given a buffer size of N, the producer thread can only be N items ahead of the consumer thread before it will be suspended, and likewise, the consumer thread cannot be more than N items behind. This brings several benefits:

  • One thread cannot over produce, thus avoiding the problems seen in Chapter 6, where we had the output of one thread queuing up into a list of ever increasing size.
  • The buffer is of finite size, unlike the list based approach seen earlier, so we can place a worst case limit on memory usage.
  • There are no "busy waits". When a thread has no work to do, it is sleeping. This avoids situations where application writers write small loops that do nothing but wait for more data without blocking. This is to be avoided, as it wastes CPU time.
Just to make this absolutely clear, I'll provide an example sequence of events. Here we have a buffer which has 4 possible entries in it, and it is initialized so all the entries are free. Many possible execution paths are possible, depending on the whim of the scheduler, but I will illustrate the path where each thread executes for as long as possible before it is suspended.
Reader thread action | Writer thread action | Entries free count | Entries used count
Thread starts | Thread inactive (not scheduled) | 4 | 0
Wait(EntriesUsed) blocks. Suspended. |  | 4 | 0
 | Wait(EntriesFree) flows through | 3 | 0
 | Item Added. Signal(EntriesUsed) | 3 | 1
 | Wait(EntriesFree) flows through | 2 | 1
 | Item Added. Signal(EntriesUsed) | 2 | 2
 | Wait(EntriesFree) flows through | 1 | 2
 | Item Added. Signal(EntriesUsed) | 1 | 3
 | Wait(EntriesFree) flows through | 0 | 3
 | Item Added. Signal(EntriesUsed) | 0 | 4
 | Wait(EntriesFree) blocks. Suspended | 0 | 4
Wait(EntriesUsed) completes |  | 0 | 3
Item Removed. Signal(EntriesFree) |  | 1 | 3
Wait(EntriesUsed) flows through |  | 1 | 2
Item Removed. Signal(EntriesFree) |  | 2 | 2
Wait(EntriesUsed) flows through |  | 2 | 1
Item Removed. Signal(EntriesFree) |  | 3 | 1
Wait(EntriesUsed) flows through |  | 3 | 0
Item Removed. Signal(EntriesFree) |  | 4 | 0
Wait(EntriesUsed) blocks. Suspended |  | 4 | 0

A Delphi implementation of the bounded buffer.

Here is a first shot Delphi implementation of the bounded buffer. As normal, the implementation brings up a few points which bear a mention, and it has a few problems which will be resolved later.
  • What values should be supplied to the semaphore creation calls?
  • How long should the wait on the mutexes or critical sections be?
  • How long should the wait on the semaphores be?
  • What is the best way of cleanly destroying the buffer?

Creation: Initializing the semaphore counts correctly.

With this implementation of the bounded buffer, the data is stored as an array of pointers with read and write indexes into this array. To aid debugging, I have arranged that if the buffer contains N entries, it will be declared as full when N-1 entries have been filled. This is most often performed with cyclic buffers where the read and write indexes are evaluated to determine whether the buffer is full or not. If the buffer is empty, the read and write indexes are the same. Unfortunately, this is also the case if the buffer is absolutely full, so it is common in cyclic buffer code to always have one entry in the cyclic buffer empty so that these two conditions can be distinguished. In our case, since we are using semaphores, this is not strictly necessary. However, I have kept with this convention in order to aid debugging.

With this in mind, we can initialize the EntriesUsed semaphore to 0. Since there are no used entries, we want reader threads to immediately block. Given that we want the writer threads to add at most N-1 items to the buffer, we then initialize EntriesFree to N-1.

We also need to consider the maximum count allowed on the semaphores. The procedure which destroys the buffer always performs a SIGNAL operation upon both semaphores. So, given that when the buffer is destroyed, it might have any number of items in it, including completely full and completely empty, we set the maximum count to N, thus allowing one signal operation on the semaphores given all possible buffer states.

Operation: correct wait values.

I have used mutexes instead of critical sections in this piece of software because they allow the software developer finer control over error situations. In addition, they also allow a time-out. The time out on wait operations for semaphores should really be infinite; It is possible that the buffer might remain full or empty for long periods of time, and we need the thread that is blocked for as long as the buffer is empty. Paranoid or insecure programmers might like a time-out of a few seconds on these primitives, to allow for unforeseen error situations where a thread becomes blocked permanently. I am sufficiently confident of my code to deem this unnecessary, at least for the moment...

The time-out on the mutex is a completely different kettle of fish. The operations inside the critical section are fast; up to N memory writes, and provided N is set fairly small (i.e. less than millions) these operations should not take more than 5 seconds. As an added bonus, part of the cleanup code acquires this mutex, and instead of releasing it, closes the handle. By setting a time-out, this ensures that threads waiting on the mutex get unblocked, and return failure.

Destruction: Cleaning up.

By now, most readers will have deduced that clean-up operations are often the hardest part of multithreaded programming. The bounded buffer is no exception. The procedure ResetState performs the cleanup. The first thing it does is check FBufInit. I have assumed that this does not require synchronized access, since the thread that creates the buffer should also destroy it, and since it is only written to by one thread, and all writes occur in a critical section (at least after creation), no conflicts will occur. The clean up routine now needs to ensure that all state is destroyed, and that any threads currently waiting, or in the process of reading or writing exit cleanly, reporting failure if appropriate.

The clean up first acquires the mutex for the shared data in the buffer, and then unblocks the reader and writer thread by releasing both semaphores. The operations are done in this order because when both semaphores are released, the buffer state is no longer consistent: the count of the semaphores does not agree with the contents of the buffer. By acquiring the mutex first, we can destroy the buffer before unblocked threads get to read it. By destroying the buffer, and setting FBufInit to false, we can ensure that unblocked threads return failure, instead of operating on garbage data.

We then unblock both threads by releasing both semaphores, and we then close all the synchronization handles. We then destroy the mutex without releasing it. This is OK, because since all wait operations on the mutex time out, we can be sure that both reader and writer threads will unblock eventually. In addition, since there is only one reader and writer thread, we can guarantee that no other threads have attempted to wait on the semaphores during this process. This means that one signal operation on both semaphores was sufficient to wake all the threads up, and since we destroy the semaphore handles while we have ownership of the mutex, any further write or read operations are bound to fail when they try to wait on one of the semaphores.

Destruction: The subtleties continue.

This code is only guaranteed to work with one reader thread, one writer thread and one control thread. Why?
If more than one reader or writer thread exists, more than one thread might be waiting on one of the semaphores at any one time. Hence we might not wake all the waiting readers or writers up when we reset the buffer state. A programmer's first reaction to this might be to modify the clean up routine to continue signalling one or other of the semaphores until all the threads have been unblocked, by doing something like this. Unfortunately, this is still insufficient, because one of the repeat loops in the clean up might finish just before yet another thread enters a read or write operation and waits upon a semaphore. Obviously we desire some sort of atomicity, but we can't wrap the semaphore operations in a critical section, because threads blocking on a semaphore will take hold of the critical section, and all the threads will deadlock.

Access to synchronization handles must be synchronized!

The next possibility might be to zero out the handle of the semaphore shortly before "unwinding" it, by doing something like this. (The author will be honest here, and admit that this pathetic non-solution crossed his mind.) However, this is no better. Instead of having a deadlock problem, we have introduced a thread conflict of a subtle kind. This particular conflict is a write after read conflict on the handle of a semaphore itself! Yes... you even have to synchronize your synchronization objects! What can potentially happen is, a worker thread reads the value of the mutex handle from the buffer object, and is suspended before making the wait call, at which point the clean-up thread destroying the buffer signals the mutex the required number of times, just in time for the worker thread to be scheduled, and promptly perform a wait operation on the mutex we thought had just been cleared! The window in which this can happen is very small, but nonetheless, this is not an acceptable solution.

Win32 handle management.

This problem is sufficiently knotty that it's worth reviewing exactly what happens when we issue a Win32 call to close a mutex or a semaphore. In particular, it is useful to know:
  • Does closing the handle unblock threads waiting on that particular mutex or semaphore?
  • In the case of mutexes, does it make any difference whether one owns the handle when releasing the mutex?
In order to determine this, we can use two test applications, A mutex testapp, and a semaphore testapp. From these applications, it can be determined that when closing a handle to a synchronization object, Win32 does not unblock any threads waiting on that object. This is most likely a by product of the reference counting mechanism that Win32 uses to keep track of handles: threads waiting on a synchronization object may well keep the internal reference count from reaching 0, and by closing the application's handle to the object, all we do is remove any semblance of control that we had over that synchronization object. In our situation, this is a real pain. Ideally, when cleaning up one would hope that an attempt to wait on a closed handle would unblock threads waiting on that synchronization object via that particular handle. This would then allow the application programmer to enter a critical section, clean up the data in that critical section, and then close the handle, thus unblocking the threads waiting on it with an appropriate error value (perhaps WAIT_ABANDONED?).

A solution.

As a result of all this, we have determined that closing handles is fine, provided that threads do not perform an indefinite wait on the handle. When applying this to the bounded buffer, at clean up time, we can guarantee to unblock all threads waiting on semaphores only if we know how many threads there are waiting on the mutexes. In the general case, we need to ensure that threads do not perform an infinite wait on the mutexes. Here is a rewritten buffer that copes with an arbitrary number of threads. In it, the wait functions on the semaphores have been modified, and the clean up routine has also undergone some small changes.

Instead of performing an infinite wait on the appropriate mutex, the reader and writer threads now call a "Controlled Wait" function. In this function, each of the threads waits on the semaphore for only a finite amount of time. This wait for the semaphore can return one of three values, as documented in the Win32 help file.

  • WAIT_OBJECT_0 (Success).
  • WAIT_ABANDONED
  • WAIT_TIMEOUT
Firstly, if the semaphore is released, the function returns success, and no further actions are required.. Secondly, in the case where the Win32 WaitFor function returns wait abandoned, the function returns error; this particular error value indicates that a thread has quit without properly releasing a synchronization object. The case that we are most interested in is where the wait times out. This can be for one of two possible reasons:
  • The thread might be blocked for a long time.
  • The internals of the buffer have been destroyed without waking up that particular thread.
In order to check for this, we attempt to enter a critical section and check that the buffer initialized variable is still true. If either of these operations fail, then we know that internals of the buffer have been reset, and the function quits, returning an error. If both these operations succeed, then we go back round the loop, waiting on the mutex again. This function guarantees that when the buffer is reset, blocked threads will eventually time out, and return an error to the calling thread.

The clean up routine has also been slightly modified. It now signals both semaphores, and releases the critical section mutex. By doing this, it guarantees that the first reader and writer thread will be unblocked immediately whenever the buffer state is reset. Of course, additional threads may still have to wait for up to the default time-out specified before quitting.

Using the bounded buffer: an example.

In order to provide a framework for this example, a simple application using two threads was devised. This application searches for palindromic prime numbers. A pair of palindromic primes exist where two numbers, X and Y are both prime, and Y is the palindrome of X. Neither X nor Y need be a palindromic number in itself, although if one of them is, then X = Y, a special case. Examples of palindromic primes include: (101, 101), (131, 131), which are both special cases and (16127, 72161) , (15737, 73751) and (15683, 38651), which are not.

In essence, the two threads (source here) perform slightly different tasks. The first thread (the "forward" thread) searches for prime numbers. When it finds one, it puts it into a bounded buffer. The second thread waits of entries in the bounded buffer. When it finds an entry, it removes the entry, reverses the digits, checks whether the reversed number is prime, and if this is the case, it sends a text string containing the two numbers to the main form (source here).

Although there is quite a lot of code, there is very little new to be discussed. The reader would be advised to have a look at the execute methods of each thread, since these provide the clearest overview of what is happening. The transfer of data from the second thread to the VCL thread, and corresponding main form is as discussed in previous chapters. The one point left to make concerns... you guessed it! Resource deallocation and clean up.

A few final points...

And you thought there couldn't possibly be anything else left to say about destruction? There is one final caveat to be mentioned. The bounded buffer code assumes that threads may attempt to access fields in the object after the buffer has been reset. This is fine, but it means that when destroying the two threads, and the buffer in between them, we must reset the buffer state, then wait for all the threads to terminate, and only then actually free the buffer, thus deallocating the memory containing the object itself. Failure to do this may result in an access violation. The StopThreads function correctly performs this, assuring a clean exit.

It is also worth noting at this point that an additional synchronisation issue exists with the SetSize procedure. In the example, I have assumed that the buffer size is set, once and for all, before any threads use the buffer. It is possible for the buffer size to be set whilst it is in use. This is generally a bad idea, since it means that if more than two threads are using the buffer; one reader and one writer, they may not correctly detect buffer destruction. If the buffer has to be resized, then all the threads using the buffer should either be terminated or suspended at a known safe point. The buffer should then be resized, and the producer and consumer threads restarted. Ambitious programmers may wish to write an extended version of the buffer which handles resize operations transparently.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 10. I/O and data flow: from blocking to asynchronous and back.

In this chapter.

  • VCL Thread differences, and I/O Interface design.
  • Roadmap.
  • Implementing a blocking to asynchronous conversion
  • Adding peek operations to the bounded buffer.
  • Creating a bi-directional bounded buffer.
  • The Blocking to Asynchronous buffer in detail.
  • Construction of the BAB.
  • Destruction of the BAB.
  • An example program using the BAB.
  • We've achieved our goal!
  • Have you spotted the memory leak?
  • Memory leak avoidance.
  • Peek problems.
  • Doing away with the intermediate buffer.
  • Miscellaneous limitations.
  • The flip side of the coin: Stream buffers.

VCL Thread differences, and I/O Interface design.

With worker threads, it makes sense to make I/O blocking, since on the whole, blocking I/O is simplest. From the point of view of a thread using an I/O resource via blocking calls, success or failure is immediately apparent after making the I/O call, and the program logic never has to worry about the period of time between the I/O operation being invoked and it being completed.

Operations involving the VCL thread are typically not allowed to block for long periods of time: the thread should always be able to process new messages with a minimum of delay. On the whole disk I/O tends to be blocking, since the delays involved are short from the point of view of the end user, but all other I/O operations tend to be asynchronous, especially operations involving communication between threads, processes, or machines, since the length of the delays involved can not be predicted in advance. The benefit of asynchronous operations, as previously discussed, is that the VCL thread always remains responsive to new messages. The main disadvantage is that the code executing in the VCL thread has to be aware of the completion status of all pending I/O operations. This can become quite complicated, involving the storage of potentially large amounts of state. Sometimes this involves constructing a state machine; especially when implementing well defined protocols such as HTTP, FTP or NNTP. More often, the problem is simple, and has to be solved as a one off. In such cases an ad-hoc solution will suffice.

When designing a set of data transfer functions, this difference has to be borne in mind. Taking communications as an example, the most commonly supported generic set of operations on a communications channel are: Open, Close, Read and Write. Blocking I/O interfaces offer these facilities as simple functions. Asynchronous interfaces offer the four basic functions, and in addition, they will provide up to four notifications, whether they be by call back or by event. These notifications indicate either that a previous pending operation has completed, or that it is possible to repeat the operation, or a mixture of both. An example interface might be:

  • An Open function, and an associated OnOpen event, which indicates that the open has completed, and reports success or failure.
  • A Read function, and an associated CanRead (or OnRead) event. The event typically indicates that a call to Read will read some new data, and/or that more data has arrived since the previous Read.
  • A Write function, and an associated CanWrite (or OnWrite) event. The event typically indicates that a call to Write will write more data, and/or that some of the data in the previous Write has been sent, resulting in buffer space available for more Write operations. Depending on the semantics, this event might or might not be triggered after a successful call to Open.
  • A Close function, and an associated OnClose event. The event typically indicates that the communications channel has finally been closed, and that no more data can be sent or received. This event normally exists in situations where it is possible to read data from the far end of a communications channel after calling Close, and tends to work well with communication set up and tear down mechanisms that use a three way handshake (e.g. TCP).

Roadmap.

Before proceeding further on this chapter, it seems appropriate to review the existing mechanism for data transfer between threads, and to outline methods by which this will be extended. If nothing else, it may persuade some readers to complete this chapter without giving up, despite that fact that there is a lot of code to be studied. The most important point to be made at this juncture is that many of the implementation details, whilst useful for those wishing to write a functional program embodying these techniques, are not of prime importance to those wishing to gain a fundamental understanding of the issues described. So far, the only data transfer mechanism we have seen is the bounded buffer, diagrammatically represented thus:
In this chapter various extensions to this buffer will be demonstrated. The first couple of modifications will be fairly simple: to place two buffers back to back, and add a non-blocking peek operation on both sides of the resultant bi-directional buffer.
So far, so good. There should be no real surprises for any readers at this point, and all those who have managed to follow up to this point should have no real trouble implementing such a construction. The next modification is far more ambitious: instead of making all read and write operations on the buffer blocking, we will make one set of operations asynchronous.
Specifically, we will create a component which converts blocking operations into asynchronous ones, and vice versa. In its default incarnation, it will simply encapsulate read and write operations on the bi-directional buffer, but future implementors might like to override this functionality to convert different I/O operations between blocking and asynchronous semantics.
The question here is: Why? The answers should be obvious: If we can make a buffer that provides bi-directional communication between two threads, where one thread uses blocking operations, and the other thread uses asynchronous operations, then:
  • We can use it to communicate between the VCL thread and the worker threads in our application without blocking the VCL thread.
  • All the complexity will be hidden inside the buffer code: no magic message numbers, no use of synchronize, no publicly visible critical sections.
  • It will perform flow control between the VCL thread and worker threads; a task not yet possible.
  • It can be used as an "off the shelf" solution for communication between the VCL thread and other threads by any old idiot who has no idea about synchronization problems.

Implementing a blocking to asynchronous conversion component.

The component we will create assumes that only one VCL thread is running, and consequently, an asynchronous interface will be provided for only one thread. The blocking operations provided by this buffer will operate under exactly the same limitations as those present in the bounded buffer example of the previous chapter, and hence, any number of blocking threads will be able to access the blocking interface concurrently. Just as the bounded buffer allowed simple Get and Put operations involving only one element, the blocking to asynchronous buffer (henceforth called called the BAB) will also allow simple operations involving only one element. The semantics of the interface will be:
  • Creation: Upon creation, the BAB component will create the required internal buffering data structures and threads, and will generate an OnWrite event to indicate that data may be written to the buffer by the VCL thread.
  • Reading: The BAB component will provide two read functions; BlockingRead and AsyncRead. BlockingRead will be used by worker threads, whilst AsyncRead will be used by the VCL thread.
  • Read Notifications: The BAB will provide an OnRead event to the main VCL thread whenever an asynchronous read operation is likely to succeed, i.e., data is waiting to be read by the VCL thread. Since this particular component only deals in reading or writing single pointers it may be assumed that only one item can be read per notification, and the VCL thread should wait for a further notification before attempting to read again.
  • Writing: The BAB will provide two functions; BlockingWrite and AsyncWrite. BlockingWrite will be used by worker threads, whilst AsyncWrite will be used by the VCL thread.
  • Write Notifications: The BAB will provide an OnWrite event to the main VCL thread whenever an asynchronous write operation is likely to succeed, i.e., there is free buffer space into which an item may be written. Again, a one to one relationship holds between notifications and successful writes, and the VCL thread should attempt exactly one write before waiting for another notification.
  • Peek operations: Any thread will be able to peek the buffers to ascertain how many entries are free or used in the buffer in a certain direction. This operation may be useful for worker threads in order to determine whether a BlockingRead or BlockingWrite operation will in fact block. The VCL thread should not use these functions to determine whether a read or write is likely to succeed, and should depend on notifications instead.

Adding peek operations to the bounded buffer.

Here is an improvement to the bounded buffer to allow peek operations. Notice that although it is possible to read the count on semaphores during certain operations, I have chosen to maintain the counts manually using a couple of extra variables FEntryCountFree and FEntryCountUsed. A couple of extra methods have been provided to read these variables. Many Delphi programmers would immediately think of exposing these attributes of the bounded buffer as properties. Unfortunately, we have to bear in mind that the synchronization operations required to access these variables might fail. Rather than return counts of -1 in an integer property, it seems more appropriate to leave the peek operations as functions, thus informing the programmer that some work is required to access the required data, and that the function might fail. Some might argue that given this rationale, it would have been sensible to also code the Size attribute of the buffer as an explicit reader function. This is very much a matter of style, since the size of the buffer can be read directly without any synchronization being required.

Creating a bi-directional bounded buffer.

This operation is almost completely trivial, and requires no complex explanation. I have implemented it as a simple encapsulation of two bounded buffer objects. All the operations supported by the bounded buffer are also supported by the bi-directional bounded buffer, with the small modification that the threads using this object must specify which side of the buffer they wish to deal with. Typically, one thread will deal with side A, and another will deal with side B. Here is the source. This class implements the functionality described pictorially in the diagram above representing the bi-directional bounded buffer.

The Blocking to Asynchronous buffer in detail.

Having done all the preparatory work required, the BAB can now be explained in more detail. The BAB contains a bi-directional buffer, and two threads, one reader and one writer. The reader and writer threads perform read and write operations upon the bounded buffer on behalf of the VCL thread. The execution of all the threads can be represented pictorially, with only minimal abuse of existing conventions:
This diagram looks rather daunting, so it is probably easiest to present a worked example. Let us consider the case where a worker thread performs a blocking write to the BAB.
  1. The worker thread performs a blocking write.
  2. The BAB reader thread is currently blocked trying to read from the Bi-directional buffer. As a result of the write, it becomes unblocked, and performs a successful read.
  3. It copies the data read into an interim buffer local to the thread class, and issues a data flow event, handled by the BAB.
  4. The BAB data flow event handling code, executing in the context of the reader thread, posts a message to its own window handle indicating that data has been read by the reader thread.
  5. The reader thread then waits on a semaphore which will indicate that the data has been read by the main VCL thread.
  6. Some time later, the main VCL thread processes outstanding messages for the component, in the same way as for all components with a window handle.
  7. Amongst those messages waiting for the component is the notification message posted by the reader thread. This message is handled, and generates an OnRead event for the component.
  8. The OnRead event is handled by the logic in the rest of the application (probably the main form), and this will likely result in the VCL thread trying to read data.
  9. The VCL thread calls the AsyncRead method of the BAB.
  10. AsyncRead copies the data out of the interim buffer and returns it to the VCL thread. It then releases the semaphore that the reader thread is blocked on, allowing it to try and perform another read operation on the bi-directional buffer.
The BAB performs in exactly the same manner when writing. The write is performed asynchronously by the VCL thread, the BAB internal writer thread is woken up and performs a blocking write on the bi-directional buffer, and once that write completes, the VCL thread is notified via an event that more write operations may be attempted.

In essence, the interface between blocking and asynchronous operation via message posting is identical to that introduced informally in earlier examples. The difference with this component is that the details are encapsulated from the end user, and the problem is solved in a more formal, well defined manner.

Here is the code for this component. Several points can be profitably noted. On the whole, TThread descendants make little use of inheritance. However, in this particular case, the reader and writer threads have a large amount of common functionality, which is implemented in a base class, TBlockAsyncThread. This class contains:

  • The interim buffer, which holds just a single pointer.
  • A critical section to allow atomic access to the interim buffer.
  • A pointer to the bi-directional buffer to use for blocking operations. This is set by the BAB to the bi-directional buffer used internally in the BAB.
  • An event, "OnDataFlow", which is handled by the BAB component.
  • An idle semaphore. This semaphore is used to implement the "Wait for VCL write" and "Wait for VCL read" operations in a generic manner.
The base thread class also implements a bare minimum of common functionality: Thread creation, destruction, and the event trigger for the OnDataFlow event. The base class has two children: TBAWriterThread and TBAReaderThread. These implement the actual thread execution methods, and they also provide read and write methods which will be indirectly executed by the VCL thread. The BAB component itself stores the bi-directional buffer and the two threads. In addition, it also stores a window handle FHWND, which is used for the specialized message processing.

Construction of the BAB.

Lets now have a look at the implementation. Upon creation, the BAB component allocates a window handle using AllocateHWnd. This is a useful function mentioned in Danny Thorpe's book "Delphi Component Design". The BAB component is rather unusual in that it requires a window handle to perform message processing, but it is not really a visual component. It is possible to give the BAB component a window handle by making it a child of TWinControl. However, this is not really the appropriate parent for the component, because it isn't a window control. By using AllocateHWnd, the component can perform its own message processing without also carrying along a large amount of unneeded extra clutter. There is also a small improvement in efficiency, since the message handling procedure in the component performs only the minimal amount of processing required, dealing with one particular message, and ignoring all others.

During creation, the BAB component also sets up a couple of event handlers from the threads to the component itself. These event handlers execute in the context of the reader and writer threads, and perform the notification posting that interfaces between the reader and writer threads, and the main VCL thread.

As a result of component creation, the threads are set up. All of the work here is common to both reader and writer threads, and is thus in the constructor for TBlockAsyncThread. This simply sets up a critical section required to maintain atomic access to the intermediate buffer in each thread, and it also creates the idle semaphore for each thread, which ensures that the thread waits for the VCL thread to read or write data before proceeding.

Destruction of the BAB.

Destruction of the component is slightly more complicated, but uses principles discussed in previous chapters. The bi-directional buffer contained in the BAB is similar to the bounded buffer discussed in the previous chapter, in that destruction is a three stage process. The first stage is to unblock all threads performing I/O operations on the buffer via a call to ResetState. The second stage is to wait for all threads to terminate, or at least be in a state where they will not perform any more operations on the buffer. Once this condition has been met, the third stage can commence, which is destruction of the physical data structures.

The destruction of the BAB thus works along similar lines:

  • The state for the BAB is reset. This involves terminating both internal threads, and then resetting the state for the bi-directional buffer, thus unblocking any buffer operations in progress.
  • The destructor for both threads is called. This releases each thread's idle semaphore, and then waits for the thread to complete before destroying the critical section and idle semaphore. Some readers may be surprised that a thread destructor can call WaitFor. This is OK, provided that we can be sure that a thread never calls its own destructor. In this case, the destructor for the reader and writer threads will be called by the VCL thread, so there is no deadlock problem here.
  • The reader and writer threads are set to nil to allow multiple calls to ResetState.
  • The bi-directional buffer is destroyed, and the window handle is deallocated.
Since the threads are internal to the BAB, these cleanup procedures have the highly desirable effect that the destructor of the BAB can unblock and clean up all the threads and synchronization objects internal to the component without the component user ever being aware of the potential ordering problems inherent in the cleanup operation. A simple call to Free the BAB will suffice. This is obviously desirable.

Despite this, the component still exposes its ResetState method. The reason for this is that the component has no control over other worker threads that may be performing blocking operations on the buffer. In situations like this, the main application must still terminate the worker threads, reset the BAB state, and wait for the worker threads to terminate before physically destroying the BAB.

An example program using the BAB.

Here is yet another variant on the prime number theme. The main form prompts the user for two numbers - the start and end of a range. These numbers are submitted, are put into a request structure, and a pointer to this structure is asynchronously written into the BAB. At some later point the worker thread performs a blocking read, and retrieves the request. It then spends a variable amount of time processing the request, determining which numbers in the range are prime. Once it has finished, it performs a blocking write, passing a pointer to a string list full of results. The main form is notified that it has data to read, and it then reads the string list back out of the BAB, and copies the results into a memo.

There are two main points to note in the code for the main form. The first is that the user interface is elegantly updated in line with the flow control for the buffer. After a request has been submitted, the request button is disabled. It is only re-enabled when the form receives an OnWrite event from the BAB indicating that more data can safely be written. The current implementation sets the Bi-directional buffer size to 4. This is sufficiently small that the user can verify that after sending four requests that take a long time to process, the button remains permanently disabled until one of the requests has been processed. Likewise, if the main form cannot process read notifications sufficiently rapidly from the BAB, the worker thread will be blocked.

The second point to note is that when the main form is destroyed, the destructor uses the ResetState method of the BAB as described earlier to ensure that thread cleanup and buffer deallocation occurs in an orderly manner. Failure to do this might result in an access violation.

The worker thread code is fairly simple. It is worth noting that since it uses blocking read and write operations, it only uses CPU when it is actively processing a request: if it cannot receive a request or send a reply, due to congestion in the buffer, then it is blocked.

We've achieved our goal!

A small recap of what has been achieved with this component:
  • Seamless data transfer between VCL and worker threads.
  • All synchronization subtleties hidden inside the BAB (with the exception of ResetState subtleties).
  • End to end flow control between VCL and worker threads.
  • No busy loops or polling: CPU is used efficiently.
  • No use of synchronize. Threads are not blocked unnecessarily.
The reader might be forgiven for thinking that his troubles are over...

Have you spotted the memory leak?

Throughout both the previous chapter and this chapter, a major issue has been side-stepped; Items in the various buffers we have designed are not destroyed properly when the buffer is destroyed. When initially designing these buffer structures, an approach similar to a TList was adopted: The list or buffer simply provides storage and synchronization. Correct object allocation and deallocation is the responsibility of the threads using the buffer.

This simplistic approach has major difficulties. In the general case, it is exceedingly hard to ensure that the buffer is empty in both directions before it is destroyed. In the example above, which is the most simple use possible of the buffer, there are four threads, four mutexes or critical sections, and six semaphores in the entire system. Determining the state of all the threads and orchestrating a perfectly clean exit in such situations is obviously not possible.

In the example program this was resolved by keeping a count of how many requests are un-serviced at any one time. If we have received as many replies as requests, then we can be sure that the various buffers are empty.

Memory leak avoidance.

One approach is to allow the various buffers to implement call-backs which destroy the various objects contained in those buffers at cleanup time. This will work in the general case, but it is open to abuse, and the implementation of such a scheme is likely to get messy in practice.

Another possibility is to have a general buffer management scheme which keeps track of specific types of objects, keeping note of when they enter and leave the various buffers in the application. Again, the implementation of this is likely to get rather messy, and would require a potentially complicated reference tracking mechanism to do a job that should really be simple.

The best solution is to make the buffer structures analogous to TObjectList; i.e. all items put into the buffers are classes. This then allows thread performing the clean up operation to call an appropriate destructor on all the items in the buffer. Even better, by using class reference types, we could automatically perform run time type checks on objects passing through the buffer, and produce a type safe set of buffers.

The implementation of a scheme such as this is left as an exercise to the reader. No changes are required to the basic synchronization mechanisms, but the signatures for the read and write procedures will require modification, as will the implementation of the destructors for the bounded buffer, and the thread classes.

Peek problems.

When implementing the bi-directional buffer, it was still possible to provide a reasonably consistent mechanism for peeking buffers to see how many items were in them. It is possible that when peeking the bi-directional buffer, the free and used counts might not always add up to the same figure, since both operations cannot be performed atomically. However, it is guaranteed that with only one reader and writer thread in each direction, peeks can be used as a reasonable indication that an operation would succeed without blocking.

With the asynchronous buffer, the problem is worsened in that it is not possible to obtain a guaranteed good peek on the buffer state with the current implementation. This is because there are essentially two buffers in each direction, the bounded buffer and the interim, single item buffer. No mechanism is provided for globally locking both buffers to atomically determine the status of both of them.

The component does make a stab at providing some peeking ability by providing a rough count of items in transit in the buffers. This is deliberately vague so as not to mislead the programmer into thinking that the results might be accurate! Is is possible to do better than this?

Doing away with the intermediate buffer.

The best way to improve the situation is to remove the intermediate buffer entirely. With a little thought this is in fact possible, but it requires a rewrite of all the buffering code. We would need to implement a new bounded buffer with slightly different semantics. This new bounded buffer would:
  • Implement a blocking read and write on one side as before.
  • On the other side, implement an asynchronous read and write (a simple success/failure without blocking), and in addition implement a couple of "Block Until" methods. These methods would block a thread until a read or write operation could be guaranteed to succeed.
In this manner, the reader and writer threads could be used to send notifications by blocking until an operation was possible, and the VCL thread could perform the actual read and write operations upon the bounded buffer, without blocking.

With these semantics, we then have only one set of buffers that needs to be managed, and it is comparatively easy to provide an atomic peek operation that provides accurate results. Again, this is left as an exercise to the reader...

Miscellaneous limitations.

All the buffering structures introduced in the last couple of chapters have assumed that the programmer is sending pointers to valid memory, and not NIL. Some readers may have noticed that some of the code in the reader and writer threads implicitly assumes that NIL is an appropriate null value which will not be sent through the buffer. This could trivially be remedied with some buffer validity flags, but this is at the expense of cluttering the code somewhat.

Another more theoretical limitation is that the end user of this component might conceivably create a very large number of buffers. The Win32 programming guidelines for threads state that it is normally a good idea to limit the number of threads to about sixteen per application, which would allow eight BAB components. Since there is no limitation of the number of worker threads that perform blocking operations on the BAB, it would seem appropriate to have only one BAB per application and to use it to communicate between the one VCL thread and all the worker threads. This of course assumes that all the worker threads are performing the same job. On the whole, this should be acceptable because most Delphi applications should be spawning just a handful of threads for time consuming background operations.

The flip side of the coin: Stream buffers.

So far, all the buffering structures discussed have implemented buffers of pointers for data transfer. While this is useful for discrete operations, most I/O operations involve streams of data. All the buffering structures have a roughly equivalent counterpart involving streams, which, by and large, can be treated in a similar manner. There are a few useful differences that are worth pointing out:
  • When buffering streams, it is not possible to use semaphores to keep track of a concrete number of items in the buffer. Instead, semaphores are used in a binary fashion, that is with counts of only 1 or 0. When reading or writing with stream buffers, a calculation must be made as to whether the buffer will be filled or emptied by the operation. If one of these occurs, then as many bytes are transferred as possible, and the thread is then blocked if appropriate.
  • Since the blocking status of reader and writer threads is calculated on the fly, state has to kept, recording the blocked or running status of the threads. This state is then used in subsequent read or write operations in order to calculate whether "peer" threads on any particular read or write operation should be unblocked. This complicates the blocking and unblocking calculations somewhat, but the overall principle is the same.
  • Notification schemes for stream buffers are similarly modified. The current notification scheme sends one notification for every read or write. BAB components operating on streams send notifications based on whether the intermediate buffer (or its equivalent) is no longer full or empty. Since notifications can be regarded as the asynchronous equivalent to Signal or ReleaseSemaphore operations, this modification is analogous to the points above.
There is plenty more that might be mentioned on this subject. If the reader wishes to see a worked example of stream buffering, he should consult the code in the final chapter.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 11. Synchronizers and Events.

In this chapter:

  • More synchronization mechanisms.
  • When optimal efficiency is a must.
  • A Simple MREWS.
  • Implementation points to note.
  • An example use of the simple MREWS.
  • An introduction to Events.
  • Event simulation using semaphores.
  • The simple MREWS using events.
  • The Delphi MREWS.

More synchronization mechanisms.

The material introduced in previous chapters has covered all of the basic synchronization mechanisms. On the whole, semaphores and mutexes allow the programmer to create all other synchronization mechanisms, albeit with some effort. Despite this, there are some situations which are very common in multithreaded programming, but not easy to deal with using the mechanisms shown so far. Two new primitives will be introduced to solve these problems: The Multi Read Exclusive Write Synchronizer, and the Event. The former is provided in some versions of Delphi as part of the VCL, and the latter is provided by the Win32 API.

When optimal efficiency is a must.

So far, all operations on a shared value have been mutually exclusive. All read and write operations have been protected to the extent that only one read or one write happens at any one time. However, in many real world situations where a critical resource must be accessed frequently by a large number of threads, this can turn out to be inefficient. Exclusive locking is in fact more cautious than is absolutely necessary. Recalling chapter 6, note that the minimum synchronization required is that:
  • Read operations can execute concurrently.
  • Write operations cannot execute at the same time as read operations.
  • Write operations cannot execute at the same time as write operations.
By allowing an absolute minimum of concurrency control, it is possible to produce a significant increase in performance. The best performance increases are realized when many read operations occur from a relatively large number of threads, write operations are relatively infrequent, and only a small number of threads perform writes.

These conditions hold in numerous real world situations. For example, the stock database for a company may contain a large number of items, and numerous reads may occur in order to calculate the availability of certain goods. However, the database is only updated when items are actually ordered or shipped. Similarly, membership records may be checked many times in order to find addresses, send out mailings and check subscriptions, but members join, leave or change their addresses relatively infrequently. The same holds in computing situations: master lists of global resources in a program may be read often, but written infrequently. The required level of concurrency control is provided by a primitive known as the Multiple Read Exclusive Write Synchronizer, henceforth referred to as an MREWS.

Most synchronizers support four main operations: StartRead, StartWrite, EndRead and EndWrite. A thread calls StartRead on a particular synchronizer when it wishes to read the shared resource. It will then perform one or more read operations, all of which are guaranteed to be atomic and consistent. Once it has finished reading, it calls EndRead. If two read operations are performed between a given pair of calls to StartRead and EndRead, the data obtained in those two reads is always consistent: no write operations will have occurred between the calls to StartRead and EndRead.

Likewise, when performing a series of write operations, a thread will call StartWrite. It may then perform one or more write operations, and it can be sure that all write operations are atomic. After the write operations, the thread will call EndWrite. The write operations will not be overwritten by other writers, and no readers will read inconsistent results due to the write operations in progress.

A Simple MREWS.

There are several ways of implementing an MREWS. The VCL contains a fairly sophisticated implementation. In order to familiarize the user with the basic principles, here is a simpler but slightly less functional implementation using semaphores. The simple MREWS contains the following items:
  • A critical section to guard shared data access (DataLock).
  • An integer count of the number of active readers (ActRead).
  • An integer count of the number of reading readers (ReadRead).
  • An integer count of the number of active writers (ActWrite).
  • An integer count of the number of writing writers (WriteWrite).
  • A pair of semaphores, known as the Reader and Writer semaphores (ReaderSem and WriterSem).
  • A critical section to enforce complete write exclusion (WriteLock).
The reading and writing can be summarized thus:

There are two stages in reading or writing. The first is the active stage, where a thread indicates its intent to read or write. Once this has occurred, the thread may be blocked, depending on whether there are other read or write operations in progress. When it becomes unblocked, it enters the second stage, performs the read or write operations, and then releases the resource, setting the counts of active and reading readers or writers to appropriate values. If it is the last active reader or writer, it unblocks all threads which were previously blocked as a result of the operation that the thread was performing (read or write). The following diagram illustrates this in more detail.

At this point, an implementation of this particular breed of synchronizer should be obvious. Here it is. If at this point the reader is still confused, then don't panic! This synchronization object is not easily understood at first sight! Stare at for a few minutes, and if you start seeing double before you understand it, then don't worry about it, and move on!

Implementation points to note.

There is an asymmetry in the synchronization scheme: threads potentially wanting to read will block before reading if there are any active writers, whilst threads wanting to write block before writing if there are any reading readers. This gives priority to writing threads; a sensible approach, given that writes are less frequent than reads. This need not necessarily be the case, and since all calculations about whether a thread is to be blocked or not occur in the critical section, it is perfectly allowable to make the synchronizer symmetrical. The downside to this is that, if many concurrent read operations occur, they may prevent writes from occurring at all. Of course, the opposite situation, with many writes stopping read operations is always the case.

Also worth noting is the use of semaphores when acquiring the resource for reading or writing: Wait operations on semaphores must always be performed outside the critical section that guards the shared data. Thus the conditional signalling of a semaphore inside the critical section is purely to ensure that the resulting wait operation does not block.

An example use of the simple MREWS.

In order to demonstrate what the MREWS does, it is necessary to digress slightly from the examples presented so far. Imagine that it is necessary for a large number of threads to keep track of the status of a number of files in a certain directory. These threads want to know if a file has changed since the thread last accessed that file. Unfortunately, the files can be changed by a number of different programs on the system, so it is not possible for a single program to keep track of the various file operations being performed on all the files.

This example has a worker thread which iterates through all the files in a directory, calculating a simple checksum for each file. It does this over and over, effectively ad infinitum. The data is stored in a list which contains an MREW synchronizer, thus allowing a large number of reader threads to read the checksums on one or more files.

First, let's look at the source for the checksum list. Here it is. The basic operations are:

  • Set the checksum for a particular file. This adds an entry for the file into the list if it does not exist.
  • Get the checksum for a particular file. This returns 0 if the file is not found.
  • Remove a file from the list.
  • Get a string list of all the filenames.
  • Get a string list of all the file names followed by their checksums.
All these publicly accessible operations have appropriate synchronization calls at the start and end of the operation.

Note that there are a couple of methods which start with the name "NoLock". The methods are methods which need to be invoked from more than one publicly visible method. The class has been written this way because of a limitation of our current synchronizer: Nested calls to start reading or writing are not allowed. All operations which use the simple synchronizer must only call StartRead or StartWrite if they have ended all previous read or write operations. This will be discussed in more detail later. Apart from this, most of the code for the checksum list is fairly mundane, consisting mostly of list handling, and should present no surprises for most Delphi programmers.

Now lets look at the worker thread code. This thread looks slightly different from most example threads that I have presented so far because it is implemented as a state machine. The execute method simply executes an action function for each state, and depending on the return value of the function, looks up the next state required in a transition table. One action function reads the list of files in from the checksum list object, the second removes unnecessary checksums from the list, and the third calculates the checksum for a particular file, and updates it if necessary. The beauty of using a state machine is that it makes thread termination a lot cleaner. The execute method calls the action functions, looks up the next state and checks for thread termination in a while loop. Since each action function normally takes a couple of seconds to complete, thread termination is normally fairly fast. In addition, only one test for termination is necessary in the code, making the code cleaner. I also like the fact that the entire state machine logic is implemented in one line of code. There is a certain neatness to it all.

Finally, we'll take a look at the code for the main form. This is relatively simple: the thread and checksum list are created at start-up, and destroyed when the program is closed. The list of files and their checksums is displayed on a regular basis as the result of a timer. The directory which is watched is hard coded in this file; readers wishing to run the program may wish to change this directory, or possibly modify the program so that it can be specified at program start-up.

This program does not perform operations on shared data in a strictly atomic manner. There are several places in the update thread where local data is implicitly assumed to be correct, when the underlying file may have been modified. A good example of this is in the thread "check file" function. Once the file checksum has been calculated, the thread reads the stored checksum for that file, and updates it if it does not agree with the current calculated checksum. These two operations are not atomic, since multiple calls to the checksum list object are not atomic. This mainly stems from the fact that nested synchronization calls do not work with our simple synchronizer. One possible solution is to give the checksum list object two new methods: "Lock for Reading" and "Lock for Writing". A lock could be acquired on the shared data, either for reading or writing, and the multiple read or write operations performed. However, this still does not solve all the possible synchronization problems. More advanced solutions will be discussed later on in this chapter.

Since the inner workings of the synchronizer occur at the Delphi level, it is possible to obtain an estimate of how often thread conflicts actually occur. By placing a breakpoint in the while loops of the EndRead and EndWrite procedures, the program will be stopped if a reader or writer thread was blocked whilst trying to access the resource. The breakpoint actually occurs when the waiting thread is unblocked, but an accurate count of conflicts can be made. In the example program, these conflicts are quite rare, especially under low load, but if the number of files and checksums becomes large, conflicts are increasingly common, since more time is spent accessing and copying shared data.

An introduction to Events.

Events are perhaps one of the simplest synchronization primitives to understand, but an explanation of them has been left to this point, simply because they are best used in conjunction with other synchronization primitives. There are two types of events: manual reset events and auto reset events. For the moment, we will consider manual reset events. An event works exactly like a traffic light (or stop light for U.S. readers). It has two possible states: signalled (analogous to a green traffic light) or non-signalled (analogous to a red traffic light). When the event is signalled, threads performing a wait on the event are not blocked and continue execution. When the event is non-signalled, threads performing a wait on the event are blocked until the event is signalled. The Win32 API provides a range of functions for dealing with events.
  • CreateEvent / OpenEvent: These functions are similar to the other Win32 functions for creating or opening synchronization objects. As well as allowing the event to be created in either a signalled or non-signalled state, a boolean flag states whether the event is a manual reset or auto reset event.
  • SetEvent: This sets the event state to signalled, thus resuming all threads that are waiting on the event, and allowing later threads to pass through without blocking.
  • ResetEvent: This sets the event state to non-signalled, thus blocking all threads that subsequently perform a wait on the event.
  • PulseEvent: This performs a set-reset on the event. Hence, all threads waiting on the event when the event is pulsed are resumed, but later threads waiting on the event still block.
Auto reset events are a special case of manual reset events. With an auto reset event, the state of a signalled event is set back to non-signalled once exactly one thread has passed through the event without blocking, or one thread has been released. In this sense, they work in an almost identical manner to semaphores, and if a programmer is using auto reset events, they should consider using semaphores instead, in order to make the behaviour of the synchronization mechanism more obvious.

Event simulation using semaphores.

An event primitive can in fact be created by using semaphores: It is possible to use a semaphore to conditionally block all threads waiting on the event primitive and unblock threads when the primitive is signalled. In order to do this, a very similar approach to the synchronizer algorithm is used. The event keeps two pieces of state: a boolean indicating whether the event is signalled or not, and a count of the number of threads currently blocked on the semaphore in the event. Here's how the operations are implemented:
  • CreateEvent: The event object is created, the count of blocked threads is set to zero, and the signal state is set as specified in the constructor.
  • SetEvent: The signal state is set to not block incoming threads. In addition, the count of threads blocked on the semaphore is examined, and if it above zero, then the semaphore is signalled repeatedly until all blocked threads are unblocked.
  • ResetEvent: The signal state is set to block incoming threads.
  • PulseEvent: All threads currently blocked on the semaphore are unblocked, but no change is made to the signal state.
  • WaitForEvent: The signal state of the event is examined. If it indicates that the event is signalled, then the internal semaphore is signalled, and the count of threads blocked on the semaphore is decremented. The count of blocked threads is then incremented, and a wait is performed on the internal semaphore.
Here is the code for a simulated event using semaphores. If the reader has understood the simple synchronizer, then this code should be fairly self explanatory. The implementation could be slightly simplified by replacing the while loops that unblock threads with a single statement that increments the count on the semaphore by the required amount, however the approach implemented is more consistent with the implementation of the synchronizer presented previously.

The simple MREWS using events.

The control structures required to simulate an event using semaphores are remarkably similar to the structures used in the simple synchronizer. Thus it seems sensible to try and create a synchronizer using events instead of semaphores. This isn't particularly difficult: here it is. As normal, the conversion raises a couple of implementation issues worth looking at.

First and foremost, the simple synchronizer calculated whether threads should be blocked in the critical section part of the StartRead and StartWrite procedures, and then performed the required blocking actions outside the critical section. The same is necessary for our new event synchronizer. In order to do this, we assign a value to a local variable called "Block" (remember, local variables are thread safe). This is done inside the DataLock critical section, to guarantee consistent results, and the blocking actions are performed outside the critical section to avoid deadlock.

Secondly, this particular synchronizer is symmetric, and affords read and write operations equal priority. Unfortunately, since there is only one set of counts in this synchronizer, it is rather more difficult to make it asymmetric.

The Delphi MREWS.

The main problem with the existing synchronizers is that they are not re-entrant. It is completely impossible to nest calls to StartWrite, and an instant deadlock will occur. It is possible to nest calls to StartRead, provided that no threads call StartWrite in the middle of a sequence of nested calls to StartRead. Again, if this occurs, deadlock will be an inevitable consequence. Ideally, we would like to be able to nest both read and write operations. If a thread is an active reader, then repeated calls to StartRead should have no effect, provided they are matched by an equal number of calls to EndRead. Similarly, nested calls to StartWrite should also be possible, and all but the outer pair of StartWrite and EndWrite calls should have no effect.

The second problem is that the synchronizers illustrated so far do not allow atomic read-modify-write operations. Ideally, it should be possible for a single thread to call: StartRead, StartWrite, EndWrite, EndRead; thus allowing a value to be read, modified and written atomically. Other threads should not be allowed to write in any part of the sequence, and they should not be allowed to read during the inner write part of the sequence. With current synchronizers, it is perfectly possible to do this by simply performing the read and write operations inside a pair of calls to StartWrite and EndWrite. However, if the synchronization calls are embedded in a shared data object (as in the example) it can be very difficult to provide a convenient interface to that object that allows read-modify-write operations without also providing separate synchronization calls to lock the object for reading or writing.

In order to do this, an altogether more sophisticated implementation is required, whereby every start and end operation looks at exactly which threads are currently performing read or write operations. This is in fact what the Delphi synchronizer does. Unfortunately, licensing agreements mean that it is not possible to display the VCL source code here and discuss exactly what it does. However, suffice to say that the Delphi MREWS:

  • Allows nested read operations.
  • Does not allow nested write operations.
  • Allows Read operations to be promoted to write operations, allowing read-modify-write operations to be done with minimal locking at each stage of the proceedings.
  • Is written very much for efficiency: Critical sections are used only where absolutely necessary, and interlocked operations are preferred. This obfuscates the code a little, but the increase in efficiency is more than worthwhile.
  • Can be swapped with the synchronizer classes presented above with no change in semantics.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 12. More Win32 synchronization facilities.

In this chapter:

  • Greater efficiency via interlocked operations.
  • Atomicity from nothing.
  • Eventcounts and sequencers.
  • Other Win32 synchronization facilities.

Greater efficiency via interlocked operations.

Conventional synchronization primitives can be a considerable overhead in simple multithreaded systems, particularly for threads that are tightly synchronized to each other. One possible alternative is to use interlocked operations.

Interlocked operations were originally conceived as a low level synchronization mechanism for shared memory symmetric multiprocessor systems. In multiprocessor systems, shared memory is an extremely efficient way to transfer data between processes and threads. A way had to be found to prevent atomicity problems when two or more processors tried to use the same piece of memory. Almost all processors introduced recently support interlocked operations to allow this. These are operations whereby a processor can read a value from memory, modify it and then write it back atomically, whilst ensuring that no other processors access the same memory, and the processor performing the operation is not interrupted. Win32 provides the following interlocked operations:

  • InterlockedCompareExchange (Win NT/2K only).
  • InterlockedDecrement.
  • InterlockedExchange.
  • InterlockedExchangeAdd (Win NT/2K only).
  • InterlockedIncrement.
So why would one use interlocked operations at all? One good example is that of a spin lock. Occasionally one wishes to create something similar to a critical section. However, there may be very little code in the critical section, and the code in the critical section may be accessed very often. In cases such as this, a full blown synchronization object may prove to be overkill. The spin lock allows us to do a similar thing, and it works like this. A thread acquires the lock if, when performing an interlocked increment, it finds that, after the increment, the value of the lock is 0. If it finds that the value is greater than 0, then another thread has the lock, and it goes around for another try. The call to sleep is included so that one thread does not spin for long periods on the lock whilst a lower priority thread has the lock. In simple schedulers, if the thread priorities are equal, then the call to sleep may not be required. The interlocked operation is necessary, because if a thread performed a memory read, increment, compare and then write back, then two threads could acquire the lock simultaneously.

Overhead is reduced because just a couple of CPU instructions are required to enter and leave the lock, provided a thread does not have to wait. If threads have to wait for any appreciable time, then CPU is wasted, so they are only useful for implementing small critical sections. Spin locks are useful when enforcing critical sections that are themselves part of synchronization structures. Shared data inside synchronization primitives or schedulers is often protected by locks of this sort: the locks are often necessary because OS level synchronization primitives cannot be used to implement OS level synchronization primitives. Spin locks have all the same concurrency problems as mutexes, with the particular quirk that cyclic acquisition results not in deadlock, but in livelock. This is a slightly worse situation than deadlock because although the "blocked" threads are not executing any useful code, they are running around an infinite loop, using up CPU and degrading the performance of the entire system. Spin locks should not be used as semaphores to "suspend" a thread.

Atomicity from nothing.

With care, it is in fact possible to create a spin lock that is atomic without assuming any interlocking at all, provided that interrupts only occur in between CPU instructions. Consider this. Lets look at the pascal first to get the general idea. We have an integer lock in memory. When trying to enter the lock, we first increment the lock in memory. We then read the value from memory into a local variable, and check, as before to see whether it is greater than zero. If it is, then someone else has the lock, and we go round again, otherwise, we have the lock.

The important thing about this set of operations is that, given certain provisos, a thread switch can occur at any point in time, and this still remains thread safe. The first increment of the lock is a straight register indirect increment. The value is always in memory, and the increment is atomic. We then read the value of the lock into a local vairable. This is not atomic. The value read in to the local variable may be different from the result of the increment. However, the really cunning thing about this is that because the increment is performed before the read operation, thread conflicts that occur will always mean that the value read is too high instead of too low: thread conflicts result in a conservative estimate of whether the lock is free.

It is often useful to write operations like this in assembler, so as to be totally sure that the correct values are being left in memory, and not cached in registers. As it turns out, under Delphi 4 at least, by passing the lock as a var parameter, and including the local variable, the Delphi compiler generates correct code which will work on uniprocessor machines. On multiprocessor machines, register indirect increments and decrements are not atomic. This has been solved in the hand coded assembler version by adding the lock prefix in front of instructions that manipulate the lock. This prefix instructs a processor to lock the memory bus exclusively for the entire duration of the instruction, thus making these operations atomic.

The bad news is that although this is correct in theory, the Win32 virtual machine does not allow user level processes to execute instructions with the lock prefix. Programmers intending to actually use this mechanism should use it only in code with ring 0 privileges. Another problem is that since this version of the spin lock does not call Sleep, it is possible for threads to monopolise the processor whilst waiting for the lock, something that is guaranteed to bring the machine to a grinding halt.

Eventcounts and sequencers.

One proposed alternative to semaphores is to use two new sorts of primitive: eventcounts and sequencers. They both contain counts, but unlike semaphores, the counts increase indefinitely from the time they are created. Some people are happier with the idea that it is possible to individually distinguish between the 32nd and 33rd occurrences of an event in the system. The values of these counts are made available to threads using them, and the values can be used by processes to order their actions. Eventcounts support three operations:
  • EVCount.Advance(): This increments the count, and returns the new value after the increment.
  • EVCount.Read(): This returns the current count.
  • EVCount.Await(WaitCount:integer): This suspends the calling thread until the internal count is greater than or equal to WaitCount.
Sequencers support just one operation:
  • Sequencer.Ticket(): Returns the current internal count in the sequencer, and increments it.
A definition of the classes involved might look something like this. It is then relatively easy to use eventcounts and sequencers to perform all the operations that can be performed using semaphores:
  • Enforcing a mutual Exclusion.
  • The bounded buffer with one producer and one consumer.
  • The bounded buffer with an arbitrary number of producers and consumers.
A particular benefit of this type of synchronization primitive is that the advance and ticket operations can be implemented very simply, using the interlocked compare exchange instruction. This is left as a slightly more difficult exercise for the reader.

Other Win32 synchronization facilities.

Waitable timers. Windows NT and Win2K provide waitable timer objects. These allow a thread or number of threads to wait for a particular amount of time on a timer object. Timers can be used to release a single thread or a certain number of threads on a timed basis; a form of thread flow control. In addition, the delay that waitable timers provide can be set very precisely: the smallest value available is around 100 nanoseconds, making timers preferable to using Sleep if a thread has to be suspended for a certain amount of time.

MessageWaits. When Delphi applications are waiting for threads to exit, the main VCL thread is permanently blocked. This is a potentially problematic situation, because the VCL thread cannot process messages. Win32 provides a MsgWaitForMultipleObjects function to get around this. A thread performing a message wait is blocked either until the synchronization objects become signalled, or a message is placed in the threads message queue. This means that you can get the main VCL thread in an application to wait for worker threads whilst also allowing it to respond to windows messages. A good article on the subject can be found at: http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.html.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 13. Using threads in conjunction with the BDE, Exceptions and DLLs.

In this chapter:

  • DLL's and Multiprocess programming.
  • Thread and process scope. A single threaded DLL.
  • Writing a multithreaded DLL.
  • DLL Set-up and Tear down.
  • Pitfall 1: The Delphi encapsulation of the Entry Point Function.
  • Writing a multiprocess DLL.
  • Global named objects.
  • The DLL in detail.
  • DLL Initialization.
  • An application using the DLL.
  • Pitfall 2: Thread context in Entry Point Functions
  • Exception Handling.
  • The BDE.

DLL's and Multiprocess programming.

Dynamic link libraries, or DLL's allow a programmer to share executable code between several processes. They are commonly used to provide shared library code. for several programs. Writing code for DLL's is in most respects similar to writing code for executables. Despite this, the shared nature of DLL's means that programmers familiar with multithreading often use them to provide system wide services: that is code which affects several processes that have the DLL loaded. In this chapter, we will look at how to write code for DLL's that operates across more than one process.

Thread and process scope. A single threaded DLL.

Global variables in DLL's have process wide scope. This means that if two separate processes have a DLL loaded, all the global variables in the DLL are local to that process. This is not limited to variables in the users code: it also includes all global variables in the Borland run time libraries, and any units used by code in the DLL. This has the advantage that novice DLL programmers can treat DLL programming in the same way as executable programming: if a DLL contains a global variable, then each process has its own copy. Furthermore, this also means that if a DLL is invoked by a processes which contain only one thread, then no special techniques are required: the DLL need not be thread safe, since all the processes have completely isolated incarnations of the DLL.

We can demonstrate this with a simple DLL which does nothing but store an integer. It exports a couple of functions that enable an application to read and write the value of that integer. We can then write a simple test application which uses this DLL. If several copies of the application are executed, one notes that each application uses its own integer, and no interference exists between any of the applications.

Writing a multithreaded DLL.

Writing a multithreaded DLL is mostly the same as writing multithreaded code in an application. The behaviour of multiple threads inside the DLL is the same as the behaviour of multiple threads in a particular application. As always, there are a couple of pitfalls for the unwary:

The main pitfall one can fall into is the behaviour of the Delphi memory manager. By default, the Delphi memory manager is not thread safe. This is for efficiency reasons: if a program only ever contains one thread, then it is pure wasted overhead to include synchronization in the memory manager. The Delphi memory manager can be made thread safe by setting the IsMultiThread variable to true. This is done automatically for a given module if a descendant class of TThread is created.

The problem is that an executable and the DLL consist of two separate modules, each with their own copy of the Delphi memory manager. Thus, if an executable creates several threads, its memory manager is multithreaded. However, if those two threads call a DLL loaded by the executable, the DLL memory manager is not aware of the fact that it is being called by multiple threads. This can be solved by setting the IsMultiThread variable. It is best to set this by using the DLL entry point function, covered later.

The second pitfall occurs as a result of the same problem; that of having two separate memory managers. Memory allocated by the Delphi memory manager that is passed from the DLL to the executable cannot be allocated in one and disposed of in the other. This occurs most often with long strings, but can occur with memory allocated using New or GetMem, and disposed using Dispose or FreeMem. The solution in this case is to include ShareMem, a unit which keeps the two memory managers in step using techniques discussed later.

DLL Set-up and Tear down.

Mindful of the fact that DLL programmers often need to be aware of how many threads and processes are active in a DLL at any given time, the Win32 system architects provide a method for DLL programmers to keep track of thread and process counts in a DLL. This method is known as the DLL Entry Point Function.

In an executable, the entry point (as specified in the module header) indicates where program execution should start. In a DLL, it points to a function that is executed whenever an executable loads or unloads the DLL, or whenever an executable that is currently using the DLL creates or destroys a thread. The function takes a single integer argument which can be one of the following values:

  • DLL_PROCESS_ATTACH: A process has attached itself to the DLL. If this is the first process, then the DLL has just been loaded.
  • DLL_PROCESS_DETACH: A process has detached from the DLL. If this is the only process using the DLL, then the DLL will be unloaded.
  • DLL_THREAD_ATTACH: A thread in the has attached to the DLL. This will happen once when the process loads the DLL, and subsequently whenever the process creates a new thread.
  • DLL_THREAD_DETACH: A thread has detached from the DLL. This will happen whenever the process destroys a thread, and finally when the process unloads the DLL.
As it turns out, DLL entry points have two characteristics which can lead to misunderstandings and problems when writing entry point code. The first characteristic occurs as a result of the Delphi encapsulation of the entry point function, and is relatively simple to work around. The second occurs as a result of thread context, and will be discussed later on.

Pitfall 1: The Delphi encapsulation of the Entry Point Function.

Delphi uses the DLL entry point function to manage initialization and finalization of units within a DLL as well as execution of the main body of DLL code. The DLL writer can put a hook into the Delphi handling by assigning an appropriate function to the variable DLLProc. The default Delphi handling works as follows:
  • The DLL is loaded, which results in the entry point function being called with DLL_PROCESS_ATTACH
  • Delphi uses this to call the initialization of all the units in the DLL, followed by the main body of the DLL code.
  • The DLL is unloaded, resulting in two calls to the entry point function, with the arguments DLL_PROCESS_DETACH.
Now, the application writer only gets code to execute in response to the entry point function when the DLLProc variable points to a function. The correct point to set this up is in the main body of the DLL. However, this is in response to the second call to the entry point function. In short, what this means is that when using the entry point function in the DLL, the delphi programmer will never see the first process attachment to the DLL. As it turns out, this isn't such a huge problem: one can simply assume that the main body of the DLL is called in response to a process loading the DLL, and hence the process and thread count is 1 at that point. Since the DLLProc variable is replicated on a per process basis, even if more processes attach themselves later, the same argument applies, since each incarnation of the DLL has separate global variables.

In case the reader is still confused, I'll present an example. Here is a modified DLL that contains a unit with a function that displays a message. As you can see, the main body, unit initialization and DLL entry point hooks all contain "ShowMessage" calls which enable one to trace what is going on. In order to test this DLL, here is a test application. It consists of a form with a button on. When the button is clicked, a thread is created, which calls the procedure in the DLL, and then destroys itself. So, what happens when we run the program?

  • The DLL reports units initialization
  • The DLL reports main DLL body execution
  • Every time the button is clicked the DLL reports:
    • Entry point: thread attach
    • Unit procedure.
    • Entry point: thread detach
  • Note that if we spawn more than one thread from the application, whilst leaving existing threads blocked on the Unit Procedure message box, the total thread attachment count can increase beyond one.
  • When the program is closed, the DLL reports entry point: process detach, followed by unit finalization.

Writing a multiprocess DLL.

Armed with a knowledge of how to use the entry point function, we will now write a multiprocess DLL. This DLL will store some information on a system wide basis using memory shared between processes. It is worth remembering that when code accesses data shared between processes, the programmer must provide appropriate synchronization. Just as multiple threads in a single process are not inherently synchronized, so the main threads in different processes are also not synchronized. We will also look at some subtleties which occur when trying to use the entry point function to keep track of global threads.

This DLL will share a single integer between processes, as well as keeping a count of the number of processes and threads in the DLL at any one time. It consists of a header file shared between the DLL and applications that use the DLL, and the DLL project file. Before we look more closely at the code, it's worth reviewing some Win32 behaviour.

Global named objects.

The Win32 API allows the programmer to create various objects. For some of these objects, they may be created either anonymously, or with a certain name. Objects created anonymously are, on the whole, limited to use by a single process, the exception being that they may be inherited by child processes. Objects created with a name can be shared between processes. Typically, one process will create the object, specifying a name for that object, and other processes will open a handle to that object by specifying its name.

The delightful thing about named objects is that handles to these objects are reference counted throughout the system. That is, several processes can acquire handles to an object, and when all the handles to that object are closed, the object itself is destroyed, and not before. This includes the situation where an application crashes: typically windows does a good job of cleaning up unused handles after a crash.

The DLL in detail.

Our DLL uses this property to maintain a memory mapped file. Normally, memory mapped files are used to create an area of memory which is a mirror image of a file on disk. This has many useful applications, not least "on demand" paging in of executable images from disk. For this DLL however, a special case is used whereby a memory mapped file is created with no corresponding disk image. This allows the programmer to allocate a section of memory which is shared between several processes. This is surprisingly efficient: once the mapping is set up, no memory copying is done between processes. Once the memory mapped file has been set up, a global named mutex is used to synchronize access to that portion of memory.

DLL Initialization.

Initialization consists of four main stages:
  • Creation of synchronization objects (global and otherwise).
  • Creation of shared data.
  • Initial increment of thread and process counts.
  • Hooking the DLL entry point function.
In the first stage, two synchronization objects are created, a global mutex, and a critical section. Little needs to be said about the critical section. The global mutex is created via the CreateMutex API call. This call has the beneficial feature that if the mutex is named, and the named object already exists, then a handle to the existing named object is returned. This occurs atomically. Were this not the case, then a whole range of unpleasant race conditions could potentially occur. Determining the precise range of possible problems and potential solutions (mainly involving optimistic concurrency control) is left as an exercise to the reader. Suffice to say that if operations on handles to global shared objects were not atomic, the application level Win32 programmer would be staring into an abyss...

In the second stage the area of shared memory is set up. Since we have already set up the global mutex, it is used when setting up the file mapping. A view of the "file" is mapped, which maps the (virtual) file into the address space of the calling process. We also check whether we happened to be the process that originally created the file mapping, and if this is the case, then we zero out the data in our mapped view. This is why the procedure is wrapped in a mutex: CreateFileMapping has the same nice atomicity properties as CreateMutex, ensuring that race conditions on handles will never occur. In the general case, however, the same is not necessarily true for the data in the mapping. If the mapping had a backing file, then we might be able to assume validity of the shared data at start-up. For virtual mappings this is not assured. In this case we need to initialize the data in the mapping atomically with setting up a handle to the mapping, hence the mutex.

In the third stage, we perform our first manipulation on the globally shared data, by incrementing the process and thread counts, since the execution of the main body of the DLL is consistent with the addition of another thread and process to those using the DLL. Note that the AtomicIncThreadCount procedure increments both the local and global threads counts whilst both the global mutex and process local critical section have been acquired. This ensures that multiple threads from the same process see a fully consistent view of both counts.

In the final stage, the DLLProc is hooked, thus ensuring that the creation and destruction of other threads in the process is monitored, and the final exit of the process is also registered.

An application using the DLL.

A simple application that uses the DLL is presented here. It consists of the global shared unit, a unit containing the main form, and a subsidiary unit containing a simple thread. Five buttons exist on the form, allowing the user to read the data contained in the DLL, increment, decrement and set the shared integer, and create one or more threads within the application, just to verify that local thread counts work. As expected, the thread counts increment whenever a new copy of the application is executed, or one of the applications creates a thread. Note that the thread need not directly use the DLL in order for the DLL to be informed of its presence.

Pitfall 2: Thread context in Entry Point Functions.

Instead of using a simple application, let's try one that does something more advanced. In this situation, the DLL is loaded manually by the application programmer, instead of being automatically loaded. This is possible by replacing the previous form unit with this one. An extra button is added which loads the DLL, and sets up the procedure addressed manually. Try running the program, creating several threads and then loading the DLL. You should find that the DLL no longer correctly keeps track of the number of threads in the various processes that use it. Why is this? The Win32 help file states that when using the entry point function with the arguments DLL_THREAD_ATTACH and DLL_THREAD_DETACH:

"DLL_THREAD_ATTACH Indicates that the current process is creating a new thread. When this occurs, the system calls the entry-point function of all DLLs currently attached to the process. The call is made in the context of the new thread. DLLs can use this opportunity to initialize a TLS slot for the thread. A thread calling the DLL entry-point function with the DLL_PROCESS_ATTACH value does not call the DLL entry-point function with the DLL_THREAD_ATTACH value.

Note that a DLL's entry-point function is called with this value only by threads created after the DLL is attached to the process. When a DLL is attached by LoadLibrary, existing threads do not call the entry-point function of the newly loaded DLL."

It drives the point home by also stating:

"DLL_THREAD_DETACH Indicates that a thread is exiting cleanly. If the DLL has stored a pointer to allocated memory in a TLS slot, it uses this opportunity to free the memory. The operating system calls the entry-point function of all currently loaded DLLs with this value. The call is made in the context of the exiting thread. There are cases in which the entry-point function is called for a terminating thread even if the DLL never attached to the thread.

  • The thread was the initial thread in the process, so the system called the entry-point function with the DLL_PROCESS_ATTACH value.
  • The thread was already running when a call to the LoadLibrary function was made, so the system never called the entry-point function for it"
This behaviour has two potentially unpleasant side effects.
  • It is not possible, in the general case to keep track of how many threads are in the DLL on a global basis unless one can guarantee that an application loads the DLL before creating any child threads. One might mistakenly assume that an application loading a DLL would have the DLL_THREAD_ATTACH entry point called for already existing threads. This is not the case because, having guaranteed that thread attachments and detachments are notified to the DLL in the context of the thread attaching or detaching, it is impossible to call the DLL entry point in the correct context of threads that are already running.
  • Since the DLL entry point can be called by several different threads, race conditions can occur between the entry point function and DLL initialization. If a thread is created at about the same time as the DLL is loaded by an application, then it is possible that the DLL entry point might be called for the thread attachment whilst the thread main body is still being executed. This is why it is always a good idea to set up the entry point function as the very last action in DLL initialization.
Readers would benefit from noting that both these side effects have repercussions when deciding when to set the IsMultiThread variable.

Exception Handling.

When writing robust applications, the programmer should always be prepared for things to go wrong. The same is true for multithreaded programming. Most of the examples presented in this tutorial have been relatively simple, and exception handling has mostly been omitted for clarity. In real world applications, this is likely to be unacceptable.

Recall that threads have their own call stack. This means that an exception in a thread does not fall through the standard VCL exception handling mechanisms. Instead of raising a user-friendly dialog box, and an unhandled exception in a thread will terminate the application. As a result of this, the execute method of a thread is one of the few places where it can be useful to create an exception handler that catches all exceptions. Once an exception has been caught in a thread, dealing with it is also slightly different from ordinary VCL handling. It may not always be appropriate to show a dialog box. Quite often, a valid tactic is to let the thread communicate the fact that a failure has occurred to the main VCL thread, using whatever communication mechanisms are in place, and then let the VCL thread decide what to do. This is particularly useful if the VCL thread has created the child thread to perform a particular operation.

Despite this, there are some situations in threads where dealing with error cases can be particularly difficult. Most of these situations occur when using threads to perform continuous background operations. Recalling chapter 10, the BAB has a couple of threads that forward read and write operations from the VCL thread to a blocking buffer. If an error occurs in either of these threads, the error may show no clear causal relationship with any particular operation in the VCL thread, and it may be difficult to communicate failure instantly back to the VCL thread. Not only this, but an exception in either of these threads is likely to break them out of the read or write loop that they are in, raising the difficult question of whether these threads can be usefully restarted. About the best that can be done is to set some state indicating that all future operations should be failed, forcing the main thread to destroy and re-initialize the buffer.

The best solution is to include the possibility of such problems into the original application design, and to determine best effort recovery attempts that may be made.

The BDE.

In Chapter 7, I indicated that one potential solution to locking problems is to put shared data in a database, and use the BDE to perform concurrency control. The programmer should note that each thread must maintain a separate database connection for this to work properly. Hence, each thread should use a separate TSession object to manage its connection to the database. Each application has a TSessionList component called Sessions to enable this to be done easily. Detailed explanation of multiple sessions is beyond the scope of this document.

[Contents] [Previous] [Next]

© Martin Harvey 2000.


Chapter 14. A real world problem, and its solution.

In this chapter:

  • The problem.
  • The solution.
  • The pipe DLL and interface files.
  • The reader and writer threads.
  • A socket based interface.

The problem.

Over the past couple of years I have been writing a distributed raytracer. This uses TCP/IP to send descriptions of scenes to be rendered across a network from a central server to a collection of clients. The clients render the image, and then return the data to the server. Some beta testers were interested in trying the program out, but mentioned that they did not have a TCP/IP stack loaded on their machine. I decided that it would be useful to write some code that emulated TCP sockets, allowing communication between two applications (both client and server) on the local machine.

Various potential solutions were investigated. The most promising at first seemed to be to use named pipes. Unfortunately a problem soon cropped up: The protocols I was using on top of TCP/IP assumed that connection semantics could be performed on a strictly peer to peer basis: either program could initiate a connection to the other, and either program could disconnect at any time. Both connection and disconnection were perfectly symmetrical: The protocols used on top of TCP performed a three way handshake over and above that performed at the TCP layer to negotiate whether a connection could be closed, and that having occured, either end could close the connection. Unfortunately, named pipes did not provide the correct disconnection semantics, and they did not cope well with various error situations.

The solution.

I do not intend to explain the solution in detail, but more advanced readers may find the code interesting reading. In the end, I decided to use shared memory for data transfer, and to implement all synchronisation from the ground up. The solution was implemented in 3 stages.
  • A DLL was written which provided a bidirectional blocking pipe between two applications.
  • A pair of reader and writer threads were written to allow asynchronous access to the blocking pipes.
  • A wrapper around the threads was written to provide an asynchronous interface similar to nonblocking sockets.

The pipe DLL and interface files.

  • MCHPipe.dpr
  • MCHPipeInterface2.pas
  • MCHPipeTypes.pas
This DLL is similar to the bounded buffer example found in chapter 9. Looking back on this code, I can only presume that I'd written it after a couple of weeks frantic hacking in C at work, because it's far more convoluted than it needs to be. One point of interest is that the semaphores used for blocking operations do not assume that the bounded buffers are any particular size; instead state is kept on whether the reader or writer threads are blocked or not.

The reader and writer threads.

  • MCHPipeThreads.pas
  • MCHMemoryStream.pas
The pipe threads are exactly analogous to the reader and writer threads in the BAB in chapter 10. Notifications are not used for write operations, instead, the writer thread buffers the data internally. This was allowable given the semantics of higher layer protocols.

A socket based interface.

  • MCHPipeSocket.pas
  • MCHTransactions.pas
  • MCHPipeTransactions.pas
This is a not-quite-pure socket interface, and should be reasonably obvious to those familiar with TCP sockets programming. Since this implementation was designed to work specifically with some other protocols I wrote, it is worthwhile including the transaction layer of the overlying protocols so you can see how the socket fits into the scheme of things.

[Contents] [Previous]

© Martin Harvey 2000.


Source1

unit test;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage('Hello World!');
end;

end.


Source2

unit PrimeForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    procedure SpawnButtonClick(Sender: TObject);
  private
      { Private declarations }
  public
      { Public declarations }
  end;

var
  PrimeFrm: TPrimeFrm;

implementation

uses PrimeThread;

{$R *.DFM}

procedure TPrimeFrm.SpawnButtonClick(Sender: TObject);

var
  NewThread: TPrimeThrd;

begin
  NewThread := TPrimeThrd.Create(True);
  NewThread.FreeOnTerminate := True;
  try
    NewThread.TestNumber := StrToInt(NumEdit.Text);
    NewThread.Resume;
  except on EConvertError do
    begin
      NewThread.Free;
      ShowMessage('That is not a valid number!');
    end;
  end;
end;

end.


Source3

unit PrimeThread;

interface

uses
  Classes;

type
  TPrimeThrd = class(TThread)
  private
    FTestNumber: integer;
  protected
    function IsPrime: boolean;
    procedure Execute; override;
  public
    property TestNumber: integer write FTestNumber;
  end;

implementation

uses SysUtils, Dialogs;

function TPrimeThrd.IsPrime: boolean;

var
  iter: integer;

begin
  result := true;
  if FTestNumber < 0 then
  begin
    result := false;
    exit;
  end;
  if FTestNumber <= 2 then
    exit;
  for iter := 2 to FTestNumber - 1 do
  begin
    if (FTestNumber mod iter) = 0 then
    begin
      result := false;
      {exit;}
    end;
  end;
end;

procedure TPrimeThrd.Execute;
begin
  if IsPrime then
    ShowMessage(IntToStr(FTestNumber) + 'is prime.')
  else
    ShowMessage(IntToStr(FTestNumber) + 'is not prime.');
end;

end.


Source4

var
  a: integer; {a is global}

begin
  a := a + 1;
end;


Source5

type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    ResultsMemo: TMemo;
    procedure SpawnButtonClick(Sender: TObject);
  private
      { Private declarations }
  public
      { Public declarations }
  end;


Source6

unit PrimeThread;

interface

uses
  Classes;

type
  TPrimeThrd = class(TThread)
  private
    FTestNumber: integer;
    FResultString: string;
  protected
    function IsPrime: boolean;
    procedure UpdateResults;
    procedure Execute; override;
  public
    property TestNumber: integer write FTestNumber;
  end;

implementation

uses SysUtils, Dialogs, PrimeForm;

procedure TPrimeThrd.UpdateResults;
begin
  PrimeFrm.ResultsMemo.Lines.Add(FResultString);
end;

function TPrimeThrd.IsPrime: boolean; {omitted for brevity}

  procedure TPrimeThrd.Execute;
  begin
    if IsPrime then
      FResultString := IntToStr(FTestNumber) + ' is prime.'
    else
      FResultString := IntToStr(FTestNumber) + ' is not prime.';
    Synchronize(UpdateResults);
  end;

end.


Source7

function TPrimeThrd.IsPrime: boolean;

var
  iter: integer;

begin
  result := true;
  if FTestNumber < 0 then
  begin
    result := false;
    exit;
  end;
  if FTestNumber <= 2 then
    exit;
  iter := 2;
  while (iter < FTestNumber) and (not terminated) do
  begin
    if (FTestNumber mod iter) = 0 then
    begin
      result := false;
      {exit;}
    end;
    Inc(iter);
  end;
end;


Source8

unit PrimeForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    ResultsMemo: TMemo;
    procedure SpawnButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  private
    { Private declarations }
    FThreadRefCount: integer;
    procedure HandleTerminate(Sender: TObject);
  public
    { Public declarations }
  end;

var
  PrimeFrm: TPrimeFrm;

implementation

uses PrimeThread;

{$R *.DFM}

procedure TPrimeFrm.SpawnButtonClick(Sender: TObject);

var
  NewThread: TPrimeThrd;

begin
  NewThread := TPrimeThrd.Create(True);
  NewThread.FreeOnTerminate := True;
  try
    with NewThread do
    begin
      TestNumber := StrToInt(NumEdit.Text);
      Inc(FThreadRefCount);
      OnTerminate := HandleTerminate;
      Resume;
    end;
  except on EConvertError do
    begin
      NewThread.Free;
      ShowMessage('That is not a valid number!');
    end;
  end;
end;

procedure TPrimeFrm.FormCreate(Sender: TObject);
begin
  FThreadRefCount := 0;
end;

procedure TPrimeFrm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  CanClose := true;
  if FThreadRefCount > 0 then
  begin
    if MessageDlg('Threads active. Do you still want to quit?',
      mtWarning, [mbYes, mbNo], 0) = mrNo then
      CanClose := false;
  end;
end;

procedure TPrimeFrm.HandleTerminate(Sender: TObject);
begin
  Dec(FThreadRefCount);
end;

end.


Source9

unit PrimeForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, PrimeThread;

const
  WM_THREAD_COMPLETE = WM_APP + 5437; { Just a magic number }

type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    ResultsMemo: TMemo;
    procedure SpawnButtonClick(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  private
    { Private declarations }
    FThread: TPrimeThrd;
    procedure HandleThreadCompletion(var Message: TMessage); message WM_THREAD_COMPLETE;
  public
    { Public declarations }
  end;

var
  PrimeFrm: TPrimeFrm;

implementation

{$R *.DFM}

procedure TPrimeFrm.HandleThreadCompletion(var Message: TMessage);
begin
  if Assigned(FThread) then
  begin
    FThread.WaitFor;
    FThread.Free;
    FThread := nil;
  end;
end;

procedure TPrimeFrm.SpawnButtonClick(Sender: TObject);

begin
  if not Assigned(FThread) then
  begin
    FThread := TPrimeThrd.Create(True);
    FThread.FreeOnTerminate := false;
    try
      with FThread do
      begin
        TestNumber := StrToInt(NumEdit.Text);
        Resume;
      end;
    except on EConvertError do
      begin
        FThread.Free;
        FThread := nil;
        ShowMessage('That is not a valid number!');
      end;
    end;
  end;
end;

procedure TPrimeFrm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  CanClose := true;
  if Assigned(FThread) then
  begin
    if MessageDlg('Threads active. Do you still want to quit?',
      mtWarning, [mbYes, mbNo], 0) = mrNo then
      CanClose := false;
  end;
  {Sleep(50000);}{Line C}
  if CanClose then
  begin
    if Assigned(FThread) then
    begin
      FThread.Terminate;
      FThread.WaitFor;
      FThread.Free;
      FThread := nil;
    end;
  end;
end;

end.


Source10

unit PrimeThread;

interface

uses
  Classes;

type
  TPrimeThrd = class(TThread)
  private
    FTestNumber: integer;
    FResultString: string;
  protected
    function IsPrime: boolean;
    procedure UpdateResults;
    procedure Execute; override;
  public
    property TestNumber: integer write FTestNumber;
  end;

implementation

uses SysUtils, Dialogs, PrimeForm, Windows;

procedure TPrimeThrd.UpdateResults;
begin
  PrimeFrm.ResultsMemo.Lines.Add(FResultString);
end;

function TPrimeThrd.IsPrime: boolean;

var
  iter: integer;

begin
  result := true;
  if FTestNumber < 0 then
  begin
    result := false;
    exit;
  end;
  if FTestNumber <= 2 then
    exit;
  iter := 2;
  while (iter < FTestNumber) and (not terminated) do {Line A}
  begin
    if (FTestNumber mod iter) = 0 then
    begin
      result := false;
      {exit;}
    end;
    Inc(iter);
  end;
end;

procedure TPrimeThrd.Execute;
begin
  if IsPrime then
    FResultString := IntToStr(FTestNumber) + ' is prime.'
  else
    FResultString := IntToStr(FTestNumber) + ' is not prime.';
  if not Terminated then {Line B}
  begin
    Synchronize(UpdateResults);
    PostMessage(PrimeFrm.Handle, WM_THREAD_COMPLETE, 0, 0);
  end;
end;

end.


Source11

{ Unit PrimeThread }

type
  TPrimeThrd = class(TThread)
  private
    FTestNumber: integer;
    FResultString: string;
  protected
    function IsPrime: boolean;
    procedure Execute; override;
  public
    property TestNumber: integer write FTestNumber;
    property ResultString: string read FResultString;
  end;

procedure TPrimeThrd.Execute;
begin
  if IsPrime then
    FResultString := IntToStr(FTestNumber) + ' is prime.'
  else
    FResultString := IntToStr(FTestNumber) + ' is not prime.';
  if not Terminated then {Line B}
    PostMessage(PrimeFrm.Handle, WM_THREAD_COMPLETE, 0, 0);
end;

{ Unit PrimeForm }

procedure TPrimeFrm.HandleThreadCompletion(var Message: TMessage);
begin
  if Assigned(FThread) then
  begin
    FThread.WaitFor;
    ResultsMemo.Lines.Add(FThread.ResultString);
    FThread.Free;
    FThread := nil;
  end;
end;


Source12

unit PrimeForm2;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, PrimeThread;

const
  WM_DATA_IN_BUF = WM_APP + 1000;
  MaxMemoLines = 20;

type
  TPrimeFrm = class(TForm)
    ResultMemo: TMemo;
    StartBtn: TButton;
    StartNumEdit: TEdit;
    StopBtn: TButton;
    procedure StartBtnClick(Sender: TObject);
    procedure StopBtnClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
    FStringSectInit: boolean;
    FPrimeThread: TPrimeThrd2;
    FStringBuf: TStringList;
    procedure UpdateButtons;
    procedure HandleNewData(var Message: TMessage); message WM_DATA_IN_BUF;
  public
    { Public declarations }
    StringSection: TRTLCriticalSection;
    property StringBuf: TStringList read FStringBuf write FStringBuf;
  end;

var
  PrimeFrm: TPrimeFrm;

implementation

{$R *.DFM}

procedure TPrimeFrm.UpdateButtons;
begin
  StopBtn.Enabled := FStringSectInit;
  StartBtn.Enabled := not FStringSectInit;
end;

procedure TPrimeFrm.StartBtnClick(Sender: TObject);
begin
  if not FStringSectInit then
  begin
    InitializeCriticalSection(StringSection);
    FStringBuf := TStringList.Create;
    FStringSectInit := true;
    FPrimeThread := TPrimeThrd2.Create(true);
    SetThreadPriority(FPrimeThread.Handle, THREAD_PRIORITY_BELOW_NORMAL);
    try
      FPrimeThread.StartNum := StrToInt(StartNumEdit.Text);
    except
      on EConvertError do FPrimeThread.StartNum := 2;
    end;
    FPrimeThread.Resume;
  end;
  UpdateButtons;
end;

procedure TPrimeFrm.StopBtnClick(Sender: TObject);
begin
  if FStringSectInit then
  begin
    with FPrimeThread do
    begin
      Terminate;
      WaitFor;
      Free;
    end;
    FPrimeThread := nil;
    FStringBuf.Free;
    FStringBuf := nil;
    DeleteCriticalSection(StringSection);
    FStringSectInit := false;
  end;
  UpdateButtons;
end;

procedure TPrimeFrm.HandleNewData(var Message: TMessage);
begin
  if FStringSectInit then {Not necessarily the case!}
  begin
    EnterCriticalSection(StringSection);
    ResultMemo.Lines.Add(FStringBuf.Strings[0]);
    FStringBuf.Delete(0);
    LeaveCriticalSection(StringSection);
    {Now trim the Result Memo.}
    if ResultMemo.Lines.Count > MaxMemoLines then
      ResultMemo.Lines.Delete(0);
  end;
end;

procedure TPrimeFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  StopBtnClick(Self);
end;

end.


Source13

unit PrimeThread;

interface

uses
  Classes, Windows;

type
  TPrimeThrd2 = class(TThread)
  private
    { Private declarations }
    FStartNum: integer;
    function IsPrime(TestNo: integer): boolean;
  protected
    procedure Execute; override;
  public
    property StartNum: integer read FStartNum write FStartNum;
  end;

implementation

uses PrimeForm2, SysUtils;

function TPrimeThrd2.IsPrime(TestNo: integer): boolean;

var
  iter: integer;

begin
  result := true;
  if TestNo < 0 then
    result := false;
  if TestNo <= 2 then
    exit;
  iter := 2;
  while (iter < TestNo) and (not terminated) do
  begin
    if (TestNo mod iter) = 0 then
    begin
      result := false;
      exit;
    end;
    Inc(iter);
  end;
end;

procedure TPrimeThrd2.Execute;

var
  CurrentNum: integer;

begin
  CurrentNum := FStartNum;
  while not Terminated do
  begin
    if IsPrime(CurrentNum) then
    begin
      EnterCriticalSection(PrimeFrm.StringSection);
      PrimeFrm.StringBuf.Add(IntToStr(CurrentNum) + ' is prime.');
      LeaveCriticalSection(PrimeFrm.StringSection);
      PostMessage(PrimeFrm.Handle, WM_DATA_IN_BUF, 0, 0);
    end;
    Inc(CurrentNum);
  end;
end;

end.


Source14

function Read(L: TList; Index: integer): integer;
begin
  if (Index > 0) and (L.Count > Index) then
  begin
    with L.Items[Index] do
    begin
      Lock;
      Result := Value;
      Unlock;
    end;
  end
  else
    raise ENotFound;
end;

procedure Write(L: TList; Index: integer; NewVal: integer);
begin
  if (Index > 0) and (L.Count > Index) then
  begin
    with L.Items[Index] do
    begin
      Lock;
      Value := NewVal;
      Unlock;
    end;
  end
  else
    raise ENotFound;
end;

function Compare(L: TList; Ind1, Ind2: integer): integer;
begin
  if (Ind1 > 0) and (Ind2 > 0) and (L.Count > Ind1) and (L.Count > Ind2) then
  begin
    L.Items[Ind1].Lock;
    L.Items[Ind2}.Lock;
    Result := L.Items[Ind2].Value - L.Items[Ind1].Value;
    L.Items[Ind2].Unlock;
    L.Items[Ind1].Unlock;
  end
  else
    raise ENotFound;
end;


Source15

function CompareIndirect(L: TList; Ind1: integer): integer;
var
  Ind2: integer;

begin
  if (Ind1 > 0) and (L.Count > Ind1) then
  begin
    L.Items[Ind1].Lock;
    Ind2 := L.Items[Ind1];
    Assert(Ind2 <> Ind1); {I'm not even going to consider this nasty case in any more detail!}
    if Ind2 > Ind1 then
      L.Items[Ind2].Lock
    else
    begin
      L.Items[Ind1].Unlock;
      L.Items[Ind2].Lock;
      L.Items[Ind1].Lock;
    end;
    Result := L.Items[Ind2].Value - L.Items[Ind1].Value;
    L.Items[Ind1].Unlock;
    L.Items[Ind2].Unlock;
  end
  else
    raise ENotFound;
end;


Source16

function CompareIndirect(L: TList; Ind1: integer): integer;
var
  Ind2: integer;
  TempValue: integer;

begin
  if (Ind1 > 0) and (L.Count > Ind1) then
  begin
    L.Items[Ind1].Lock;
    Ind2 := L.Items[Ind1];
    Assert(Ind2 <> Ind1); {I'm not even going to consider this nasty case in any more detail!}
    if Ind2 > Ind1 then
      L.Items[Ind2].Lock
    else
    begin
      TempValue := L.Items[Ind1].Value;
      L.Items[Ind1].Unlock;
      L.Items[Ind2].Lock;
      L.Items[Ind1].Lock;
    end;
    if TempValue := L.Items[Ind1].Value then
      Result := L.Items[Ind2].Value - L.Items[Ind1].Value
    else
      {Perhaps some retry mechanism?};
    L.Items[Ind1].Unlock;
    L.Items[Ind2].Unlock;
  end
  else
    raise ENotFound;
end;


Source17

type
  TPrimeFrm = class(TForm)
    { No change here until public declarations }
  public
    { Public declarations }
    StringSemaphore: THandle; { Now a semaphore instead of a critical section }
    property StringBuf: TStringList read FStringBuf write FStringBuf;
  end;

procedure TPrimeFrm.StartBtnClick(Sender: TObject);
begin
  if not FStringSectInit then
  begin
    StringSemaphore := CreateSemaphore(nil, 1, 1, SemName); { Now creating a semaphore instead of a critical section }
    FStringBuf := TStringList.Create;
    FStringSectInit := true;
    FPrimeThread := TPrimeThrd2.Create(true);
    SetThreadPriority(FPrimeThread.Handle, THREAD_PRIORITY_BELOW_NORMAL);
    try
      FPrimeThread.StartNum := StrToInt(StartNumEdit.Text);
    except
      on EConvertError do FPrimeThread.StartNum := 2;
    end;
    FPrimeThread.Resume;
  end;
  UpdateButtons;
end;

procedure TPrimeFrm.StopBtnClick(Sender: TObject);
begin
  if FStringSectInit then
  begin
    with FPrimeThread do
    begin
      Terminate;
      WaitFor;
      Free;
    end;
    FPrimeThread := nil;
    FStringBuf.Free;
    FStringBuf := nil;
    CloseHandle(StringSemaphore); { Deleting semaphore }
    FStringSectInit := false;
  end;
  UpdateButtons;
end;

procedure TPrimeFrm.HandleNewData(var Message: TMessage);
begin
  if FStringSectInit then {Not necessarily the case!}
  begin
    WaitForSingleObject(StringSemaphore, INFINITE); { New wait call }
    ResultMemo.Lines.Add(FStringBuf.Strings[0]);
    FStringBuf.Delete(0);
    ReleaseSemaphore(StringSemaphore, 1, nil); { New release call }
    {Now trim the Result Memo.}
    if ResultMemo.Lines.Count > MaxMemoLines then
      ResultMemo.Lines.Delete(0);
  end;
end;

procedure TPrimeThrd2.Execute;

var
  CurrentNum: integer;

begin
  CurrentNum := FStartNum;
  while not Terminated do
  begin
    if IsPrime(CurrentNum) then
    begin
      WaitForSingleObject(PrimeFrm.StringSemaphore, INFINITE); { New wait call }
      PrimeFrm.StringBuf.Add(IntToStr(CurrentNum) + ' is prime.');
      ReleaseSemaphore(PrimeFrm.StringSemaphore, 1, nil); { New release call }
      PostMessage(PrimeFrm.Handle, WM_DATA_IN_BUF, 0, 0);
    end;
    Inc(CurrentNum);
  end;
end;


Source18

unit BoundedBuf;

{Martin Harvey 24/4/2000}

interface

uses Windows, SysUtils;

const
  DefaultWaitTime = 5000; { Five second wait on mutexes }

type
  { I don't particularly like dynamic arrays, so I'm going to do things
    the "C" way here, explicitly allocating memory
    Think of TBufferEntries as ^(array of pointer) }

  TBufferEntries = ^Pointer;

  TBoundedBuffer = class
  private
    FBufInit: boolean;
    FBufSize: integer;
    FBuf: TBufferEntries;
    FReadPtr, { ReadPtr points to next used entry in buffer}
    FWritePtr: integer; { WritePtr points to next free entry in buffer}
    FEntriesFree, FEntriesUsed: THandle; { Flow control semaphores }
    FCriticalMutex: THandle; { Critical section mutex }
  protected
    procedure SetSize(NewSize: integer);
  public
    procedure ResetState;
    destructor Destroy; override;
    function PutItem(NewItem: Pointer): boolean;
    function GetItem: Pointer;
  published
    property Size: integer read FBufSize write SetSize;
  end;

  { No constructor required because default values of 0, false etc acceptable }

implementation

const
  FailMsg1 = 'Flow control failed, or buffer not initialised';
  FailMsg2 = 'Critical section failed, or buffer not initialised';

procedure TBoundedBuffer.SetSize(NewSize: integer);

{ Initialises handles and allocates memory.
  If the buffer size has previously been set, then this may invoke a buffer
  reset }

begin
  if FBufInit then ResetState;
  if NewSize < 2 then NewSize := 2;
  FBufSize := NewSize;
  GetMem(FBuf, Sizeof(Pointer) * FBufSize);
  FillMemory(FBuf, Sizeof(Pointer) * FBufSize, 0);
  FBufInit := true;
  FCriticalMutex := CreateMutex(nil, false, nil); { note lack of name }
  { The initial count on the semaphores requires some thought,
    The maximum count requires more thought.
    Again, all synchronisation objects are anonymous }
  FEntriesFree := CreateSemaphore(nil, FBufSize - 1, FBufSize, nil);
  FEntriesUsed := CreateSemaphore(nil, 0, FBufSize, nil);
  if (FCriticalMutex = 0)
    or (FEntriesFree = 0)
    or (FEntriesUsed = 0) then ResetState
end;

procedure TBoundedBuffer.ResetState;

{ Closes handles and deallocates memory.
  Note that this must unblock threads in such a manner that they quit cleanly }

begin
  if FBufInit then
  begin
    WaitForSingleObject(FCriticalMutex, DefaultWaitTime);
    FBufInit := false;
    FBufSize := 0;
    FreeMem(FBuf);
    { Now wake up all threads currently waiting.
      Currently assumes only 1 producer and 1 consumer.
      Plenty of ordering subtleties and pitfalls to be discussed here }
    ReleaseSemaphore(FEntriesFree, 1, nil);
    ReleaseSemaphore(FEntriesUsed, 1, nil);
    CloseHandle(FEntriesFree);
    CloseHandle(FEntriesUsed);
    { If reader or writer threads are waiting,
      then they will be waiting on the mutex.
      We will close the handle and let them time out }
    CloseHandle(FCriticalMutex);
  end;
end;

function TBoundedBuffer.PutItem(NewItem: Pointer): boolean;

{ Called by producer thread }
var
  NthItem: TBufferEntries;

begin
  result := false;
  { WAIT(EntriesFree) }
  if WaitForSingleObject(FEntriesFree, INFINITE) <> WAIT_OBJECT_0 then
    exit;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then
    exit;
  NthItem := FBuf;
  Inc(NthItem, FWritePtr);
  NthItem^ := NewItem;
  FWritePtr := (FWritePtr + 1) mod FBufSize;
  ReleaseMutex(FCriticalMutex);
  { SIGNAL(EntriesUsed) }
  ReleaseSemaphore(FEntriesUsed, 1, nil);
  result := true;
end;

function TBoundedBuffer.GetItem: Pointer;

{ Called by consumer thread }
var
  NthItem: TBufferEntries;

begin
  result := nil;
  { WAIT(EntriesUsed) }
  if WaitForSingleObject(FEntriesUsed, INFINITE) <> WAIT_OBJECT_0 then
    exit;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then
    exit;
  NthItem := FBuf;
  Inc(NthItem, FReadPtr);
  Result := NthItem^;
  FReadPtr := (FReadPtr + 1) mod FBufSize;
  ReleaseMutex(FCriticalMutex);
  { SIGNAL(EntriesFree) }
  ReleaseSemaphore(FEntriesFree, 1, nil);
end;

destructor TBoundedBuffer.Destroy;
begin
  ResetState;
  inherited Destroy;
end;

end.


Source19

procedure TBoundedBuffer.ResetState;

{ Closes handles and deallocates memory.
  Note that this must unblock threads in such a manner that they quit cleanly }

var
  SemCount: integer;

begin
  if FBufInit then
  begin
    WaitForSingleObject(FCriticalMutex, DefaultWaitTime);
    FBufInit := false;
    FBufSize := 0;
    FreeMem(FBuf);
    repeat
      ReleaseSemaphore(FEntriesFree, 1, @SemCount);
    until SemCount = 0;
    repeat
      ReleaseSemaphore(FEntriesUsed, 1, @SemCount);
    until SemCount = 0;
    CloseHandle(FEntriesFree);
    CloseHandle(FEntriesUsed);
    CloseHandle(FCriticalMutex);
  end;
end;


Source20

procedure TBoundedBuffer.ResetState;

{ Closes handles and deallocates memory.
  Note that this must unblock threads in such a manner that they quit cleanly }

var
  SemCount: integer;
  LocalHandle: THandle;

begin
  if FBufInit then
  begin
    WaitForSingleObject(FCriticalMutex, DefaultWaitTime);
    FBufInit := false;
    FBufSize := 0;
    FreeMem(FBuf);
    LocalHandle := FEntriesFree;
    FEntriesFree := 0;
    repeat
      ReleaseSemaphore(LocalHandle, 1, @SemCount);
    until SemCount = 0;
    CloseHandle(LocalHandle);
    LocalHandle := FEntriesUsed;
    FEntriesUsed := 0;
    repeat
      ReleaseSemaphore(LocalHandle, 1, @SemCount);
    until SemCount = 0;
    CloseHandle(LocalHandle);
    CloseHandle(FCriticalMutex);
  end;
end;


Source21

unit HandleForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  THandleFrm = class(TForm)
    CreateBtn: TButton;
    CloseOwnerBtn: TButton;
    CloseNonOwnerBtn: TButton;
    procedure CreateBtnClick(Sender: TObject);
    procedure CloseOwnerBtnClick(Sender: TObject);
    procedure CloseNonOwnerBtnClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    Mutex: THandle;
  end;

var
  HandleFrm: THandleFrm;

implementation

uses HandleThreads;

{$R *.DFM}

procedure THandleFrm.CreateBtnClick(Sender: TObject);

var
  NewThread: THandleThread;

begin
  Mutex := CreateMutex(nil, false, nil);
  WaitForSingleObject(Mutex, INFINITE);
  NewThread := THandleThread.Create(false);
  NewThread := THandleThread.Create(false);
  ShowMessage('Threads Created.');
end;

procedure THandleFrm.CloseOwnerBtnClick(Sender: TObject);
begin
  CloseHandle(Mutex);
end;

procedure THandleFrm.CloseNonOwnerBtnClick(Sender: TObject);
begin
  ReleaseMutex(Mutex);
  CloseHandle(Mutex);
end;

end.

unit HandleThreads;

interface

uses
  Classes, Windows, SysUtils, Dialogs;

type
  THandleThread = class(TThread)
  private
    { Private declarations }
  protected
    procedure Execute; override;
  end;

implementation

uses HandleForm;

procedure THandleThread.Execute;

var
  RetVal: integer;

begin
  RetVal := WaitForSingleObject(HandleFrm.Mutex, INFINITE);
  case RetVal of
    WAIT_OBJECT_0: ShowMessage('Unblocked: WAIT_OBJECT_0');
    WAIT_ABANDONED: ShowMessage('Unblocked: WAIT_ABANDONED');
    WAIT_TIMEOUT: ShowMessage('Unblocked: WAIT_TIMEOUT');
  else
    ShowMessage('Unblocked. Unknown return code.');
  end;
end;

end.


Source22

unit HandleForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  THandleFrm = class(TForm)
    CreateBtn: TButton;
    CloseOwnerBtn: TButton;
    CloseNonOwnerBtn: TButton;
    RelBtn: TButton;
    procedure CreateBtnClick(Sender: TObject);
    procedure CloseOwnerBtnClick(Sender: TObject);
    procedure CloseNonOwnerBtnClick(Sender: TObject);
    procedure RelBtnClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    Semaphore: THandle;
  end;

var
  HandleFrm: THandleFrm;

implementation

uses HandleThreads;

{$R *.DFM}

procedure THandleFrm.CreateBtnClick(Sender: TObject);

begin
  Semaphore := CreateSemaphore(nil, 1, 1, nil);
  WaitForSingleObject(Semaphore, INFINITE);
  THandleThread.Create(false);
  THandleThread.Create(false);
  ShowMessage('Threads Created.');
end;

procedure THandleFrm.CloseOwnerBtnClick(Sender: TObject);
begin
  CloseHandle(Semaphore);
end;

procedure THandleFrm.CloseNonOwnerBtnClick(Sender: TObject);
begin
  ReleaseSemaphore(Semaphore, 1, nil);
  CloseHandle(Semaphore);
end;

procedure THandleFrm.RelBtnClick(Sender: TObject);
begin
  ReleaseSemaphore(Semaphore, 1, nil);
end;

end.

unit HandleThreads;

interface

uses
  Classes, Windows, SysUtils, Dialogs;

type
  THandleThread = class(TThread)
  private
    { Private declarations }
  protected
    procedure Execute; override;
  end;

implementation

uses HandleForm;

procedure THandleThread.Execute;

var
  RetVal: integer;

begin
  RetVal := WaitForSingleObject(HandleFrm.Semaphore, 10000);
  case RetVal of
    WAIT_OBJECT_0: ShowMessage('Unblocked: WAIT_OBJECT_0');
    WAIT_ABANDONED: ShowMessage('Unblocked: WAIT_ABANDONED');
    WAIT_TIMEOUT: ShowMessage('Unblocked: WAIT_TIMEOUT');
  else
    ShowMessage('Unblocked. Unknown return code.');
  end;
end;

end.


Source23

unit BoundedBuf;

{Martin Harvey 24/4/2000}

interface

uses Windows, SysUtils;

const
  DefaultWaitTime = 1000; { One second wait on all synchronisation primitives }

type
  { I don't particularly like dynamic arrays, so I'm going to do things
    the "C" way here, explicitly allocating memory
    Think of TBufferEntries as ^(array of pointer) }

  TBufferEntries = ^Pointer;

  TBoundedBuffer = class
  private
    FBufInit: boolean;
    FBufSize: integer;
    FBuf: TBufferEntries;
    FReadPtr, { ReadPtr points to next used entry in buffer}
    FWritePtr: integer; { WritePtr points to next free entry in buffer}
    FEntriesFree, FEntriesUsed: THandle; { Flow control semaphores }
    FCriticalMutex: THandle; { Critical section mutex }
  protected
    procedure SetSize(NewSize: integer);
    function ControlledWait(Semaphore: THandle): boolean;
    { Returns whether wait returned OK, or an error occurred }
  public
    procedure ResetState;
    destructor Destroy; override;
    function PutItem(NewItem: Pointer): boolean;
    function GetItem: Pointer;
  published
    property Size: integer read FBufSize write SetSize;
  end;

  { No constructor required because default values of 0, false etc acceptable }

implementation

procedure TBoundedBuffer.SetSize(NewSize: integer);

{ Initialises handles and allocates memory.
  If the buffer size has previously been set, then this may invoke a buffer
  reset }

begin
  if FBufInit then ResetState;
  if NewSize < 2 then NewSize := 2;
  FBufSize := NewSize;
  GetMem(FBuf, Sizeof(Pointer) * FBufSize);
  FillMemory(FBuf, Sizeof(Pointer) * FBufSize, 0);
  FBufInit := true;
  FCriticalMutex := CreateMutex(nil, false, nil); { note lack of name }
  { The initial count on the semaphores requires some thought,
    The maximum count requires more thought.
    Again, all synchronisation objects are anonymous }
  FEntriesFree := CreateSemaphore(nil, FBufSize - 1, FBufSize, nil);
  FEntriesUsed := CreateSemaphore(nil, 0, FBufSize, nil);
  if (FCriticalMutex = 0)
    or (FEntriesFree = 0)
    or (FEntriesUsed = 0) then ResetState
end;

procedure TBoundedBuffer.ResetState;

{ Closes handles and deallocates memory.
  Note that this must unblock threads in such a manner that they quit cleanly }

begin
  if FBufInit then
  begin
    WaitForSingleObject(FCriticalMutex, DefaultWaitTime);
    FBufInit := false;
    FBufSize := 0;
    FreeMem(FBuf);
    ReleaseSemaphore(FEntriesUsed, 1, nil);
    ReleaseSemaphore(FEntriesFree, 1, nil);
    CloseHandle(FEntriesFree);
    CloseHandle(FEntriesUsed);
    ReleaseMutex(FCriticalMutex);
    CloseHandle(FCriticalMutex);
  end;
end;

function TBoundedBuffer.ControlledWait(Semaphore: THandle): boolean;

var
  ErrCode: integer;

begin
  repeat
    ErrCode := WaitForSingleObject(Semaphore, DefaultWaitTime);
    if (ErrCode = WAIT_OBJECT_0) or (ErrCode = WAIT_ABANDONED) then
    begin
      { If wait abandoned, return failure. Buffer not properly cleaned up }
      result := ErrCode = WAIT_OBJECT_0;
      exit;
    end;
    { Wait timed out. Check whether buffer state initialised }
    if WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0 then
    begin
      result := false;
      exit;
    end
    else
    begin
      result := FBufInit;
      ReleaseMutex(FCriticalMutex);
    end;
  until not Result;
end;

function TBoundedBuffer.PutItem(NewItem: Pointer): boolean;

{ Called by producer thread }
var
  NthItem: TBufferEntries;

begin
  result := false;
  { WAIT(EntriesFree) }
  if not ControlledWait(FEntriesFree) then
    exit;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then { NB.This condition depends on L -> R lazy evaluation }
    exit;
  NthItem := FBuf;
  Inc(NthItem, FWritePtr);
  NthItem^ := NewItem;
  FWritePtr := (FWritePtr + 1) mod FBufSize;
  ReleaseMutex(FCriticalMutex);
  { SIGNAL(EntriesUsed) }
  ReleaseSemaphore(FEntriesUsed, 1, nil);
  result := true;
end;

function TBoundedBuffer.GetItem: Pointer;

{ Called by consumer thread }
var
  NthItem: TBufferEntries;

begin
  result := nil;
  { WAIT(EntriesUsed) }
  if not ControlledWait(FEntriesUsed) then
    exit;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then { NB.This condition depends on L -> R lazy evaluation }
    exit;
  NthItem := FBuf;
  Inc(NthItem, FReadPtr);
  Result := NthItem^;
  FReadPtr := (FReadPtr + 1) mod FBufSize;
  ReleaseMutex(FCriticalMutex);
  { SIGNAL(EntriesFree) }
  ReleaseSemaphore(FEntriesFree, 1, nil);
end;

destructor TBoundedBuffer.Destroy;
begin
  ResetState;
  inherited Destroy;
end;

end.


Source24

unit PrimeThreads;

interface

uses
  Windows, Classes, SysUtils, BoundedBuf, Forms;

type
  TIntRec = record
    Num: integer;
  end;
  PIntRec = ^TIntRec;

  TPrimeThread = class(TThread)
  private
    FBuffer: TBoundedBuffer;
  protected
    function IsPrime(TestNum: integer): boolean;
  public
    property Buffer: TBoundedBuffer read FBuffer write FBuffer;
  end;

  TForwardPrimeThread = class(TPrimeThread)
  private
  protected
    procedure SendToBackThread(TestNum: integer);
    procedure Execute; override;
  end;

  TBackwardPrimeThread = class(TPrimeThread)
  private
    FDestSection: PRTLCriticalSection;
    FDestMsgNum: integer;
    FDestForm: TForm;
    FDestList: TStrings;
  protected
    function ReverseNumber(Input: integer): integer;
    function RecieveFromForwardThread(var TestNum: integer): boolean;
    procedure SendToVCLThread(CurrentNumber, ReversedNumber: integer);
    procedure Execute; override;
  public
    property DestSection: PRTLCriticalSection read FDestSection write FDestSection;
    property DestMsgNum: integer read FDestMsgNum write FDestMsgNum;
    property DestForm: TForm read FDestForm write FDestForm;
    property DestList: TStrings read FDestList write FDestList;
  end;

var
  ForwardThread: TForwardPrimeThread;
  BackwardThread: TBackwardPrimeThread;
  Buffer: TBoundedBuffer;

procedure StartThreads(Form: TForm;
  Section: PRTLCriticalSection;
  MsgNum: integer;
  List: TStrings);
procedure StopThreads;

implementation

const
  DefBufSize = 16;

{ Ancillary procedures }

procedure StartThreads(Form: TForm;
  Section: PRTLCriticalSection;
  MsgNum: integer;
  List: TStrings);
begin
  ForwardThread := TForwardPrimeThread.Create(true);
  BackwardThread := TBackwardPrimeThread.Create(true);
  SetThreadPriority(ForwardThread.Handle, THREAD_PRIORITY_BELOW_NORMAL);
  SetThreadPriority(BackwardThread.Handle, THREAD_PRIORITY_BELOW_NORMAL);
  Buffer := TBoundedBuffer.Create;
  Buffer.Size := DefBufSize;
  ForwardThread.Buffer := Buffer;
  BackwardThread.Buffer := Buffer;
  with BackwardThread do
  begin
    DestForm := Form;
    DestSection := Section;
    DestMsgNum := MsgNum;
    DestList := List;
  end;
  ForwardThread.Resume;
  BackwardThread.Resume;
end;

procedure StopThreads;
begin
  ForwardThread.Terminate;
  BackwardThread.Terminate;
  Buffer.ResetState;
  ForwardThread.WaitFor;
  BackwardThread.WaitFor;
  Buffer.Free;
  ForwardThread.Free;
  BackwardThread.Free;
end;

{ TPrimeThread }

function TPrimeThread.IsPrime(TestNum: integer): boolean;

var
  iter: integer;

begin
  result := true;
  if TestNum < 0 then
    result := false;
  if TestNum <= 2 then
    exit;
  iter := 2;
  while (iter < TestNum) and (not terminated) do {Line A}
  begin
    if (TestNum mod iter) = 0 then
    begin
      result := false;
      exit;
    end;
    Inc(iter);
  end;
end;

{ TForwardPrimeThread }

procedure TForwardPrimeThread.SendToBackThread(TestNum: integer);

var
  NewRec: PIntRec;

begin
  New(NewRec);
  NewRec.Num := TestNum;
  if not Buffer.PutItem(NewRec) then Dispose(NewRec);
end;

procedure TForwardPrimeThread.Execute;

var
  CurrentNumber: integer;

begin
  CurrentNumber := 2;
  while not Terminated do
  begin
    if IsPrime(CurrentNumber) then
      SendToBackThread(CurrentNumber);
    Inc(CurrentNumber);
  end;
end;

{ TBackwardPrimeThread }

function TBackwardPrimeThread.RecieveFromForwardThread(var TestNum: integer): boolean;

var
  NewRec: PIntRec;

begin
  NewRec := Buffer.GetItem;
  Result := Assigned(NewRec);
  if Result then TestNum := NewRec^.Num;
end;

procedure TBackwardPrimeThread.SendToVCLThread(CurrentNumber, ReversedNumber: integer);

var
  Msg: string;

begin
  Msg := 'Palindromic primes: ' + IntToStr(CurrentNumber) + ' and '
    + IntToStr(ReversedNumber);
  EnterCriticalSection(FDestSection^);
  DestList.Add(Msg);
  LeaveCriticalSection(FDestSection^);
  PostMessage(DestForm.Handle, DestMsgNum, 0, 0);
end;

function TBackwardPrimeThread.ReverseNumber(Input: integer): integer;

var
  InStr, OutStr: string;
  Len, Iter: integer;

begin
  Input := Abs(Input);
  InStr := IntToStr(Input);
  OutStr := '';
  Len := Length(InStr);
  for Iter := Len downto 1 do
    OutStr := OutStr + InStr[Iter];
  try
    Result := StrToInt(OutStr);
  except
    on EConvertError do Result := Input;
  end;
end;

procedure TBackwardPrimeThread.Execute;

var
  CurrentNumber,
    ReversedNumber: integer;

begin
  while not Terminated do
  begin
    if RecieveFromForwardThread(CurrentNumber) then
    begin
      ReversedNumber := ReverseNumber(CurrentNumber);
      if IsPrime(ReversedNumber) then
        SendToVCLThread(CurrentNumber, ReversedNumber);
    end;
  end;
end;

end.


Source25

unit PalPrimeForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

const
  WM_DATA_IN_BUF = WM_APP + 1000;
  MaxMemoLines = 20;

type
  TPalFrm = class(TForm)
    ResultsMemo: TMemo;
    StartButton: TButton;
    StopButton: TButton;
    procedure StartButtonClick(Sender: TObject);
    procedure StopButtonClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
    FStringSectInit: boolean;
    FStringBuf: TStringList;
    StringSection: TRTLCriticalSection;
    procedure UpdateButtons;
    procedure HandleNewData(var Message: TMessage); message WM_DATA_IN_BUF;
  public
    { Public declarations }
  end;

var
  PalFrm: TPalFrm;

implementation

uses PrimeThreads;

{$R *.DFM}

procedure TPalFrm.UpdateButtons;
begin
  StopButton.Enabled := FStringSectInit;
  StartButton.Enabled := not FStringSectInit;
end;

procedure TPalFrm.StartButtonClick(Sender: TObject);
begin
  if not FStringSectInit then
  begin
    InitializeCriticalSection(StringSection);
    FStringBuf := TStringList.Create;
    FStringSectInit := true;
    StartThreads(Self, @StringSection, WM_DATA_IN_BUF, FStringBuf);
  end;
  UpdateButtons;
end;

procedure TPalFrm.StopButtonClick(Sender: TObject);
begin
  if FStringSectInit then
  begin
    ResultsMemo.Lines.Add('Please wait...');
    StopThreads;
    ResultsMemo.Lines.Add('Done!');
    FStringBuf.Free;
    FStringBuf := nil;
    DeleteCriticalSection(StringSection);
    FStringSectInit := false;
  end;
  UpdateButtons;
end;

procedure TPalFrm.HandleNewData(var Message: TMessage);
begin
  if FStringSectInit then
  begin
    EnterCriticalSection(StringSection);
    ResultsMemo.Lines.Add(FStringBuf.Strings[0]);
    FStringBuf.Delete(0);
    LeaveCriticalSection(StringSection);
    if ResultsMemo.Lines.Count > MaxMemoLines then
      ResultsMemo.Lines.Delete(0);
  end;
end;

procedure TPalFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  StopButtonClick(Self);
end;

end.


Source26

unit BoundedBuf;

{Martin Harvey 24/4/2000}

interface

uses Windows, SysUtils;

const
  DefaultWaitTime = 1000; { One second wait on all synchronisation primitives }

type
  { I don't particularly like dynamic arrays, so I'm going to do things
    the "C" way here, explicitly allocating memory
    Think of TBufferEntries as ^(array of pointer) }

  TBufferEntries = ^Pointer;

  TBoundedBuffer = class
  private
    FBufInit: boolean;
    FBufSize: integer;
    FBuf: TBufferEntries;
    FReadPtr, { ReadPtr points to next used entry in buffer}
    FWritePtr: integer; { WritePtr points to next free entry in buffer}
    FEntriesFree, FEntriesUsed: THandle; { Flow control semaphores }
    FCriticalMutex: THandle; { Critical section mutex }
    FEntryCountFree, FEntryCountUsed: integer; { Used for peeking operations }
  protected
    procedure SetSize(NewSize: integer);
    function ControlledWait(Semaphore: THandle): boolean;
    { Returns whether wait returned OK, or an error occurred }
  public
    procedure ResetState;
    destructor Destroy; override;
    function PutItem(NewItem: Pointer): boolean;
    function GetItem: Pointer;
    { New peeking operations. Note that we can't use simple properties, since
      we have to communicate success or failure of the operation, in addition
      to providing a result }
    function GetEntriesFree(var Free: integer): boolean;
    function GetEntriesUsed(var Used: integer): boolean;
  published
    property Size: integer read FBufSize write SetSize;
  end;

  { No constructor required because default values of 0, false etc acceptable }

implementation

procedure TBoundedBuffer.SetSize(NewSize: integer);

{ Initialises handles and allocates memory.
  If the buffer size has previously been set, then this may invoke a buffer
  reset }

begin
  if FBufInit then ResetState;
  if NewSize < 2 then NewSize := 2;
  FBufSize := NewSize;
  GetMem(FBuf, Sizeof(Pointer) * FBufSize);
  FillMemory(FBuf, Sizeof(Pointer) * FBufSize, 0);
  FCriticalMutex := CreateMutex(nil, false, nil); { note lack of name }
  WaitForSingleObject(FCriticalMutex, INFINITE);
  FBufInit := true;
  { The initial count on the semaphores requires some thought,
    The maximum count requires more thought.
    Again, all synchronisation objects are anonymous }
  FEntriesFree := CreateSemaphore(nil, FBufSize - 1, FBufSize, nil);
  FEntriesUsed := CreateSemaphore(nil, 0, FBufSize, nil);
  FEntryCountFree := FBufSize - 1;
  FEntryCountUsed := 0;
  ReleaseMutex(FCriticalMutex);
  if (FCriticalMutex = 0)
    or (FEntriesFree = 0)
    or (FEntriesUsed = 0) then ResetState
end;

procedure TBoundedBuffer.ResetState;

{ Closes handles and deallocates memory.
  Note that this must unblock threads in such a manner that they quit cleanly }

begin
  if FBufInit then
  begin
    WaitForSingleObject(FCriticalMutex, DefaultWaitTime);
    FBufInit := false;
    FBufSize := 0;
    FreeMem(FBuf);
    ReleaseSemaphore(FEntriesUsed, 1, nil);
    ReleaseSemaphore(FEntriesFree, 1, nil);
    CloseHandle(FEntriesFree);
    CloseHandle(FEntriesUsed);
    ReleaseMutex(FCriticalMutex);
    CloseHandle(FCriticalMutex);
  end;
end;

function TBoundedBuffer.ControlledWait(Semaphore: THandle): boolean;

var
  ErrCode: integer;

begin
  repeat
    ErrCode := WaitForSingleObject(Semaphore, DefaultWaitTime);
    if (ErrCode = WAIT_OBJECT_0) or (ErrCode = WAIT_ABANDONED) then
    begin
      { If wait abandoned, return failure. Buffer not properly cleaned up }
      result := ErrCode = WAIT_OBJECT_0;
      exit;
    end;
    { Wait timed out. Check whether buffer state initialised }
    if WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0 then
    begin
      result := false;
      exit;
    end
    else
    begin
      result := FBufInit;
      ReleaseMutex(FCriticalMutex);
    end;
  until not Result;
end;

function TBoundedBuffer.PutItem(NewItem: Pointer): boolean;

{ Called by producer thread }
var
  NthItem: TBufferEntries;

begin
  result := false;
  { WAIT(EntriesFree) }
  if not ControlledWait(FEntriesFree) then
    exit;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then { NB.This condition depends on L -> R lazy evaluation }
    exit;
  NthItem := FBuf;
  Inc(NthItem, FWritePtr);
  NthItem^ := NewItem;
  FWritePtr := (FWritePtr + 1) mod FBufSize;
  Inc(FEntryCountUsed);
  Dec(FEntryCountFree);
  ReleaseMutex(FCriticalMutex);
  { SIGNAL(EntriesUsed) }
  ReleaseSemaphore(FEntriesUsed, 1, nil);
  result := true;
end;

function TBoundedBuffer.GetItem: Pointer;

{ Called by consumer thread }
var
  NthItem: TBufferEntries;

begin
  result := nil;
  { WAIT(EntriesUsed) }
  if not ControlledWait(FEntriesUsed) then
    exit;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then { NB.This condition depends on L -> R lazy evaluation }
    exit;
  NthItem := FBuf;
  Inc(NthItem, FReadPtr);
  Result := NthItem^;
  FReadPtr := (FReadPtr + 1) mod FBufSize;
  Inc(FEntryCountFree);
  Dec(FEntryCountUsed);
  ReleaseMutex(FCriticalMutex);
  { SIGNAL(EntriesFree) }
  ReleaseSemaphore(FEntriesFree, 1, nil);
end;

destructor TBoundedBuffer.Destroy;
begin
  ResetState;
  inherited Destroy;
end;

function TBoundedBuffer.GetEntriesFree(var Free: integer): boolean;
begin
  result := false;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then
    exit;
  Free := FEntryCountFree;
  result := true;
  ReleaseMutex(FCriticalMutex);
end;

function TBoundedBuffer.GetEntriesUsed(var Used: integer): boolean;
begin
  result := false;
  if (WaitForSingleObject(FCriticalMutex, DefaultWaitTime) <> WAIT_OBJECT_0)
    or not FBufInit then
    exit;
  Used := FEntryCountUsed;
  result := true;
  ReleaseMutex(FCriticalMutex);
end;

end.


Source27

unit BiDirBuf;

{Martin Harvey 7/5/2000}

interface

uses BoundedBuf;

type
  TBufferSide = (bsSideA, bsSideB);
  TBufferOp = (boWriting, boReading);

  TBiDirBuf = class
  private
    FAtoBBuf, FBtoABuf: TBoundedBuffer;
  protected
    function GetBuf(Side: TBufferSide; Op: TBufferOp): TBoundedBuffer;
    function GetSize: integer;
    procedure SetSize(NewSize: integer);
  public
    constructor Create;
    destructor Destroy; override;
    procedure ResetState;
    function PutItem(Side: TBufferSide; Item: pointer): boolean;
    function GetItem(Side: TBufferSide): pointer;
    { Entries used function peeks buffer one is reading from, and
      Entried free function peeks buffer one is writing to. It seems
      a bit useless to allow the other two operations: why worry about
      your neighbour when you have plenty else to worry about? }
    function GetEntriesUsed(Side: TBufferSide; var Used: integer): boolean;
    function GetEntriesFree(Side: TBufferSide; var Free: integer): boolean;
  published
    property Size: integer read GetSize write SetSize;
  end;

implementation

{ TBiDirBuf }

constructor TBiDirBuf.Create;
begin
  inherited Create;
  FAToBBuf := TBoundedBuffer.Create;
  FBToABuf := TBoundedBuffer.Create;
end;

destructor TBiDirBuf.Destroy;
begin
  FAToBBuf.Free;
  FBToABuf.Free;
  inherited Destroy;
end;

procedure TBiDirBuf.ResetState;
begin
  FAToBBuf.ResetState;
  FBToABuf.ResetState;
end;

function TBiDirBuf.GetBuf(Side: TBufferSide; Op: TBufferOp): TBoundedBuffer;
begin
  if ((Side = bsSideA) and (Op = boWriting))
    or ((Side = bsSideB) and (Op = boReading)) then
    result := FAToBBuf
  else if ((Side = bsSideA) and (Op = boReading))
    or ((Side = bsSideB) and (Op = boWriting)) then
    result := FBToABuf
  else
  begin
    result := FAToBBuf;
    Assert(false);
  end;
end;

function TBidirBuf.GetSize: integer;
begin
  Assert(FAToBBuf.Size = FBToABuf.Size);
  result := FAToBBuf.Size;
end;

procedure TBiDirBuf.SetSize(NewSize: integer);
begin
  FAToBBuf.Size := NewSize;
  FBToABuf.Size := NewSize;
  Assert(FAToBBuf.Size = FBToABuf.Size);
end;

function TBiDirBuf.PutItem(Side: TBufferSide; Item: Pointer): boolean;
begin
  result := GetBuf(Side, boWriting).PutItem(Item);
end;

function TBiDirBuf.GetItem(Side: TBufferSide): Pointer;
begin
  result := GetBuf(Side, boReading).GetItem;
end;

function TBiDirBuf.GetEntriesUsed(Side: TBufferSide; var Used: integer): boolean;
begin
  result := GetBuf(Side, boReading).GetEntriesUsed(Used);
end;

function TBiDirBuf.GetEntriesFree(Side: TBufferSide; var Free: integer): boolean;
begin
  result := GetBuf(Side, boWriting).GetEntriesFree(Free);
end;

end.


Source28

unit BlockToAsyncBuf;

{ Martin Harvey 10/5/2000 }

interface

uses Classes, Forms, Messages, Windows, BiDirBuf;

const
  InternalBufferSize = 4;
  WM_BLOCK_ASYNC = WM_USER + 2876;

type

  { With this component, as with previous buffering schemes, one cannot read
    or write nil pointers. }

  TThreadNotify = (tnReaderDataFlow, tnWriterDataFlow);

  TBlockAsyncThread = class(TThread)
  private
    FDataSection: TRTLCriticalSection;
    FIdleSemaphore: THandle;
    FInterimBuf: Pointer;
    FOnDataFlow: TNotifyEvent;
    FBuffer: TBiDirBuf;
  protected
    procedure DataFlow; virtual;
    function GetItemsInTransit: integer;
  public
    constructor Create(CreateSuspended: boolean);
    destructor Destroy; override;
  published
    property OnDataFlow: TNotifyEvent read FOnDataFlow write FOnDataFlow;
    property Buffer: TBiDirBuf write FBuffer;
    property ItemsInTransit: integer read GetItemsInTransit;
  end;

  TBAWriterThread = class(TBlockAsyncThread)
  private
  protected
    procedure Execute; override;
  public
    function WriteItem(Item: Pointer): boolean;
  published
  end;

  TBAReaderThread = class(TBlockAsyncThread)
  private
  protected
    procedure Execute; override;
  public
    function ReadItem: pointer;
  published
  end;

  TBlockToAsyncBuf = class(TComponent)
  private
    FHWND: THandle;
    FBuffer: TBiDirBuf;
    FReaderThread: TBAReaderThread;
    FWriterThread: TBAWriterThread;
    FOnRead, FOnWrite: TNotifyEvent;
  protected
    procedure MessageHandler(var Msg: TMessage);
    procedure ReaderDataFlow(Sender: TObject);
    procedure WriterDataFlow(Sender: TObject);
    procedure Read; virtual;
    procedure Write; virtual;
    function GetItemsInTransit: integer;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function BlockingRead: pointer;
    function BlockingWrite(Item: pointer): boolean;
    function AsyncRead: pointer;
    function AsyncWrite(Item: pointer): boolean;
    procedure ResetState;
  published
    property OnRead: TNotifyEvent read FOnRead write FOnRead;
    property OnWrite: TNotifyEvent read FOnWrite write FOnWrite;
    property ItemsInTransit: integer read GetItemsInTransit;
  end;

implementation

procedure TBlockAsyncThread.DataFlow;
begin
  if Assigned(FOnDataFlow) then FOnDataFlow(Self);
end;

constructor TBlockAsyncThread.Create(CreateSuspended: boolean);
begin
  inherited Create(CreateSuspended);
  InitializeCriticalSection(FDataSection);
  FIdleSemaphore := CreateSemaphore(nil, 0, High(Integer), nil);
end;

destructor TBlockAsyncThread.Destroy;
begin
  ReleaseSemaphore(FIdleSemaphore, 1, nil);
  WaitFor;
  DeleteCriticalSection(FDataSection);
  CloseHandle(FIdleSemaphore);
end;

function TBlockAsyncThread.GetItemsInTransit: integer;
begin
  EnterCriticalSection(FDataSection);
  if Assigned(FInterimBuf) then
    result := 1
  else
    result := 0;
  LeaveCriticalSection(FDataSection);
end;

{ Buffer error handling needs to be discussed }

procedure TBAWriterThread.Execute;

var
  Temp: Pointer;

begin
  while not Terminated do
  begin
    DataFlow;
    WaitForSingleObject(FIdleSemaphore, INFINITE);
    EnterCriticalSection(FDataSection);
    Temp := FInterimBuf;
    FInterimBuf := nil;
    LeaveCriticalSection(FDataSection);
    if not FBuffer.PutItem(bsSideA, Temp) then Terminate;
  end;
end;

function TBAWriterThread.WriteItem(Item: Pointer): boolean;
begin
  result := false;
  EnterCriticalSection(FDataSection);
  if not Assigned(FInterimBuf) then
  begin
    FInterimBuf := Item;
    result := true;
  end;
  LeaveCriticalSection(FDataSection);
  if Result then ReleaseSemaphore(FIdleSemaphore, 1, nil);
end;

procedure TBAReaderThread.Execute;

var
  Temp: Pointer;

begin
  while not Terminated do
  begin
    Temp := FBuffer.GetItem(bsSideA);
    if Assigned(Temp) then
    begin
      EnterCriticalSection(FDataSection);
      FInterimBuf := Temp;
      LeaveCriticalSection(FDataSection);
      DataFlow;
      WaitForSingleObject(FIdleSemaphore, INFINITE);
    end
    else Terminate;
  end;
end;

function TBAReaderThread.ReadItem: pointer;
begin
  EnterCriticalSection(FDataSection);
  result := FInterimBuf;
  LeaveCriticalSection(FDataSection);
  if Assigned(Result) then ReleaseSemaphore(FIdleSemaphore, 1, nil);
end;

procedure TBlockToAsyncBuf.MessageHandler(var Msg: TMessage);
begin
  if (Msg.Msg = WM_BLOCK_ASYNC) then
  begin
    case TThreadNotify(Msg.LParam) of
      tnReaderDataflow: Read;
      tnWriterDataflow: Write;
    else
      Assert(false);
    end;
  end;
end;

procedure TBlockToAsyncBuf.ReaderDataFlow(Sender: TObject);
begin
  PostMessage(FHWND, WM_BLOCK_ASYNC, 0, Integer(tnReaderDataflow));
end;

procedure TBlockToAsyncBuf.WriterDataFlow(Sender: TObject);
begin
  PostMessage(FHWND, WM_BLOCK_ASYNC, 0, Integer(tnWriterDataflow));
end;

procedure TBlockToAsyncBuf.Read;
begin
  if Assigned(FOnRead) then FOnRead(Self);
end;

procedure TBlockToAsyncBuf.Write;
begin
  if Assigned(FOnWrite) then FOnWrite(Self);
end;

constructor TBlockToAsyncBuf.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FHWND := AllocateHWnd(MessageHandler);
  FBuffer := TBiDirBuf.Create;
  FBuffer.Size := InternalBufferSize;
  FReaderThread := TBAReaderThread.Create(true);
  FReaderThread.Buffer := Self.FBuffer;
  FReaderThread.OnDataFlow := ReaderDataFlow;
  FWriterThread := TBAWriterThread.Create(true);
  FWriterThread.Buffer := Self.FBuffer;
  FWriterThread.OnDataFlow := WriterDataFlow;
  FReaderThread.Resume;
  FWriterThread.Resume;
end;

procedure TBlockToAsyncBuf.ResetState;
begin
  if Assigned(FReaderThread) then FReaderThread.Terminate;
  if Assigned(FWriterThread) then FWriterThread.Terminate;
  FBuffer.ResetState;
  FReaderThread.Free;
  FWriterThread.Free;
  FReaderThread := nil;
  FWriterThread := nil;
end;

destructor TBlockToAsyncBuf.Destroy;
begin
  { A few destruction subtleties here }
  ResetState;
  FBuffer.Free;
  DeallocateHWnd(FHWND);
  inherited Destroy;
end;

function TBlockToAsyncBuf.BlockingRead: pointer;
begin
  result := FBuffer.GetItem(bsSideB);
end;

function TBlockToAsyncBuf.BlockingWrite(Item: pointer): boolean;
begin
  result := FBuffer.PutItem(bsSideB, Item);
end;

function TBlockToAsyncBuf.AsyncRead: pointer;
begin
  result := FReaderThread.ReadItem;
end;

function TBlockToAsyncBuf.AsyncWrite(Item: pointer): boolean;
begin
  result := FWriterThread.WriteItem(Item);
end;

function TBlockToAsyncBuf.GetItemsInTransit: integer;

var
  Entries: integer;

begin
  result := FReaderThread.ItemsInTransit + FWriterThread.ItemsInTransit;
  if FBuffer.GetEntriesUsed(bsSideA, Entries) then
    Inc(result, Entries);
  if FBuffer.GetEntriesUsed(bsSideB, Entries) then
    Inc(result, Entries);
end;

end.


Source29

unit BlockAsyncForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, BlockToAsyncBuf, PrimeRangeThread;

const
  MaxCount = 20;

type
  TBlockAsyncFrm = class(TForm)
    Label1: TLabel;
    StartRangeEdit: TEdit;
    Label2: TLabel;
    EndRangeEdit: TEdit;
    SubmitBtn: TButton;
    ResultsMemo: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure SubmitBtnClick(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    FItemsInTransit: integer;
    FBuf: TBlockToAsyncBuf;
    FWorkerThread: TPrimeRangeThread;
    procedure BufRead(Sender: TObject);
    procedure BufWrite(Sender: TObject);
  public
    { Public declarations }
  end;

var
  BlockAsyncFrm: TBlockAsyncFrm;

implementation

{$R *.DFM}

procedure TBlockAsyncFrm.FormCreate(Sender: TObject);
begin
  FWorkerThread := TPrimeRangeThread.Create(true);
  FBuf := TBlockToAsyncBuf.Create(Self);
  with FBuf do
  begin
    { Note that these changes will take effect before
      events from this component could possibly occur }
    OnRead := BufRead;
    OnWrite := BufWrite;
  end;
  SetThreadPriority(FWorkerThread.Handle, THREAD_PRIORITY_BELOW_NORMAL);
  with FWorkerThread do
  begin
    Buf := FBuf;
    Resume;
  end;
end;

procedure TBlockAsyncFrm.SubmitBtnClick(Sender: TObject);

var
  Request: PRangeRequestType;
  Temp: integer;

begin
  New(Request);
  try
    Request.Low := StrToInt(StartRangeEdit.Text);
    Request.High := StrToInt(EndRangeEdit.Text);
    if Request.Low > Request.High then
    begin
      Temp := Request.Low;
      Request.Low := Request.High;
      Request.High := Temp;
    end;
    if FBuf.AsyncWrite(Request) then
    begin
      Request := nil;
      SubmitBtn.Enabled := false;
      Inc(FItemsInTransit);
    end;
  finally
    if Assigned(Request) then Dispose(Request);
  end;
end;

procedure TBlockAsyncFrm.BufWrite(Sender: TObject);
begin
  { Buffer has indicated that there is space for us to write }
  SubmitBtn.Enabled := true;
end;

procedure TBlockAsyncFrm.BufRead(Sender: TObject);

var
  Reply: TStringList;

begin
  { We have received a notification that we may read. }
  Reply := TStringList(FBuf.AsyncRead);
  if Assigned(Reply) then
  begin
    Dec(FItemsInTransit);
    ResultsMemo.Lines.BeginUpdate;
    ResultsMemo.Lines.AddStrings(Reply);
    while ResultsMemo.Lines.Count > MaxCount do
      ResultsMemo.Lines.Delete(0);
    ResultsMemo.Lines.EndUpdate;
  end;
end;

procedure TBlockAsyncFrm.FormCloseQuery(Sender: TObject;
  var CanClose: Boolean);
begin
  CanClose := true;
  if FItemsInTransit > 0 then
    if MessageDlg('Some requests in transit, close anyway?', mtWarning,
      mbOKCancel, 0) <> mrOK then
      CanClose := false;
end;

procedure TBlockAsyncFrm.FormDestroy(Sender: TObject);
begin
  FWorkerThread.Terminate;
  FBuf.ResetState;
  FWorkerThread.WaitFor;
  FBuf.Free;
  FWorkerThread.Free;
end;

end.


Source30

unit PrimeRangeThread;

interface

uses
  Classes, BlockToAsyncBuf;

type
  TPrimeRangeThread = class(TThread)
  private
    { Private declarations }
    FBuf: TBlockToAsyncBuf;
  protected
    function IsPrime(TestNum: integer): boolean;
    procedure Execute; override;
  public
  published
    property Buf: TBlockToAsyncBuf read FBuf write FBuf;
  end;

  TRangeRequestType = record
    Low, High: integer;
  end;

  PRangeRequestType = ^TRangeRequestType;

  { Results returned in a string list }

implementation

uses SysUtils;

{ TPrimeRangeThread }

function TPrimeRangeThread.IsPrime(TestNum: integer): boolean;

var
  iter: integer;

begin
  result := true;
  if TestNum < 0 then
    result := false;
  if TestNum <= 2 then
    exit;
  iter := 2;
  while (iter < TestNum) and (not terminated) do {Line A}
  begin
    if (TestNum mod iter) = 0 then
    begin
      result := false;
      exit;
    end;
    Inc(iter);
  end;
end;

procedure TPrimeRangeThread.Execute;

var
  PRange: PRangeRequestType;
  TestNum: integer;
  Results: TStringList;

begin
  while not Terminated do
  begin
    PRange := PRangeRequestType(FBuf.BlockingRead);
    if Assigned(PRange) then
    begin
      Assert(PRange.Low <= PRange.High);
      Results := TStringList.Create;
      Results.Add('Primes from: ' + IntToStr(PRange.Low) +
        ' to: ' + IntToStr(PRange.High));
      for TestNum := PRange.Low to PRange.High do
      begin
        if IsPrime(TestNum) then
          Results.Add(IntToStr(TestNum) + ' is prime.');
      end;
      if not FBuf.BlockingWrite(Results) then
      begin
        Results.Free;
        Terminate;
      end;
    end
    else Terminate;
  end;
end;

end.


Source31

unit SimpleSync;

{ Martin Harvey 27/5/2000 }

interface

uses Windows;

type
  TSimpleSynchronizer = class(TObject)
  private
    FDataLock, FWriteLock: TRTLCriticalSection;
    FActRead, FReadRead, FActWrite, FWriteWrite: integer;
    FReaderSem, FWriterSem: THandle;
  protected
  public
    constructor Create;
    destructor Destroy; override;
    procedure StartRead;
    procedure StartWrite;
    procedure EndRead;
    procedure EndWrite;
  published
  end;

implementation

constructor TSimpleSynchronizer.Create;
begin
  inherited Create;
  InitializeCriticalSection(FDataLock);
  InitializeCriticalSection(FWriteLock);
  FReaderSem := CreateSemaphore(nil, 0, High(Integer), nil);
  FWriterSem := CreateSemaphore(nil, 0, High(Integer), nil);
  { Initial values of 0 OK for all counts }
end;

destructor TSimpleSynchronizer.Destroy;
begin
  DeleteCriticalSection(FDataLock);
  DeleteCriticalSection(FWriteLock);
  CloseHandle(FReaderSem);
  CloseHandle(FWriterSem);
  inherited Destroy;
end;

procedure TSimpleSynchronizer.StartRead;
begin
  EnterCriticalSection(FDataLock);
  Inc(FActRead);
  if FActWrite = 0 then
  begin
    Inc(FReadRead);
    ReleaseSemaphore(FReaderSem, 1, nil);
  end;
  LeaveCriticalSection(FDataLock);
  WaitForSingleObject(FReaderSem, INFINITE);
end;

procedure TSimpleSynchronizer.StartWrite;
begin
  EnterCriticalSection(FDataLock);
  Inc(FActWrite);
  if FReadRead = 0 then
  begin
    Inc(FWriteWrite);
    ReleaseSemaphore(FWriterSem, 1, nil);
  end;
  LeaveCriticalSection(FDataLock);
  WaitForSingleObject(FWriterSem, INFINITE);
  EnterCriticalSection(FWriteLock);
end;

procedure TSimpleSynchronizer.EndRead;
begin
  EnterCriticalSection(FDataLock);
  Dec(FReadRead);
  Dec(FActRead);
  if FReadRead = 0 then
  begin
    while FWriteWrite < FActWrite do
    begin
      Inc(FWriteWrite);
      ReleaseSemaphore(FWriterSem, 1, nil);
    end;
  end;
  LeaveCriticalSection(FDataLock);
end;

procedure TSimpleSynchronizer.EndWrite;
begin
  LeaveCriticalSection(FWriteLock);
  EnterCriticalSection(FDataLock);
  Dec(FWriteWrite);
  Dec(FActWrite);
  if FActWrite = 0 then
  begin
    while FReadRead < FActRead do
    begin
      Inc(FReadRead);
      ReleaseSemaphore(FReaderSem, 1, nil);
    end;
  end;
  LeaveCriticalSection(FDataLock);
end;

end.


Source32

unit ChecksumList;

{ Martin Harvey 29/5/2000 }

interface

uses SimpleSync, Classes, SysUtils;

type
  TChecksumList = class
  private
    FCheckList: TList;
    FSync: TSimpleSynchronizer;
  protected
    { Find function returns -1 if not found }
    function FindFileIndex(FileName: string): integer;
    function NoLockGetFileList: TStringList;
    function NoLockGetChecksum(FileName: string): integer;
  public
    constructor Create;
    destructor Destroy; override;
    procedure SetChecksum(FileName: string; Checksum: integer);
    procedure RemoveChecksum(FileName: string);
    function GetChecksum(FileName: string): integer;
    function GetFileList: TStringList;
    function GetChecksumList: TStringList;
  end;

implementation

type
  TCheckSum = record
    FileName: string;
    Checksum: integer;
  end;

  PCheckSum = ^TCheckSum;

constructor TChecksumList.Create;
begin
  inherited Create;
  FCheckList := TList.Create;
  FSync := TSimpleSynchronizer.Create;
end;

destructor TCheckSumList.Destroy;

var
  iter: integer;
  CurSum: PCheckSum;

begin
  if FCheckList.Count > 0 then
  begin
    for iter := 0 to FCheckList.Count - 1 do
    begin
      CurSum := PCheckSum(FCheckList.Items[iter]);
      if Assigned(CurSum) then Dispose(CurSum);
    end;
  end;
  FCheckList.Free;
  FSync.Free;
  inherited Destroy;
end;

function TCheckSumList.FindFileIndex(FileName: string): integer;

var
  iter: integer;
  CurSum: PCheckSum;

begin
  result := -1;
  if FCheckList.Count > 0 then
  begin
    for iter := 0 to FCheckList.Count - 1 do
    begin
      CurSum := PCheckSum(FCheckList.Items[iter]);
      Assert(Assigned(CurSum));
      if AnsiCompareText(FileName, CurSum.FileName) = 0 then
      begin
        result := iter;
        exit;
      end;
    end;
  end;
end;

procedure TCheckSumList.SetChecksum(FileName: string; Checksum: integer);

var
  CurSum: PCheckSum;
  CurIndex: integer;

begin
  FSync.StartWrite;
  CurIndex := FindFileIndex(FileName);
  if CurIndex >= 0 then
    CurSum := PCheckSum(FCheckList.Items[CurIndex])
  else
  begin
    New(CurSum);
    FCheckList.Add(CurSum);
  end;
  CurSum.FileName := FileName;
  CurSum.Checksum := Checksum;
  FSync.EndWrite;
end;

procedure TCheckSumList.RemoveChecksum(FileName: string);

var
  CurIndex: integer;

begin
  FSync.StartWrite;
  CurIndex := FindFileIndex(FileName);
  if CurIndex >= 0 then
  begin
    FCheckList.Delete(CurIndex);
    FCheckList.Pack;
  end;
  FSync.EndWrite;
end;

function TCheckSumList.NoLockGetChecksum(FileName: string): integer;

var
  CurIndex: integer;
  CurSum: PCheckSum;

begin
  result := 0;
  CurIndex := FindFileIndex(FileName);
  if CurIndex >= 0 then
  begin
    CurSum := PCheckSum(FCheckList.Items[CurIndex]);
    Assert(Assigned(CurSum));
    result := CurSum.Checksum;
  end;
end;

function TCheckSumList.GetChecksum(FileName: string): integer;
begin
  FSync.StartRead;
  result := NoLockGetChecksum(FileName);
  FSync.EndRead;
end;

function TCheckSumList.NoLockGetFileList: TStringList;

var
  iter: integer;
  CurSum: PCheckSum;

begin
  result := TStringList.Create;
  if FCheckList.Count > 0 then
  begin
    for iter := 0 to FCheckList.Count - 1 do
    begin
      CurSum := PCheckSum(FCheckList.Items[iter]);
      Assert(Assigned(CurSum));
      result.Add(CurSum.FileName);
    end;
  end;
  result.Sort;
end;

function TCheckSumList.GetFileList: TStringList;
begin
  FSync.StartRead;
  result := NoLockGetFileList;
  FSync.EndRead;
end;

function TCheckSumList.GetChecksumList: TStringList;

var
  iter: integer;

begin
  FSync.StartRead;
  result := NoLockGetFileList;
  if result.Count > 0 then
  begin
    for iter := 0 to result.Count - 1 do
    begin
      result.strings[iter] := result.strings[iter]
        + ' ' + IntToStr(NoLockGetChecksum(result.strings[iter]));
    end;
  end;
  FSync.EndRead;
end;

end.


Source33

unit CheckThread;

{ Martin Harvey 30/5/2000 }

interface

uses
  Classes, Windows, ChecksumList, SysUtils;

type

  TState = (sGetCurrentCRCs,
    sBuildFileList,
    sRemoveCRCs,
    sCheckFile,
    sDone);

  TStateReturn = (rvOK, rvFail1, rvFail2);

  TActionFunc = function: TStateReturn of object;

  TStateActions = array[TState] of TActionFunc;

  TNextStates = array[TState, TStateReturn] of TState;

  TCheckThread = class(TThread)
  private
    FStartDir: string;
    FCurrentState: TState;
    FActionFuncs: TStateActions;
    FNextStates: TNextStates;
    FInternalFileList: TStringList;
    FExternalFileList: TStringList;
    FExternalCRCList: TStringList;
    FCheckList: TChecksumList;
    FFileToProcess: integer;
  protected
    procedure InitActionFuncs;
    procedure InitNextStates;
    function GetCurrentCRCs: TStateReturn;
    function BuildFileList: TStateReturn;
    function RemoveCRCs: TStateReturn;
    function CheckFile: TStateReturn;
    procedure Execute; override;
  public
    constructor Create(CreateSuspended: boolean);
    destructor Destroy; override;
    property StartDir: string read FStartDir write FStartDir;
    property CheckList: TChecksumList read FCheckList write FCheckList;
  end;

implementation

{ TCheckThread }

{(*} {Prettyprinter auto-formatting off}

const
  BaseStateTransitions:TNextStates = (
                      {rvOK}             {rvFail1}        {rvFail2}
{sGetCurrentCRCs } ( sBuildFileList,      sDone,           sDone ),
{sBuildFileList  } ( sRemoveCRCs,         sDone,           sDone ),
{sRemoveCRCs     } ( sCheckFile,          sDone,           sDone ),
{sCheckFile      } ( sCheckFile,          sGetCurrentCRCs, sDone ),
{sDone           } ( sDone,               sDone,           sDone ));

 {*)}{Prettyprinter auto-formatting on}

procedure TCheckThread.InitActionFuncs;
begin
  FActionFuncs[sGetCurrentCRCs] := GetCurrentCRCs;
  FActionFuncs[sBuildFileList] := BuildFileList;
  FActionFuncs[sRemoveCRCs] := RemoveCRCs;
  FActionFuncs[sCheckFile] := CheckFile;
end;

procedure TCheckThread.InitNextStates;
begin
  FNextStates := BaseStateTransitions;
end;

function TCheckThread.GetCurrentCRCs: TStateReturn;
begin
  FExternalFileList.Free;
  FExternalFileList := nil;
  FExternalCRCList.Free;
  FExternalCRCList := nil;

  FExternalFileList := FCheckList.GetFileList;
  FExternalCRCList := FCheckList.GetChecksumList;
  result := rvOK;
end;

function TCheckThread.BuildFileList: TStateReturn;

var
  FindRet: integer;
  SearchRec: TSearchRec;

begin
  FInternalFileList.Clear;
  FindRet := FindFirst(StartDir + '*.*', faAnyFile and not faDirectory, SearchRec);
  if FindRet <> 0 then
    result := rvFail1
  else
  begin
    while FindRet = 0 do
    begin
      { Found a file.}
      FInternalFileList.Add(SearchRec.Name);
      FindRet := FindNext(SearchRec);
    end;
    result := rvOK;
  end;
  FindClose(SearchRec);
  FFileToProcess := 0;
end;

function TCheckThread.RemoveCRCs: TStateReturn;

var
  iter: integer;
  dummy: integer;

begin
  FInternalFileList.Sort;
  FExternalFileList.Sort;
  if FExternalFileList.Count > 0 then
  begin
    for iter := 0 to FExternalFileList.Count - 1 do
    begin
      if not FInternalFileList.Find(FExternalFileList[iter], dummy) then
        FCheckList.RemoveChecksum(FExternalFileList[iter]);
    end;
  end;
  result := rvOK;

end;

function TCheckThread.CheckFile: TStateReturn;

var
  FileData: TFileStream;
  MemImage: TMemoryStream;
  Data: byte;
  Sum: integer;
  iter: integer;

begin
  if FFileToProcess >= FInternalFileList.Count then
  begin
    result := rvFail1;
    exit;
  end;
  Sum := 0;
  FileData := nil;
  MemImage := nil;
  try
    FileData := TFileStream.Create(StartDir + FInternalFileList[FFileToProcess],
      fmOpenRead or fmShareDenyWrite);
    FileData.Seek(0, soFromBeginning);
    MemImage := TMemoryStream.Create;
    MemImage.CopyFrom(FileData, FileData.Size);
    MemImage.Seek(0, soFromBeginning);
    for iter := 1 to FileData.Size do
    begin
      MemImage.ReadBuffer(Data, sizeof(Data));
      Inc(Sum, Data);
    end;
    FileData.Free;
    MemImage.Free;
    if (FCheckList.GetChecksum(FInternalFileList[FFileToProcess]) <> Sum) then
      FCheckList.SetChecksum(FInternalFileList[FFileTOProcess], Sum);
  except
    on EStreamError do
    begin
      FileData.Free;
      MemImage.Free;
    end;
  end;
  Inc(FFileToProcess);
  result := rvOK;
end;

procedure TCheckThread.Execute;
begin
  SetThreadPriority(Handle, THREAD_PRIORITY_IDLE);
  while not (Terminated or (FCurrentState = sDone)) do
    FCurrentState := FNextStates[FCurrentState, FActionFuncs[FCurrentState]];
end;

constructor TCheckThread.Create(CreateSuspended: boolean);
begin
  inherited Create(CreateSuspended);
  InitActionFuncs;
  InitNextStates;
  FInternalFileList := TStringList.Create;
end;

destructor TCheckThread.Destroy;
begin
  FInternalFileList.Free;
  FExternalFileList.Free;
  FExternalCRCList.Free;
  inherited Destroy;
end;

end.


source34

unit SyncForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls, ChecksumList, CheckThread;

type
  TForm1 = class(TForm)
    FileMemo: TMemo;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
    FChecksumList: TChecksumList;
    FCheckThread: TCheckThread;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);
begin
  FChecksumList := TChecksumList.Create;
  FCheckThread := TCheckThread.Create(true);
  with FCheckThread do
  begin
    StartDir := 'D:\Netscape Profiles\Martin\News\host-newsgroups.borland.com\';
    CheckList := FChecksumList;
    Resume;
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  with FCheckThread do
  begin
    Terminate;
    WaitFor;
    Free;
  end;
  FChecksumList.Free;
end;

procedure TForm1.Timer1Timer(Sender: TObject);

var
  TempList: TStringList;

begin
  TempList := FChecksumList.GetChecksumList;
  with FileMemo do
  begin
    with Lines do
    begin
      BeginUpdate;
      Assign(TempList);
      EndUpdate;
    end;
    selstart := gettextlen;
    perform(em_scrollcaret, 0, 0);
  end;
  TempList.Free;
end;

end.


Source35

unit SimulatedEvent;

{ Martin Harvey 4/6/2000 }

interface

uses Windows;

type
  TSimulatedEvent = class
  private
    FBlockCount: integer;
    FSignalled: boolean;
    FDataSection: TRTLCriticalSection;
    FBlockSem: THandle;
  protected
  public
    constructor Create(CreateSignalled: boolean);
    destructor Destroy; override;
    procedure SetEvent;
    procedure ResetEvent;
    procedure PulseEvent;
    procedure WaitFor;
  published
  end;

implementation

constructor TSimulatedEvent.Create(CreateSignalled: boolean);
begin
  inherited Create;
  FSignalled := CreateSignalled;
  InitializeCriticalSection(FDataSection);
  FBlockSem := CreateSemaphore(nil, 0, High(Integer), nil);
end;

destructor TSimulatedEvent.Destroy;
begin
  DeleteCriticalSection(FDataSection);
  CloseHandle(FBlockSem);
  inherited Destroy;
end;

procedure TSimulatedEvent.SetEvent;
begin
  EnterCriticalSection(FDataSection);
  FSignalled := true;
  while FBlockCount > 0 do
  begin
    ReleaseSemaphore(FBlockSem, 1, nil);
    Dec(FBlockCount);
  end;
  LeaveCriticalSection(FDataSection);
end;

procedure TSimulatedEvent.ResetEvent;
begin
  EnterCriticalSection(FDataSection);
  FSignalled := false;
  LeaveCriticalSection(FDataSection);
end;

procedure TSimulatedEvent.PulseEvent;
begin
  EnterCriticalSection(FDataSection);
  while FBlockCount > 0 do
  begin
    ReleaseSemaphore(FBlockSem, 1, nil);
    Dec(FBlockCount);
  end;
  LeaveCriticalSection(FDataSection);
end;

procedure TSimulatedEvent.WaitFor;
begin
  EnterCriticalSection(FDataSection);
  if FSignalled then
  begin
    Dec(FBlockCOunt);
    ReleaseSemaphore(FBlockSem, 1, nil);
  end;
  Inc(FBlockCount);
  LeaveCriticalSection(FDataSection);
  WaitForSingleObject(FBlockSem, INFINITE);
end;

end.


Source36

unit EventSync;

{ Martin Harvey 5/6/2000 }

interface

uses Windows;

type
  TEventSynchronizer = class(TObject)
  private
    FDataLock, FWriteLock: TRTLCriticalSection;
    FReaders, FWriters: integer;
    FNoReaders, FNoWriters: THandle;
  protected
  public
    constructor Create;
    destructor Destroy; override;
    procedure StartRead;
    procedure StartWrite;
    procedure EndRead;
    procedure EndWrite;
  published
  end;

implementation

constructor TEventSynchronizer.Create;
begin
  inherited Create;
  InitializeCriticalSection(FDataLock);
  InitializeCriticalSection(FWriteLock);
  FNoReaders := CreateEvent(nil, true, true, nil);
  FNoWriters := CreateEvent(nil, true, true, nil);
end;

destructor TEventSynchronizer.Destroy;
begin
  DeleteCriticalSection(FDataLock);
  DeleteCriticalSection(FWriteLock);
  CloseHandle(FNoReaders);
  CloseHandle(FNoWriters);
  inherited Destroy;
end;

procedure TEventSynchronizer.StartRead;

var
  Block: boolean;

begin
  EnterCriticalSection(FDatalock);
  if FReaders = 0 then
    ResetEvent(FNoReaders);
  Inc(FReaders);
  Block := FWriters > 0;
  LeaveCriticalSection(FDataLock);
  if Block then
    WaitForSingleObject(FNoWriters, INFINITE);
end;

procedure TEventSynchronizer.StartWrite;

var
  Block: boolean;

begin
  EnterCriticalSection(FDataLock);
  if FWriters = 0 then
    ResetEvent(FNoWriters);
  Inc(FWriters);
  Block := FReaders > 0;
  LeaveCriticalSection(FDataLock);
  if Block then
    WaitForSingleObject(FNoReaders, INFINITE);
  EnterCriticalSection(FWriteLock);
end;

procedure TEventSynchronizer.EndRead;
begin
  EnterCriticalSection(FDataLock);
  Dec(FReaders);
  if FReaders = 0 then
    SetEvent(FNoReaders);
  LeaveCriticalSection(FDataLock);
end;

procedure TEventSynchronizer.EndWrite;
begin
  LeaveCriticalSection(FWriteLock);
  EnterCriticalSection(FDataLock);
  Dec(FWriters);
  if FWriters = 0 then
    SetEvent(FNoWriters);
  LeaveCriticalSection(FDataLock);
end;

end.


Source37

var
  Lock:integer;

{Lock Initialization}
Lock := -1;

{Entering the spin lock}
while InterlockedIncrement(Lock)>0 do
begin
  Dec(Lock);
  Sleep(0);
end;

{Leaving the spin lock}
Dec(Lock);


Source38

procedure DoSomethingCritical(var Lock: integer);

var
  Temp: integer;

begin
{ Initialize lock }
  Lock := -1;
{ Enter Lock }
  repeat
    Inc(Lock);
    Temp := Lock;
    if Temp > 0 then
      Dec(Lock);
  until not (Temp > 0);
{ Perform operations }
{ Leave Lock }
  Dec(Lock);
end;

procedure AsmDoSomethingCritical(var Lock: integer);
asm
{ Initialize lock }
  lock mov dword ptr[eax],$FFFFFFFF
{ Enter Lock }
@spin:
  lock inc dword ptr[eax]
  mov edx,[eax]
  test edx,edx
  jng @skipdec
  lock dec dword ptr[eax]
@skipdec:
  test edx,edx
  jg @spin
{ Perform operations }
{ Leave Lock }
  lock dec dword ptr[eax]
end;


Source39

type
  TEventCount = class
  private
  protected
  public
    constructor Create;
    destructor Destroy; override;
    function Advance: integer;
    function Read: integer;
    procedure Await(WaitCount: integer);
  published
  end;

  TSequencer = class
  private
  protected
  public
    constructor Create;
    destructor Destroy; override;
    function Ticket: integer;
  published
  end;


Source40

{ Enforcing a mutual exclusion }

var
  MyTurn: integer;
  EC: TEventCount;
  S: TSequencer; { assume already created appropriately }

begin
  MyTurn := S.Ticket;
  EC.Await(MyTurn);
    {  Critical operations }
  EC.Advance;
end;


Source41

{ Single producer consumer bounded buffer }
{ buffer has N slots }

var
  InE, OutE: TEventCount; { Set up and initially 0 }

{ producer }

var
  I: integer;

begin
  while not terminated do
  begin
    OutE.Await(I - N);
    { insert item at I mod N }
    InE.Advance;
    Inc(I);
  end;
end;

{ consumer }

var
  I: integer;

begin
  while not terminated do
  begin
    InE.Await(I);
    { remove item at i mod N }
    OutE.Advance;
  end;
end;


Source42

{ Bounded buffer with multiple producers and consumers }

var
  InE, OutE: TEventCount; { Set up and initially 0 }
  PrTk, CnTk: TSequencer; { Set up and initially 0 }

{ producer }

var
  MyTurn: integer;

begin
  while not terminated do
  begin
    MyTurn := PrTk.Ticket;
    InE.Await(MyTurn);
    OutE.Await(MyTurn - N + 1);
    { insert item at myturn mod N }
    InE.Advance;
  end;
end;

{ consumer }

var
  MyTurn: integer;

begin
  while not terminated do
  begin
    MyTurn := CnTk.Ticket;
    OutE.Await(MyTurn);
    InE.Await(MyTurn + 1);
    { remove item at MyTurn mod N }
    OutE.Advance;
  end;
end;


Source43

library NumberDLL;

{ Martin Harvey 9/10/2000 }

var
  ANumber:integer;

function GetNumber:integer;stdcall;
begin
  Result := ANumber;
end;

procedure SetNumber(NewNumber:integer);stdcall;
begin
  ANumber := NewNumber;
end;

exports
  GetNumber,
  SetNumber;

begin
end.


Source44

unit NumberForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TNumberFrm = class(TForm)
    GetBtn: TButton;
    SetBtn: TButton;
    NumberEdit: TEdit;
    procedure GetBtnClick(Sender: TObject);
    procedure SetBtnClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  NumberFrm: TNumberFrm;

implementation

{$R *.DFM}

function GetNumber:integer;stdcall;external 'NumberDLL.dll';
procedure SetNumber(NewNumber:integer);stdcall;external 'NumberDLL.dll';

procedure TNumberFrm.GetBtnClick(Sender: TObject);
begin
  NumberEdit.Text := IntToStr(GetNumber);
end;

procedure TNumberFrm.SetBtnClick(Sender: TObject);
begin
  try
    SetNumber(StrToInt(NumberEdit.Text));
  except
    on EConvertError do ShowMessage('Not a valid number. Number not set.');
  end;
end;

end.


Source45

library InitDLL;

{Martin Harvey 9/10/2000}

uses
  SysUtils,
  Windows,
  Dialogs,
  TestUnit in 'TestUnit.pas';

procedure FancyMessage;stdcall;
begin
  TestUnit.UnitProc;
end;

procedure EntryPointFunc(Reason:integer);
begin
  case reason of
    DLL_PROCESS_ATTACH:ShowMessage('EntryPoint. Process Attach.');
    DLL_THREAD_ATTACH:ShowMessage('EntryPoint. Thread Attach.');
    DLL_PROCESS_DETACH:ShowMessage('EntryPoint. Process Detach.');
    DLL_THREAD_DETACH:ShowMessage('EntryPoint. Thread Detach.');
  else
    ShowMessage('EntryPoint. Unknown reason');
  end;
end;

exports FancyMessage;

begin
  ShowMessage('Main DLL Body.');
  DLLProc := @EntryPointFunc;
end.


Source46

unit TestUnit;

interface

procedure UnitProc;

implementation

uses Dialogs;

procedure UnitProc;
begin
  ShowMessage('Unit procedure');
end;

initialization
  ShowMessage('Unit initialization.');
finalization
  ShowMessage('Unit finalization.');
end.


Source47

unit InitForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TInitFrm = class(TForm)
    SpawnThrd: TButton;
    procedure SpawnThrdClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  InitFrm: TInitFrm;

implementation

uses DLLCaller;

{$R *.DFM}

procedure TInitFrm.SpawnThrdClick(Sender: TObject);

var
  CallerThread:TDLLCaller;

begin
  CallerThread:=TDLLCaller.Create(true);
  with CallerThread do
  begin
    FreeOnTerminate := true;
    Resume;
  end;
end;

end.


Source48

unit DLLCaller;

interface

uses
  Classes;

type
  TDLLCaller = class(TThread)
  private
    { Private declarations }
  protected
    procedure Execute; override;
  end;

implementation

{ TDLLCaller }

procedure FancyMessage;stdcall;external 'InitDLL.dll';

procedure TDLLCaller.Execute;
begin
  FancyMessage;
end;

end.


Source49

unit GlobalHdr;

interface

type
  TGlobalInfo = record
    GlobalProcesses: integer;
    GlobalThreads: integer;
    SharedNumber: integer;
  end;

implementation

end.


Source50

library GlobalData;

uses
  Windows,
  GlobalHdr in 'GlobalHdr.pas';

const
  GlobalMutexName = 'MCHGlobalMutex - BBARAgAGBQI4r8mOAAoJEPwzBQsIT9FngEA';
  GlobalMapName = 'MCHGlobalMap   - AJ9Qfi3m4vqgUhKYA7qBfzrMDbkpbQtTWFy';

type
  TLocalInfo = record
    LocalThreads: integer;
  end;

  TSyncHandles = record
    LocalSection: TRTLCriticalSection;
    GlobalMutex: THandle;
  end;

  PGlobalInfo = ^TGlobalInfo;

  TGlobalHandles = record
    FileMapping: THandle;
    GlobalInfo: PGlobalInfo;
  end;

var
  SyncHandles: TSyncHandles;
  LocalInfo: TLocalInfo;
  GlobalHandles: TGlobalHandles;

procedure ReadInfo(var GlobalInfo: TGlobalInfo; var LocalThreads: integer); stdcall;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  EnterCriticalSection(SyncHandles.LocalSection);
  GlobalInfo := GlobalHandles.GlobalInfo^;
  LocalThreads := LocalInfo.LocalThreads;
  LeaveCriticalSection(SyncHandles.LocalSection);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure SetSharedInteger(NewValue: integer); stdcall;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  GlobalHandles.GlobalInfo.SharedNumber := NewValue;
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure IncSharedInteger; stdcall;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  Inc(GlobalHandles.GlobalInfo.SharedNumber);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure DecSharedInteger; stdcall;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  Dec(GlobalHandles.GlobalInfo.SharedNumber);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

function AtomicIncThreadCount: integer;
{returns number of local threads for Delphi Memory Manager}
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  EnterCriticalSection(SyncHandles.LocalSection);
  Inc(GlobalHandles.GlobalInfo.GlobalThreads);
  Inc(LocalInfo.LocalThreads);
  result := LocalInfo.LocalThreads;
  LeaveCriticalSection(SyncHandles.LocalSection);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure AtomicDecThreadCount;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  EnterCriticalSection(SyncHandles.LocalSection);
  Dec(GlobalHandles.GlobalInfo.GlobalThreads);
  Dec(LocalInfo.LocalThreads);
  LeaveCriticalSection(SyncHandles.LocalSection);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure AtomicIncProcessCount;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  Inc(GlobalHandles.GlobalInfo.GlobalProcesses);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure AtomicDecProcessCount;
begin
  WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
  Dec(GlobalHandles.GlobalInfo.GlobalProcesses);
  ReleaseMutex(SyncHandles.GlobalMutex);
end;

procedure SetupSynchronisation;
begin
  with SyncHandles do
  begin
    InitializeCriticalSection(LocalSection);
    GlobalMutex := CreateMutex(nil, false, GlobalMutexName);
    Assert(GlobalMutex <> INVALID_HANDLE_VALUE);
  end;
end;

procedure CloseSynchronisation;
begin
  with SyncHandles do
  begin
    DeleteCriticalSection(LocalSection);
    CloseHandle(GlobalMutex);
  end;
end;

procedure SetupGlobalInfo;

var
  FirstCaller: boolean;

begin
  with GlobalHandles do
  begin
    WaitForSingleObject(SyncHandles.GlobalMutex, INFINITE);
    FileMapping := CreateFileMapping(High(Cardinal), nil, PAGE_READWRITE,
      0, sizeof(TGlobalInfo), GlobalMapName);
    Assert(FileMapping <> INVALID_HANDLE_VALUE);
    FirstCaller := GetLastError <> ERROR_ALREADY_EXISTS;
    GlobalInfo := MapViewOfFile(FileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
    Assert(GlobalInfo <> nil);
    if FirstCaller then FillMemory(GlobalInfo, sizeof(TGlobalInfo), 0);
    ReleaseMutex(SyncHandles.GlobalMutex);
  end;
end;

procedure CloseGlobalInfo;
begin
  with GlobalHandles do
  begin
    UnmapViewOfFile(GlobalInfo);
    CloseHandle(FileMapping);
  end;
end;

procedure DLLFinalisation;
begin
  AtomicDecThreadCount;
  AtomicDecProcessCount;
  CloseGlobalInfo;
  CloseSynchronisation;
end;

procedure EntryPoint(Reason: integer);
begin
  case Reason of
    DLL_THREAD_ATTACH: if AtomicIncThreadCount > 1 then IsMultiThread := true;
    DLL_THREAD_DETACH: AtomicDecThreadCount;
    DLL_PROCESS_DETACH: DLLFinalisation;
  else
    Assert(false);
  end;
end;

exports
  ReadInfo,
  SetSharedInteger,
  IncSharedInteger,
  DecSharedInteger;

begin
  SetupSynchronisation;
  SetupGlobalInfo;
  AtomicIncProcessCount;
  AtomicIncThreadCount;
  DLLProc := @EntryPoint;
end.


Source51

unit GlobalForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TGlobalFrm = class(TForm)
    GlobalProcessEdit: TEdit;
    GlobalThreadEdit: TEdit;
    LocalThreadEdit: TEdit;
    SharedDataEdit: TEdit;
    ReadBtn: TButton;
    IncBtn: TButton;
    DecBtn: TButton;
    SetBtn: TButton;
    CreateThread: TButton;
    procedure ReadBtnClick(Sender: TObject);
    procedure IncBtnClick(Sender: TObject);
    procedure DecBtnClick(Sender: TObject);
    procedure SetBtnClick(Sender: TObject);
    procedure CreateThreadClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  GlobalFrm: TGlobalFrm;

implementation

uses GlobalHdr, DelayThread;

procedure ReadInfo(var GlobalInfo: TGlobalInfo; var LocalThreads: integer);
  stdcall; external 'GlobalData.dll';

procedure SetSharedInteger(NewValue: integer);
  stdcall; external 'GlobalData.dll';

procedure IncSharedInteger;
  stdcall; external 'GlobalData.dll';

procedure DecSharedInteger;
  stdcall; external 'GlobalData.dll';

{$R *.DFM}

procedure TGlobalFrm.ReadBtnClick(Sender: TObject);

var
  GlobalInfo: TGlobalInfo;
  LocalThreads: integer;

begin
  ReadInfo(GlobalInfo, LocalThreads);
  GlobalProcessEdit.Text := IntToStr(GlobalInfo.GlobalProcesses);
  GlobalThreadEdit.Text := IntToStr(GlobalInfo.GlobalThreads);
  SharedDataEdit.Text := IntToStr(GlobalInfo.SharedNumber);

  LocalThreadEdit.Text := IntToStr(LocalThreads);
end;

procedure TGlobalFrm.IncBtnClick(Sender: TObject);
begin
  IncSharedInteger;
  ReadBtnClick(Sender);
end;

procedure TGlobalFrm.DecBtnClick(Sender: TObject);
begin
  DecSharedInteger;
  ReadBtnClick(Sender);
end;

procedure TGlobalFrm.SetBtnClick(Sender: TObject);

var
  NewInt: integer;

begin
  NewInt := StrToInt(SharedDataEdit.Text);
  SetSharedInteger(NewInt);
end;

procedure TGlobalFrm.CreateThreadClick(Sender: TObject);

var
  NewThread: TDelayThread;

begin
  NewThread := TDelayThread.Create(true);
  NewThread.FreeOnTerminate := true;
  NewThread.Resume;
end;

end.


Source52

unit DelayThread;

interface

uses
  Windows, Classes;

type
  TDelayThread = class(TThread)
  private
  protected
    procedure Execute; override;
  end;

implementation

{ TDelayThread }

procedure TDelayThread.Execute;
begin
  Sleep(5000);
end;

end.


Source53

unit GlobalForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, GlobalHdr;

type

  TReadInfoProc = procedure(var GlobalInfo: TGlobalInfo; var LocalThreads: integer); stdcall;
  TSetProc = procedure(NewValue: integer); stdcall;
  TIncDecProc = procedure; stdcall;

  TProcs = record
    ReadProc: TReadInfoProc;
    SetProc: TSetProc;
    IncProc, DecProc: TIncDecProc;
  end;

  TGlobalFrm = class(TForm)
    GlobalProcessEdit: TEdit;
    GlobalThreadEdit: TEdit;
    LocalThreadEdit: TEdit;
    SharedDataEdit: TEdit;
    ReadBtn: TButton;
    IncBtn: TButton;
    DecBtn: TButton;
    SetBtn: TButton;
    CreateThread: TButton;
    LoadDLLBtn: TButton;
    procedure ReadBtnClick(Sender: TObject);
    procedure IncBtnClick(Sender: TObject);
    procedure DecBtnClick(Sender: TObject);
    procedure SetBtnClick(Sender: TObject);
    procedure CreateThreadClick(Sender: TObject);
    procedure LoadDLLBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    Procs: TProcs;
    Loaded: boolean;
    LibHandle: THandle;
  public
    { Public declarations }
  end;

var
  GlobalFrm: TGlobalFrm;

implementation

uses DelayThread;

{$R *.DFM}

procedure TGlobalFrm.ReadBtnClick(Sender: TObject);

var
  GlobalInfo: TGlobalInfo;
  LocalThreads: integer;

begin
  if Loaded then
  begin
    Procs.ReadProc(GlobalInfo, LocalThreads);
    GlobalProcessEdit.Text := IntToStr(GlobalInfo.GlobalProcesses);
    GlobalThreadEdit.Text := IntToStr(GlobalInfo.GlobalThreads);
    SharedDataEdit.Text := IntToStr(GlobalInfo.SharedNumber);
    LocalThreadEdit.Text := IntToStr(LocalThreads);
  end;
end;

procedure TGlobalFrm.IncBtnClick(Sender: TObject);
begin
  if Loaded then
  begin
    Procs.IncProc;
    ReadBtnClick(Sender);
  end;
end;

procedure TGlobalFrm.DecBtnClick(Sender: TObject);
begin
  if Loaded then
  begin
    Procs.DecProc;
    ReadBtnClick(Sender);
  end;
end;

procedure TGlobalFrm.SetBtnClick(Sender: TObject);

var
  NewInt: integer;

begin
  if Loaded then
  begin
    NewInt := StrToInt(SharedDataEdit.Text);
    Procs.SetProc(NewInt);
  end;
end;

procedure TGlobalFrm.CreateThreadClick(Sender: TObject);

var
  NewThread: TDelayThread;

begin
  NewThread := TDelayThread.Create(true);
  NewThread.FreeOnTerminate := true;
  NewThread.Resume;
end;

procedure TGlobalFrm.LoadDLLBtnClick(Sender: TObject);
begin
  if not Loaded then
  begin
    LibHandle := LoadLibrary('GlobalData.dll');
    if LibHandle = INVALID_HANDLE_VALUE then exit;
    with Procs do
    begin
      ReadProc := GetProcAddress(LibHandle, 'ReadInfo');
      IncProc := GetProcAddress(LibHandle, 'IncSharedInteger');
      DecProc := GetProcAddress(LibHandle, 'DecSharedInteger');
      SetProc := GetProcAddress(LibHandle, 'SetSharedInteger');
      if not (Assigned(ReadProc) and Assigned(IncProc) and Assigned(DecProc)
        and Assigned(SetProc)) then
      begin
        FreeLibrary(LibHandle);
        LibHandle := INVALID_HANDLE_VALUE;
        exit;
      end;
      Loaded := true;
    end;
  end;
end;

procedure TGlobalFrm.FormCreate(Sender: TObject);
begin
  LibHandle := INVALID_HANDLE_VALUE;
end;

procedure TGlobalFrm.FormDestroy(Sender: TObject);
begin
  if LibHandle <> INVALID_HANDLE_VALUE then
    FreeLibrary(LibHandle);
end;

end.