. .
. . .
. .
.

Multithreaded Programming - Part 2.

Conch

In 1954, the English author, William Golding, wrote his most famous book, a book that would become a classic. It was called "Lord of the Flies". (Yes, this is part 2 of the multithreading tutorial - read on!) The book concerns a group of school boys marooned on an island when the plane they are travelling in is shot down.

Ralph, one of the boys, is elected the groups leader. They hold meetings to decide what to do, but these are chaotic because everybody is speaking at the same time. To solve the problem, Ralph makes it a rule, that only the person holding "The Conch", (a large seashell), may speak. When they have finished, they pass the conch to someone else, who then may speak.

Perhaps now you see why a programming tutorial starts like a literature lesson! Yes, the conch is being used as a synchronisation tool! If you understand the concept of the conch, then you will have no problems with synchronisation.

Many approaches to thread synchronisation involve acquiring and then releasing some kind of token or object. Windows provides 5 different tools for synchronisation, usually called synchronisation objects. Some are more sophisticated then others, not all are available, or fully featured in early releases of Windows.

The synchronisation object we will use in this program is the simplest, but least flexible type. For this kind of problem though, it is the right choice. It is fast, and acheives the desired aim. It is called a Critical Section.

Our aim in this program is actually very similar to Ralph's in the Lord of the Flies, we have 2 threads trying to "talk" to the screen at the same time. What we need to do is introduce The Conch! From part 1, you'll remember what I said about pre-empting a thread, it can happen at any instruction. We need to be sure that the cout statement starts, and completes without another thread outputting anything in the mean time.

----------

To use a CRITICAL_SECTION object, you must declare one. For this program we will simply declare it as a global variable.

It is important to remember when working with multithreaded programs, that although Windows schedules threads to run in much the same way as it schedules programs, your threads are not seperate programs, they are still a part of your program! As such, Func1() and Func2(), although running in seperate threads, can still access any global variable declared in your program in exactly the same way as a non threaded function could.

Here is the modified global area of our program.

#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;

void Func1(void *);
void Func2(void *);

CRITICAL_SECTION Section;

The only change is the declaration of the global CRITICAL_SECTION object which I chose to call Section. You can. of course, call it whatever you like, it could just as well be called "Conch" for example. CRITICAL_SECTION is a type defined in windows.h, (actually winbase.h, but winbase.h is included in windows.h!), so you can create variables of type CRITICAL_SECTION in just the same way as you can create an int for example.

----------

Before any calls can be made to use the critical section, it has to be initialised. Any thread can do this, but you must make sure this is done before you try to acquire the critical section. In our program, the most obvious place to do this is in the main() function before we spin any threads. You do this by calling InitializeCriticalSection(). This fucntion takes a pointer to the CRITICAL_SECTION object we wish to initialise, in this case, Section, so we pass &Section.

Similaly, once you have finished using a critical section, you should tell Windows you are finished with it. Again, we will do this in the main() function by calling DeleteCriticalSection() with the section pointer as the only parameter. You must not delete the critical section until all your threads that use it are finished with it.

The modified main() function appears here.

int main()
{
    InitializeCriticalSection(&Section);

    _beginthread(Func1,
                 0,
                 NULL);
    _beginthread(Func2,
                 0,
                 NULL);

    Sleep(10000);

    DeleteCriticalSection(&Section);

    cout << "Main exit" << endl;

    return 0;
}
----------

We now need to modify Func1() and Func2() to try to acquire the critical section object before the cout statement and release it afterwards. To synchronise 2 or more threads against a single resource, they must both/all try to acquire the same critical section object. In that way, if a thread tries to acquire the critical section, whilst another has it, the thread attempting acquisition will wait until the thread that currently owns it, releases it. It works just like The Conch.

Here is the modified Func1() function.

void Func1(void *P)
{
    int Count;

    for (Count = 1; Count < 11; Count++)
    {
        EnterCriticalSection(&Section);
        cout << "Func1 loop " << Count << endl;
        LeaveCriticalSection(&Section);
    }
    return;
}

As you can see, I have added a call to EnterCriticalSection() immediately before the cout statement, and a call to LeaveCriticalSection() just after it. Both these functions take the same CRITICAL_SECTION pointer that we have used earlier. Func2() is modified in exactly the same way. The placement of the Enter and Leave calls should be arranged so that the minimum amount of code lies between them. I could have done this...

void Func1(void *P)
{
    int Count;

    EnterCriticalSection(&Section);
    for (Count = 1; Count < 11; Count++)
    {
        cout << "Func1 loop " << Count << endl;
    }
    LeaveCriticalSection(&Section);
    return;
}

... which would have worked, but now, the first thread to acquire the critical section will run to completion whilst the other thread waits - basically, we are back where we started. Always consider carefully the placing of your synchronisation calls.

----------

The full source of the synchronised program appears here.

#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;

void Func1(void *);
void Func2(void *);

CRITICAL_SECTION Section;

int main()
{
    InitializeCriticalSection(&Section);

    _beginthread(Func1,
                 0,
                 NULL);
    _beginthread(Func2,
                 0,
                 NULL);

    Sleep(10000);

    DeleteCriticalSection(&Section);

    cout << "Main exit" << endl;

    return 0;
}

void Func1(void *P)
{
    int Count;

    for (Count = 1; Count < 11; Count++)
    {
        EnterCriticalSection(&Section);
        cout << "Func1 loop " << Count << endl;
        LeaveCriticalSection(&Section);
    }
    return;
}

void Func2(void *P)
{
    int Count;

    for (Count = 10; Count > 0; Count--)
    {
        EnterCriticalSection(&Section);
        cout << "Func2 loop " << Count << endl;
        LeaveCriticalSection(&Section);
    }
    return;
}

The output looks like this.

Threads3

As you can see, the introduction of the CRITICAL_SECTION has solved our output problem. There are still problems with the program however. In part 1 of the tutorial, when converting the program to multithreaded, I added a call to Sleep(). I didn't explain why - we'll explore that area in the next part of the tutorial.

As a side note, if you are using a 32bit WIndows system, (NT, 2000, XP), there is another function you can use called TryEnterCriticalSection(), which takes the section pointer as a parameter as above. If the section could be acquired, it is, and the function returns non-zero, (remember you will need to test the return, and if non-zero, call LeaveCriticalSection() when you are finished with it). If the section was in use by another thread, this function returns zero immediately, i.e. it does not block the calling thread. This function is useful in many situations, deadlock avoidence for example. Sadly, this function is not available on 16 bit systems, (95, 98, ME).


.
. .
.
Previous Index Next Page
Site Home
.
. .
Copyright © adrianxw, 1997 - 2004.