| Games and SDL SDL Installation SDL for Embedded SDL API SDL Events | SDL Graphics SDL Threads Thread Example SDL Animation SDL Sound | Raw Video Player Video Formats Video Compression | Game Trees About The Author |
| 1 | 2 | 3 |
So far, we have discussed how to create SDL threads and use them to do some independent activities, which can be coordinated using SDL_WaitThread(). If the threads are working on activities that are not independent of each other, we need more sophisticated coordination. This is because threads that run simultaneously can interfere with each other when they access the same area of memory. Consider, for instance, we have two threads, namely, Writer and Reader. Writer updates two variables, account_value and total; whatever has been changed to account_value should be reflected in total. Suppose at a certain point, their values are:
Consistency can be accomplished by using a mutex, which actually means mutual exclusion. A mutex is a data structure which provides a "locking" mechanism that allows only one thread at a time to execute a section of code. This is in analogy with the case when you use the rest room in a small fast food restaurant. When you use it, you have to first lock it to prohibit other customers to enter. When you are done, you will unlock it. A mutex has to be created before using and destroyed after all threads finished using it. SDL provides four functions for mutex operations:
The first two are self-explained. The last two, ending with P and V require some explanations. Actually, mutexes are special forms of semaphores, first used by E. W. Dijkstra, a professor in the Department of Mathematics at the Technological University, Eindhoven, Netherlands, to do synchronization in computer programs. A counting semaphore ( aka PV semaphore ) consists of a variable that can be incremented to a very large value, but decremented only to zero. A V operation ( aka "V" - verhogen in Dutch ) increments the semaphore value, while a P operation ( aka "P" - Proberen te verlagen ) tries to decrement it. Many textbooks in computer science name the P and V opeations as Down and Up or wait and signal or lock and unlock respectively. When the semaphore's ability to count is not required, its simplified version, mutex is often used. In general, a mutex has two two states: unlocked or locked. When a thread needs access to a critical section, it calls mutex_lock(). If the mutex is currently unlocked, meaning that the critical section is not used, the call succeeds and the calling thread enters the critical section. On the other hand, if the mutex is already locked, the calling thread is blocked until the thread executing the critical section is finished and calls mutex_unlock(). Mutexes are good only for managing mutual exclusion for some shared resources or piece of code; they are easy and efficient to implement, which makes them especially useful in thread packages that are implemented entirely in user space.
When a thread executes P() ( wait ) to enter a critical section, if the mutex ( semaphore ) value is nonzero, it will be decremented by one and the operation succeeds; if not, then the calling thread must go to sleep until a different thread incremnets it; if more than one thread has executed P() while its value is zero, then the threads will be put in a sleeping queue. When a thread has finished the critical section, it must execute V() ( signal() ) to wake up a thread in the sleeping queue.
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" SDL_mutex *SDL_CreateMutex ( void ); |
|
|---|---|---|
| Description | Creates a new, unlocked mutex. | |
| Returns |
Pointer to the newly created mutex. |
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" void SDL_DestroyMutex ( SDL_mutex *mutex ); |
|
|---|---|---|
| Description | Destroys a previously created mutex. | |
| Returns |
None |
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" int SDL_mutexP ( SDL_mutex *mutex ); |
|
|---|---|---|
| Description | Locks the mutex, which was previously created with SDL_CreateMutex(). If the mutex is already locked by another thread then SDL_mutexP() will not return until the thread that locked it unlocks it (with SDL_mutexV() ). If called repeatedly on a mutex, SDL_mutexV() must be called equal amount of times to return the mutex to unlocked state. | |
| Returns |
0 on success, -1 on error. |
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" int SDL_mutexV ( SDL_mutex *mutex ); |
|
|---|---|---|
| Description | Unlocks the mutex, which was previously created with SDL_CreateMutex(). | |
| Returns |
0 on success, -1 on error |
The following program, sync0.cpp implements the Reader/Writer example we discussed above. The mutex value_mutex is used to ensure the accessing of the shared variables, account_value and total is done atomically. Before accessing the shared variables, SDL_mutexV ( value_mutex ) is called; if another thread is accessing it, the thread is blocked ( put to sleep ). When finished accessing the variables, SDL_mutexP ( value_mutex ) is called to 'unlock' the state and wake up a sleeping thread if there is any.
/*
sync0.cpp
A simple example demonstrating the usage of mutex.
Compile: g++ -o sync0 sync0.cpp -lSDL -lpthread
Execute: ./sync0
*/
#include <SDL/SDL.h>
#include <SDL/SDL_thread.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int account_value = 0; //shared variable
int total = 0; //shared variable
SDL_mutex *value_mutex; //mutex to lock variables
bool quit = false;
//This thread reads account_value and total
int reader ( void *data )
{
char *tname = ( char * )data;
while ( !quit ) {
printf("I am %s: ", tname );
SDL_mutexP ( value_mutex ); //lock to read value
//now you can sefely access account_value and total
printf(" My account value and total are: %d, %d.\n",
account_value, total );
//release the lock
SDL_mutexV ( value_mutex );
//delay for a random amount of time
SDL_Delay ( rand() % 1000 );
}
printf("%s is quiting.\n", tname );
return 0;
}
//This thread writes value
int writer ( void *data )
{
char *tname = ( char * )data;
while ( !quit ) {
int a = rand() % 100; //get a random number
printf("I am %s: ", tname );
SDL_mutexP ( value_mutex ); //lock before upgrading
//now you can sefely access values
account_value += a;
total += a;
printf(" I deposited an amount of %d\n", a );
//release the lock
SDL_mutexV ( value_mutex );
//delay for a random amount of time
SDL_Delay ( rand() % 2000 );
}
printf("%s is quiting.\n", tname );
return 0;
}
int main ()
{
SDL_Thread *id1, *id2; //thread identifiers
char *tnames[2] = { "Reader", "Writer" }; //names of threads
value_mutex = SDL_CreateMutex();
//create the threads
id1 = SDL_CreateThread ( reader, tnames[0] );
id2 = SDL_CreateThread ( writer, tnames[1] );
//experiment with 10 seconds
for ( int i = 0; i < 5; ++i )
SDL_Delay ( 2000 );
quit = true; //signal the threads to return
//wait for the threads to exit
SDL_WaitThread ( id1, NULL );
SDL_WaitThread ( id2, NULL );
SDL_DestroyMutex ( value_mutex ); //release the resources
return 0;
}
|
When you execute the above program sync0, you might notice that the shared variables can be updated more than once before they are read and vice versa. Now suppose we require that the variables be updated only if their values have been read and vice versa. That is, the read and write process must be done alternatively. Can we accomplish this using the mutex operations? Though this can be done using POSIX mutexes, this basically cannot be done using SDL mutexes alone. This is because the P ( lock ) operation of SDL is implemented differently from that of Pthreads. The SDL_mutexP() operation is implemented in a way that it allows re-entrance. That is, even if the mutex value has been decremented to 0, the thread will not be blocked as long as the value was previously decremented to 0 by the same thread. Because of this special property, a thread cannot use an SDL mutex to block itself to achieve strict alternation.
Therefore, we need to use some other methods to accomplish strict alternation. A simple way to do this is to use another variable say, turn, to guarantee the threads take turns to enter the critical section. For example, when turn = 1, only thread 1 is allowed to enter; when turn = 2, thread 2 is allowed, and so on. The following code uses the variable value_consumed to enforce the writer and reader threads to take turns to access the shared variables.
/*
sync1.cpp
A simple example demonstrating the use of a global variable
to enforce strict alternation between two threads.
Compile: g++ -o sync1 sync1.cpp -lSDL -lpthread
Execute: ./sync1
*/
#include <SDL/SDL.h>
#include <SDL/SDL_thread.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int account_value = 0; //shared variable
int total = 0; //shared variable
bool value_consumed = true; //variable to control synchronization
bool quit = false;
//This thread reads account_value and total
int reader ( void *data )
{
char *tname = ( char * )data;
while ( !quit ) {
while ( value_consumed && !quit )
SDL_Delay ( 20 ); //wait for new value
if ( quit ) break; //when you wake up
// the world might have changed
//now you can sefely access account_value and total
printf("I am %s: ", tname );
printf(" My account value and total are: %d, %d.\n",
account_value, total );
//tell writer that value has been read
value_consumed = true;
//delay for a random amount of time
SDL_Delay ( rand() % 1000 );
}
printf("%s is quiting.\n", tname );
return 0;
}
//This thread writes value
int writer ( void *data )
{
char *tname = ( char * )data;
while ( !quit ) {
int a = rand() % 100; //get a random number
//don't write until previous value has been read
while ( !value_consumed && !quit )
SDL_Delay ( 20 );
if ( quit ) break; //when you wake up,
// the world might haved changed
printf("I am %s: ", tname );
account_value += a;
total += a;
printf(" I deposited an amount of %d\n", a );
//tell reader new value is available
value_consumed = false;
//delay for a random amount of time
SDL_Delay ( rand() % 2000 );
}
printf("%s is quiting.\n", tname );
return 0;
}
int main ()
{
SDL_Thread *id1, *id2; //thread identifiers
char *tnames[2] = { "Reader", "Writer" }; //names of threads
//create the threads
id1 = SDL_CreateThread ( reader, tnames[0] );
id2 = SDL_CreateThread ( writer, tnames[1] );
//experiment with 10 seconds
for ( int i = 0; i < 5; ++i )
SDL_Delay ( 2000 );
quit = true; //signal the threads to return
//wait for the threads to exit
SDL_WaitThread ( id1, NULL );
SDL_WaitThread ( id2, NULL );
return 0;
}
|
In program sync1.cpp, synchronization between reader and write is enforced by the variable value_consumed. Each thread has to check value_consumed to see if its her turn to access variables account_value and total. If not, she has to sleep for 20 milli-seconds ( SDL_Delay ( 20 ); ) and check again. This is not very desirable as she might have to keep waking up to check her turn and go back to sleep until her turn has reached. This behavior is often referred to as busy waiting. Wouldn't it be nice, if the thread could sleep all the way until her turn comes and someone else wakes her up? Indeed, this is a preferable way and can be done using SDL semaphores.
We have discussed semaphores briefly in the above section. In SDL, a semaphore is a generalization of a mutex with more functions and flexibilities. Though semaphores are primitive and simple, they can be used to solve many synchronization problems. As you'll see, the above Reader/Writer problem with strict alternation constraint can easily be handled using two semaphores.
SDL provides a few basic functions of semaphore operations:
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" SDL_sem *SDL_CreateSemaphore ( Uint32 initial_value ) |
|
|---|---|---|
| Description | Creates a new semaphore and initializes it with the value initial_value. | |
| Returns |
A pointer to an initialized semaphore or NULL if there was an error.
|
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" void SDL_DestroySemaphore ( SDL_sem *sem ) |
|
|---|---|---|
| Description | Destroys a semaphore that was created by SDL_CreateSemaphore. | |
| Returns |
None
|
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" int SDL_SemWait ( SDL_sem *sem ); |
|
|---|---|---|
| Description |
Suspends the calling thread until either the semaphore pointed to by sem has
a positive value or the call is interrupted by a signal or error. If the call
is successful it will automically decrement the semaphore value.
After SDL_SemWait() is successfully executed, the semaphore can be released and its count atomically incremented by a successful call to SDL_SemPost(). |
|
| Returns |
0 on success, -1 on error
( leaving the semaphore unchanged ). |
| Synopsis |
#include "SDL.h"
#include "SDL_thread.h" int SDL_SemPost ( SDL_sem *sem ); |
|
|---|---|---|
| Description |
Unlocks the semaphore pointed to by sem and atomically
increments the semaphore's value. Threads that were blocking on the
semaphore may be scheduled after this call succeeds.
SDL_SemPost() should be called after a semaphore is locked by a successful call to SDL_SemWait(), SDL_SemTryWait() or SDL_SemWaitTimeout(). |
|
| Returns |
0 on success, -1 on error
( leaving the semaphore unchanged ). |
The following program sync2.cpp demonstrates the use of SDL semaphores to coordinate two threads to access a common value alternatively. Two semaphore variables, read_sem, and write_sem are used to synchronize the threads. At the instance when they are created, read_sem is initialized to a value of 0 and write_sem to 1.
The reader thread waits on read_sem to enter the critical section ( to access value ). If the value of read_sem is not 0, it decrements it and proceeds to execute the critical section, otherwise it goes to sleep. As its initial value is 0, reader has to wait until writer has increased read_sem value to 1. Upon exiting the critical section ( i.e. finished reading the value ), reader executes 'SDL_SemPost ( write_sem )' to increment write_sem and wakes up writer if writer is sleeping.
On the other hand, the writer thread waits on write_sem. As its initial value is 1, at the beginning, it decrements it to zero and proceeds to execute the critical section ( writing value ). Upon exiting the critical section, it executes 'SDL_SemPost ( read_sem )' to increment read_sem and wakes up reader. In the next round, it will wait on write_sem which is 0 unless the reader thread has read value and executed 'SDL_SemPost ( write_sem )' and so on. So the writer thread and the reader thread will access value alternatively.
/*
sync2.cpp
A simple example demonstrating the synchronization of sdl threads using semaphores.
Compile: g++ -o sync2 sync2.cpp -lSDL -lpthread
Execute: ./sync2
*/
#include <SDL/SDL.h>
#include <SDL/SDL_thread.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int value; //shared variable
SDL_sem *read_sem; //semaphore to lock value when read
SDL_sem *write_sem; //semaphore to lock value when write
bool quit = false; //signal for all threads to quit
//This thread reads value
int reader ( void *data )
{
char *tname = ( char * )data;
while ( !quit ) {
if ( SDL_SemWait ( read_sem ) == -1 ){ //get a lock to read value
printf("Failed to lock\n");
exit ( -1 );
}
if ( quit ) break; //don't read further if quit
//now you can sefely access value
printf("I am %s. ", tname );
printf(" I got a value of %d\n", value );
//release the lock, wake up writer to read
SDL_SemPost ( write_sem );
//Delay for a random amount of time
SDL_Delay ( rand() % 1000 );
}
printf("%s quits\n", tname);
//before quiting, wake up anyone who may be sleeping
SDL_SemPost ( write_sem );
return 0;
}
//This thread writes value
int writer ( void *data )
{
char *tname = ( char * )data;
while ( !quit ) {
if ( SDL_SemWait ( write_sem ) == -1){ //get a lock on variable value
printf("Failed to lock write_sem!\n");
exit ( -1 );
}
if ( quit ) break; //don't write further if quit
int a = rand() % 100;
printf("I am %s. ", tname );
//now you can sefely access value
value = a;
printf(" I wrote a value of %d\n", a );
SDL_SemPost ( read_sem ); //wake up reader to read
//delay for a random amount of time
SDL_Delay ( rand() % 2500 );
}
printf("%s quits\n", tname);
//before quiting, wake up reader if she's sleeping
SDL_SemPost ( read_sem );
return 0;
}
int main ()
{
SDL_Thread *id1, *id2; //thread identifiers
char *tnames[2] = { "Thread 1", "Thread 2" }; //names of threads
read_sem = SDL_CreateSemaphore ( 0 ); //write before read
write_sem = SDL_CreateSemaphore ( 1 );
//create the threads
id1 = SDL_CreateThread ( reader, tnames[0] );
id2 = SDL_CreateThread ( writer, tnames[1] );
//experiment with 10 seconds
for ( int i = 0; i < 5; ++i )
SDL_Delay ( 2000 );
quit = true; //signal the threads to return
//destroy the semaphores
SDL_DestroySemaphore ( write_sem );
SDL_DestroySemaphore ( read_sem );
//wait for the threads to exit
SDL_WaitThread ( id1, NULL );
SDL_WaitThread ( id2, NULL );
return 0;
}
|
Semaphores are primitive methods to synchronize threads. A number of things could go wrong when you use semaphores to coordinate the interactions of threads. Even if you have carefully locked all shared variable, other unexpected problems may still arise. One common problem you may encounter is deadlock, which will be discussed in next section. In general, all synchronization problems have a solution though not perfect.