Créer des programmes utilisant des threads et des points de rendez-vous est l'une des choses les moins faciles de l'informatique. Comme les événements sont asynchrones il est très difficile de prévoir le comportement sauf à appliquer une méthodologie stricte et d'avoir aussi un peu d'expérience.

Certaines personnes n'ont pas le temps d'apprendre la théorie mais ont l'instinct de comprendre superficiellement les choses et de mal les appliquer.

Pour illustrer la théorie je vais prendre l'exemple classique de l'incrémentation d'une variable en C :


# include <stdio.h>

int var = 0 ;

void inc (void)
{
  ++ var ;
}

int main (int argC, char * * argV)
{
  int i ;

  for (i = 0 ; i < 100000 : i ++)
     inc () ;

  printf ("Total : %d\n",var) ;
  return (0) ;
}

Ce code anodin va afficher 100000. Maintenant en utilisant deux threads comme ceci (code simplifié) :


# include <stdio.h>

int var = 0 ;

void inc (void)
{
  ++ var ;
}

void thread1 (void * scratch)
{
  int i ;

  for (i = 0 ; i < 50000 : i ++)
     inc () ;
}

void thread2 (void * scratch)
{
  int i ;

  for (i = 0 ; i < 50000 : i ++)
     inc () ;
}

int main (int argC, char * * argV)
{
  CreateThread (thread1, NULL) ;
  CreateThread (thread2, NULL) ;
  Sleep (5) ;

  printf ("Total : %d\n",var) ;
  return (0) ;
}

Nous allons avoir deux threads qui vont procéder à l'incrémentation de la variable donc 2 * 50000 = 100000. Et bien non le résultat sera inférieur à 100000, pour comprendre pourquoi il faut aller voir au niveau du code en assembleur i386 :


inc:
  mov ebx,[var]
  inc ebx
  mov [var],ebx
  ret

Pour incrémenter la variable il faut (en simplifiant) trois opérations élémentaires, donc entre le moment où la variable est chargée dans le registre ebx et le moment où la valeur est réactualisée il se peut très bien que dans l'intervalle le deuxième thread ait fait les mêmes opérations mais qui seront écrasées par le mov [var],ebx du premier thread.

Il faut donc que la variable var ne puisse pas être modifiée durant les phases de chargement dans le registre, incrémentation du registre, recopie du registre dans la variable, pour ce faire il existe un objet appelé mutex ou section critique qui permet de garantir que les opérations ne seront pas interrompues pas un autre thread, ce qui donne :


CRITICAL_SECTION cs ;

void inc (void)
{
  EnterCriticalSection (& cs) ;
  ++ var ;
  LeaveCriticalSection (& cs) ;
}

Donc un objet section critique (cs) commun aux deux threads et le compte sera bien de 100000 mais comme il ne faut pas freiner l'innovation je laisse à votre sagacité le code suivant qui est en production sur des serveurs (disclaimer : je n'ai pas pondu ce code).


...
void inc (void)
{
  CRITICAL_SECTION cs ;

  InitializeCriticalSection (& cs) ;
  EnterCriticalSection (& cs) ;
  ++ var ;
  LeaveCriticalSection (& cs) ;
}
...

Pas mal, non ?