Allegro 5 Tutorial/Threads

From Allegro Wiki
Jump to: navigation, search

In this section we'll learn how to use the Allegro 5 threading interface.

We're going to use the same bitmap created in the previous example, but instead of changing its axis with the mouse, we're going to let two threads modify them. We use the terms:

Parent Thread: All that is running inside our int main() function.

First Thread: The first thread created.

Second Thread: The second thread created.

#include <stdio.h>
#include <allegro5/allegro.h>

class DATA{

   public:

      ALLEGRO_MUTEX *mutex;
      ALLEGRO_COND  *cond;
      float          posiX;
      float          posiY;
      bool           modi_X;
      bool           ready;

   DATA() : mutex(al_create_mutex()),
            cond(al_create_cond()),
            posiX (0),
            posiY (0),
            modi_X(false),
            ready (false) {}

   ~DATA(){

      al_destroy_mutex(mutex);
      al_destroy_cond(cond);

   }

};

const float FPS        = 30;
const int SCREEN_W     = 640;
const int SCREEN_H     = 480;
const int BOUNCER_SIZE = 32;

static void *Func_Thread(ALLEGRO_THREAD *thr, void *arg);

int main(int argc, char **argv){

   ALLEGRO_DISPLAY     *display     = NULL;
   ALLEGRO_EVENT_QUEUE *event_queue = NULL;
   ALLEGRO_TIMER       *timer       = NULL;
   ALLEGRO_BITMAP      *bouncer     = NULL;
   ALLEGRO_THREAD      *thread_1    = NULL;
   ALLEGRO_THREAD      *thread_2    = NULL;

   bool redraw = true;

   if(!al_init()) {
      fprintf(stderr, "failed to initialize allegro!\n");
      return -1;
   }

   if(!al_install_mouse()) {
      fprintf(stderr, "failed to initialize the mouse!\n");
      return -1;
   }

   timer = al_create_timer(1.0 / FPS);
   if(!timer) {
      fprintf(stderr, "failed to create timer!\n");
      return -1;
   }

   display = al_create_display(SCREEN_W, SCREEN_H);
   if(!display) {
      fprintf(stderr, "failed to create display!\n");
      al_destroy_timer(timer);
      return -1;
   }

   bouncer = al_create_bitmap(BOUNCER_SIZE, BOUNCER_SIZE);
   if(!bouncer) {
      fprintf(stderr, "failed to create bouncer bitmap!\n");
      al_destroy_display(display);
      al_destroy_timer(timer);
      return -1;
   }

   al_set_target_bitmap(bouncer);
   al_clear_to_color(al_map_rgb(255, 0, 255));
   al_set_target_bitmap(al_get_backbuffer(display));
   event_queue = al_create_event_queue();

   if(!event_queue) {
      fprintf(stderr, "failed to create event_queue!\n");
      al_destroy_bitmap(bouncer);
      al_destroy_display(display);
      al_destroy_timer(timer);
      return -1;
   }

   al_register_event_source(event_queue, al_get_display_event_source(display));
   al_register_event_source(event_queue, al_get_timer_event_source(timer));
   al_register_event_source(event_queue, al_get_mouse_event_source());
   al_clear_to_color(al_map_rgb(0,0,0));
   al_flip_display();
   al_start_timer(timer);

   DATA data;

   thread_1 = al_create_thread(Func_Thread, &data);
   al_start_thread(thread_1);

   al_lock_mutex(data.mutex);
   while (!data.ready){

      al_wait_cond(data.cond, data.mutex);

   }
   al_unlock_mutex(data.mutex);

   al_lock_mutex(data.mutex);
   data.modi_X = true;
   data.ready  = false;
   al_unlock_mutex(data.mutex);

   thread_2 = al_create_thread(Func_Thread, &data);
   al_start_thread(thread_2);

   al_lock_mutex(data.mutex);
   while (!data.ready){

      al_wait_cond(data.cond, data.mutex);

   }
   al_unlock_mutex(data.mutex);


   while(1)
   {
      ALLEGRO_EVENT ev;
      al_wait_for_event(event_queue, &ev);

      if(ev.type == ALLEGRO_EVENT_TIMER) {
         redraw = true;
      }
      else if(ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
         break;
      }
      else if(ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {
         break;
      }
      if(redraw && al_is_event_queue_empty(event_queue)) {
         redraw = false;

         al_lock_mutex(data.mutex);
         float X = data.posiX;
         float Y = data.posiY;
         al_unlock_mutex(data.mutex);
         
         al_draw_bitmap(bouncer, X, Y, 0);

         al_flip_display();
      }
   }
   al_destroy_thread(thread_1);
   al_destroy_thread(thread_2);

   al_destroy_bitmap(bouncer);
   al_destroy_timer(timer);
   al_destroy_display(display);
   al_destroy_event_queue(event_queue);

   return 0;
}

   static void *Func_Thread(ALLEGRO_THREAD *thr, void *arg){

   DATA *data  = (DATA*) arg;
   float num   = 0.1;

   al_lock_mutex(data->mutex);

   bool modi_X = data->modi_X;
   data->ready = true;
   al_broadcast_cond(data->cond);

   al_unlock_mutex(data->mutex);

   while(!al_get_thread_should_stop(thr)){

      al_lock_mutex(data->mutex);
      if(modi_X)
         data->posiX += num;
      else
         data->posiY += num;
      al_unlock_mutex(data->mutex);

      al_rest(0.01);

   }


   return NULL;
   }


Walk Through

   
class DATA{

   public:

      ALLEGRO_MUTEX *mutex;
      ALLEGRO_COND  *cond;
      float          posiX;
      float          posiY;
      bool           modi_X;
      bool           ready;

   DATA() : mutex(al_create_mutex()),
            cond(al_create_cond()),
            posiX (0),
            posiY (0),
            modi_X(false),
            ready (false) {}

   ~DATA(){

      al_destroy_mutex(mutex);
      al_destroy_cond(cond);

   }

};

First we create a class, which is going to contain the data we want to exchange between threads.

ALLEGRO_MUTEX *mutex

Here we declare a mutex (mutual exclusion). In essence it's a flag that's supposed to tell other threads to leave some data alone. You can't set it directly, though. This should become clear why.

ALLEGRO_COND  *cond;

Here we declare a condition. It is sort of a sign that can asynchrously be picked up by other threads. Again you'll have to use functions to use it.

      float          posiX;
      float          posiY;
      bool           modi_X;
      bool           ready;

posiX, and posiY are the X, and Y coordinates of our bitmap, modi_X and ready are two flags that we're going to use later on.

   DATA() : mutex(al_create_mutex()),
            cond(al_create_cond()),
            posiX (0),
            posiY (0),
            modi_X(false),
            ready (false) {}
 
   ~DATA(){
 
      al_destroy_mutex(mutex);
      al_destroy_cond(cond);
 
   }

Class's constructor, using an initialization list. Followed by the destructor. Note: you cannot make an instance of this object on global scope, because al_create_mutex and al_create_cond would be called before al_init().

   ALLEGRO_THREAD      *thread_1    = NULL;
   ALLEGRO_THREAD      *thread_2    = NULL;

Then inside the int main()function we create a pointer to an ALLEGRO_THREAD, an opaque structure representing a thread. A thread is like a program running separately.

DATA data;

After the Allegro initialization process, since it's using some allegro functions, we create our object data.

thread_1 = al_create_thread(Func_Thread, &data);
al_start_thread(thread_1);

Here we create our thread and immediately the data is sent. Func_Thread is a pointer to a function and &data is the address of our recently created object. And since when a thread is created, it is initially in a suspended state, we need to call al_start_thread(thread_1) to begin its actual execution.

So, you have to write your own function that matches this prototype:

 static void *Func_Thread(ALLEGRO_THREAD *thr, void *arg)

Whatever you do inside this functions is going to be running on a different thread.

Next lines get technical, but can be seen as 1 unit:

   al_lock_mutex(data.mutex);
   while (!data.ready){
 
      al_wait_cond(data.cond, data.mutex);
 
   }
   al_unlock_mutex(data.mutex);

Firstly, we lock the mutex, this is how we tell Allegro that one of our many threads is going to use a shared resource. An analogy would be shouting "it's my turn now!". If this thread is the first one to shout, it gets the mutex. If another thread has locked the mutex first, this thread will have to wait until that particular thread unlocks it. while (!data.ready){ First of all, we need to check out if the flag ready is true, because in case it is, we don't need to wait for any condition, since for some reason the second thread previously created was faster than the parent thread and it's ready. In case ready isn't true al_wait_cond will unlock the previously locked mutex and pauses this thread until the condition is signaled. The function will return when cond is signaled, acquiring the lock on the mutex in the process. Finally since al_wait_cond acquired the mutex again, we need to call al_unlock_mutex(data.mutex) to unlock the mutex.

al_wait_cond can return for other reasons other than the condition becoming true (e.g. the process was signalled). If multiple threads are blocked on the condition variable, the condition may no longer be true by the time the second and later threads are unblocked. Remember not to unlock the mutex prematurely.

Now you should understand the reason a while loop was put around al_wait_cond; and a variable named ready was put into the DATA class.

 
   al_lock_mutex(data.mutex);
   data.modi_X = true;       
   data.ready  = false;      
   al_unlock_mutex(data.mutex);
 
   thread_2 = al_create_thread(Func_Thread, &data);
   al_start_thread(thread_2);
 
   al_lock_mutex(data.mutex);
   while (!data.ready){
 
      al_wait_cond(data.cond, data.mutex);
 
   }
   al_unlock_mutex(data.mutex);

When we come to this point its because our first thread is already done. Now we're going to create and start our second thread, so first we lock the mutex, and change the flag modi_X to true, the first thread received this flag as false so the first thread will be changing the Y axis of the bitmap, and the second thread, since it will receive it as true, will change the X axis. We also set our ready flag to false again since we need to do the same process we did with the first thread.

After that, we proceed to create our second thread, as you can see we're using the same function (Func_Thread), for that reason we changed the flag modi_X to true otherwise we could create another thread function which changes the X value, but we would need to rewrite all the code again just to change a little bit of the code and that is not a good practice.

After starting the second thread we need wait until is done, so we do the same we did with the first one. Now before we move on, let's see what it's doing the Func_Thread function.

 

static void *Func_Thread(ALLEGRO_THREAD *thr, void *arg){
 
   DATA *data  = (DATA*) arg;
   float num   = 0.1;
 
   al_lock_mutex(data->mutex);
 
   bool modi_X = data->modi_X;
   data->ready = true;
   al_broadcast_cond(data->cond);
 
   al_unlock_mutex(data->mutex);
 
   while(!al_get_thread_should_stop(thr)){
 
      al_lock_mutex(data->mutex);
      if(modi_X)
         data->posiX += num;
      else
         data->posiY += num;
      al_unlock_mutex(data->mutex);
 
      al_rest(0.01);
 
   }
 
 
   return NULL;
   }

The data you sent using al_create_thread() is received by this function as a void pointer data type (void *arg). So the first thing we need to do inside here is convert our object back to a DATA datatype, which is our original class.

 DATA *data  = (DATA*) arg;

With some typecasting it's done. Remember that data now it's a pointer to a DATA object so we'll use data->example instead of data.example.

 float num   = 0.1;

This is the value we're going to add to our X or Y axis.

 al_lock_mutex(data->mutex);

Again, we lock the mutex since we're going to use shared resources.

 bool modi_X = data->modi_X;

So, here we're creating a bool variable , and assigning it a value. As you can see, when we created our DATA object, this value is initialized as false, so the bool modi_X variable of this thread, if it's the first thread created, is going to be false. We're doing this because we need to tell the first thread to modified the X value and the second thread to modified the Y value, so the thread which receive modi_X (Modify X) as true, is going to do exactly that, modified the X Axi, and the thread which receive modi_X as false, is going to modified the Y axis.

 data->ready = true;

This is the second flag of our data object, we need to use this, because our parent thread needs to wait until we have assigned the modi_X value. Since we're working with threads we can never assume the order or speed in which two things in parallel happen. So this flag along with the cond struct, allow us to tell our parent thread, when it can move on.

al_broadcast_cond(data->cond);

After assigning the threads modi_X to false, we change the ready flag to true, to tell to the parent thread that it doesn't need to wait, because the thread is ready, but in case it's waiting, it'll be waiting for this cond, so we also broadcast our condition so it can move on, with this line:

 al_unlock_mutex(data->mutex);

Finally we unlock our mutex to tell Allegro that we're not going to be using the data object anymore so other threads can start using it.

Ok, we need to go back to our parent thread, and make a little review:


 0    thread_1 = al_create_thread(Func_Thread, &data);
 1    al_start_thread(thread_1);
 2  
 3    al_lock_mutex(data.mutex);
 4    if (!data.ready)
 5  
 6       al_wait_cond(data.cond, data.mutex);
 7  
 8    al_unlock_mutex(data.mutex);
 9  
10    al_lock_mutex(data.mutex);
11    data.modi_X = true;
12    data.ready  = false;
13    al_unlock_mutex(data.mutex);
14  
15    thread_2 = al_create_thread(Func_Thread, &data);
16    al_start_thread(thread_2);
17  
18    al_lock_mutex(data.mutex);
19    if (!data.ready)
20  
21       al_wait_cond(data.cond, data.mutex);
22  
23    al_unlock_mutex(data.mutex);

Quick Review

  • In line 0 we create the thread.
  • In line 1 we start the thread.
  • in line 3 we lock our mutex because we're going to check if ready it's true
  • In case it is not true that means our second thread for some reason hasn't initialized its modi_X bool variable, so we need to wait.
  • In line 6 we start waiting.
  • Ok our first thread has broadcast the conditional that means he is ready.
  • In line 8 we unlock the mutex because we're not going to use it anymore.
  • In line 10 we lock the mutex again, we need to change our variables because we're about to create another thread, and if we don't change the flags the second thread would be a complete mess.
  • In line 11 we change our modi_X variable to true, that way our second thread is going to modify the X value of our bitmap.
  • In line 12 we change back our ready variable to false again, because now we need to do the same with the second thread, we need to know when it's ready.
  • In line 13 we unlock everything again.

And the next lines do everything again, but for the second thread.

Two Threads Running

At this point we have two threads running!

Now what?

Well those threads are changing the posi_X and posi_Y variables, so if you don't hurry up and draw the bitmap you're not going to see anything.

But before drawing anything let's see how these threads are changing these variables:

   while(!al_get_thread_should_stop(thr)){
 
      al_lock_mutex(data->mutex);
      if(modi_X)
         data->posiX += num;
      else
         data->posiY += num;
      al_unlock_mutex(data->mutex);
 
      al_rest(0.01);
 
   }
 
 
   return NULL;

We're back to our Func_Thread() function.

After initializing some values and telling to our parent thread that we're ready, the thread enter in a loop. A very simple loop that you should understand very well. To keep it simple we're not going to create another timer but just use al_rest() which allow us to rest the thread some seconds, otherwise this loop would run so fast that we wouldn't be able to see our bitmap on the screen.

while(!al_get_thread_should_stop(thr))

The same way this thread talked with the parent thread to tell it was ready using the cond variable, this function al_get_thread_should_stop(thr) allows the parent thread to talk with this thread, but this time we use it to tell our thread that must stop.

If al_get_thread_should_stop(thr) returns true, the thread will stop, how can we make this function to return true?, we'll see it later on. thr it's the other value that Func_Thread receives. It's nothing more than a pointer to the current thread.

So we're done with the thread function. Now let's advance in our parent thread.

 0     while(1)
 1    {
 2       ALLEGRO_EVENT ev;
 3       al_wait_for_event(event_queue, &ev);
 4  
 5       if(ev.type == ALLEGRO_EVENT_TIMER) {
 6          redraw = true;
 7       }
 8       else if(ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
 9          break;
10       }
11       else if(ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {
12          break;
13       }
14       if(redraw && al_is_event_queue_empty(event_queue)) {
15          redraw = false;
16          
17          al_lock_mutex(data.mutex);
18          float X = data.posiX;
19          float Y = data.posiY;
20          al_unlock_mutex(data.mutex);
21          
22          al_draw_bitmap(bouncer, X, Y, 0);
23  
24          al_flip_display();
25       }
26    }
27    al_destroy_thread(thread_1);
28    al_destroy_thread(thread_2);
29  
30    al_destroy_bitmap(bouncer);
31    al_destroy_timer(timer);
32    al_destroy_display(display);
33    al_destroy_event_queue(event_queue);

This shouldn't be anything new. As you can see in line 17, 18, 19 and 20, we are creating two variables and initializing them with the corresponding value, this is to clarify, that you should keep locked the mutex the less possible, al_draw_bitmap() take much more time than just creating and initializing two float variables, for that reason we're doing this.

al_destroy_thread(thread_1);
al_destroy_thread(thread_2);

And finally we're destroying the threads when we exit the program. This implicitly performs al_join_thread which at the same time implicitly calls al_set_thread_should_stop that way al_get_thread_should_stop(thr) will return true and the thread is going to stop.

Note: This is a very simple example to help you get the idea, normally you don't need to initialize first a thread then another one and so on, you can do whatever you want, though ;).