TUTO Multi-Threads

Informations pour bien débuter en PureBasic
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

TUTO Multi-Threads

Message par microdevweb »

Bonjour à tous,

Voici un petit tuto sur le multi-threads.

Pourquoi faire du multi-threads, si vous développez une application qui doit faire des traitements de données (images / vidéo / recherche web / etc...), et que vous réalisez des boucles standards votre programme va se figer car une boucle est bloquante.

Une solution est la création de threads, qui sont des processus autonomes et peuvent tourner en parallèle avec le code de votre logiciel un peux comme si vous lanciez d'autres exécutables mais en conservant l'accès a vos variables globales ou aux données passées en paramètre par pointeur .

Par contre il existe quelques soucis qu'il faut prendre en compte.

C'est l'os qui va se charger de lancer les threads et pas forcément dans l'ordre de leurs création. Donc il faut pouvoir synchroniser les threads, mais sans prendre en compte l'ordre de leurs création.

Pour cela il existent deux outils très utiles
  • Les sémaphores
  • Les mutex
Le sémaphore n'est en vérité rien d'autre q'un entier, qui peut être incrémenté ou décrémenté.

2 méthodes sont possibles
  • wait , qui décrémente le sémaphore et endort le thread dans cas ou la valeur du sémaphore est inférieur à 0
  • signal, qui incrémente le sémaphore et réveille un thread endormi.
Il faut évidement vous rendre compte, que même si le principe est simple bien utiliser les sémaphores est beaucoup moins évident.

Allen B. Downey un professeur d'université Américain à écrit un petit livre à ce sujet "The Little Book of Semaphores", proposant des "paterns" pour des cas d'école bien connu.

The Little Book of Semaphores

Dans ce tutoriel évolutif, je vous propose quelques exemples de ce livre écrit en PureBasic évidement.

Table des exemples :
Dernière modification par microdevweb le dim. 19/janv./2020 11:22, modifié 10 fois.
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Le rendez-vous

Scénario :

Deux amis décident d'aller voir un film dans leurs ville respective, la distance de chez eux au cinéma et l'heure de la séance ne sont pas forcément les mêmes et nous ne les connaissons pas. Par contre ils se disent qu'il vont se donner rendez-vous après avoir regardé le film pour en parler.


Pattern :

Thread A
1 statement a1
2 aArrived.signal()
3 bArrived.wait
4 statement a2

Thread B
1 statement b1
2 bArrived.signal()
3 aArrived.wait()
4 statement b2

Code pure basic

Code : Tout sélectionner

;-----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; STAGE         : rendezvous
; AUTHOR      : MicrodevWeb
; DATE           : 2020-01-17
; SCENARIO    : 2 friends decide to watch a film in 2 different towns. 
;             Hours To start are Not same And they want discuss about 
;             movie after watch it.
; TIME STAGE  : 
; FRIEND_1
;  watch      wait on     discuss
;             RDV SPOT    about
; ----------|-----------|--------
; FRIEND_2
;  watch    wait on       discuss
;            RDV SPOT     about
; -------|--------------|--------
;-----------------------------------------------------------------------------
EnableExplicit
Global.i aArrived,bArrived ; semaphores
Global.i THR_A,THR_B       ; thread id
;-----------------------------------------------------------------------------
; THREAD FUNCTIONS
;-----------------------------------------------------------------------------
Procedure THR_Friend_a(*p)
  Debug "Friend a watch a film"
  ; random time for watch the movie
  Delay(Random(5000,1000))
  ; tell a is arrived
  SignalSemaphore(aArrived)
  ; wait friend for b
  WaitSemaphore(bArrived)
  Debug "a discuss about movie with b"
EndProcedure
Procedure THR_Friend_b(*p)
  Debug "Friend b watch a film"
  ; random time for watch the movie
  Delay(Random(5000,1000))
  ; tell b is arrived
  SignalSemaphore(bArrived)
  ; wait friend for a
  WaitSemaphore(aArrived)
  Debug "b discuss about movie with a"
  
EndProcedure
;-----------------------------------------------------------------------------
; MAIN PROGRAM
;-----------------------------------------------------------------------------
; create semaphores
aArrived = CreateSemaphore(0)
bArrived = CreateSemaphore(0)
; run threads
THR_A = CreateThread(@THR_Friend_a(),0)
THR_B = CreateThread(@THR_Friend_b(),0)
Define i
For i = 0 To 10
  Debug "The main software can count "+Str(i)
  Delay(1000)
Next
; wait for threads
WaitThread(THR_A)
WaitThread(THR_B)
Debug "END PROGRAM"
End
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Le mutex

description :
Le mutex (mutual exclusion), est en vérité un sémaphore initialisé à 1.

Quand l'utilisé :
Il doit être utilisé principalement quand plusieurs threads essaient de modifier une même variable.

Méthodes
  • Lock (bloque l’accès à la variable)
  • unLock (débloque l’accès à la variable)
Exemple de code

Code : Tout sélectionner


;-----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; STAGE       : mutex
; AUTHOR      : MicrodevWeb
; DATE        : 2020-01-17
;-----------------------------------------------------------------------------

Global THR_A,THR_B ; threads id
Global mutex       ; mutex id
Global counter = 0 ;
                   ; create mutex 
mutex = CreateMutex();

#MAX = 10; Macimum to count

;------------------------------------------------------------------------------
; thread functions
;------------------------------------------------------------------------------
Procedure f_thr_a(*p)
  Repeat
    Debug "Thread A is running"
    LockMutex(mutex)    ; we lock variable access
    counter +1          ;
    Debug "A increment counter "+Str(counter)
    UnlockMutex(mutex)  ; we unlock variable access
    Delay(Random(1000,800)) ; wait some time
    If counter >= #MAX
      Debug "Thread A STOP"
      ProcedureReturn
    EndIf
  ForEver
EndProcedure
Procedure f_thr_b(*p)
  Repeat
    Debug "Thread B is running"
    LockMutex(mutex)    ; we lock variable access
    counter +1          ;
    Debug "B increment counter "+Str(counter)
    UnlockMutex(mutex)  ; we unlock variable access
    Delay(Random(1000,800)) ; wait some time
    If counter >= #MAX
      Debug "Thread B STOP"
      ProcedureReturn 
    EndIf
  ForEver
EndProcedure
;------------------------------------------------------------------------------
; main
;------------------------------------------------------------------------------
; create threads
THR_A = CreateThread(@f_thr_a(),0)
THR_B = CreateThread(@f_thr_b(),0)
; waiting for threads
WaitThread(THR_A)
WaitThread(THR_B)

Debug "END OF PROGRAM"

End

Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Marc56
Messages : 2146
Inscription : sam. 08/févr./2014 15:19

Re: TUTO Multi-Threads

Message par Marc56 »

[removed]

Ce n'est ce que j'avais compris en lisant trop vite la doc PB:
https://www.purebasic.com/french/docume ... index.html
Dernière modification par Marc56 le sam. 18/janv./2020 8:17, modifié 2 fois.
Avatar de l’utilisateur
Naheulf
Messages : 191
Inscription : dim. 10/mars/2013 22:22
Localisation : France

Re: TUTO Multi-Threads

Message par Naheulf »

Marc56 tu confond thread et processus. (Il y a aussi les coroutines mais c'est encore autre chose)

Les processus sont des programmes qui ont leur propre zone mémoire réservée. Les différents processus peuvent s'exécuter en parallèle. Cela est géré par le système d'exploitation. Certains programmes, comme Firefox depuis la version quantum, vont effectivement lancer plusieurs processus.
Dans le cas de Firefox, ce sont tous les "Firefox.exe" que l'on peut voir dans le gestionnaire de tâche ou dans process explorer.
En PureBasic cela se fait avec les procédures de la bibliothèque "Process".

Les threads sont des procédures qui s'exécutent indépendamment les unes des autres mais qui font partie du même processus. Elles ont donc une partie de la mémoire du programme en commun. Les différents threads peuvent s'exécuter en parallèle. Cela est géré par le système d'exploitation.
Si on reprend l'exemple de Firefox vu par process explorer, on peut voir la liste des threads avec un clic droit > Properties... > Onglet "Threads". Dans mon cas, rien que pour le processus firefox qui à lancé les 8 autres j'ai plus de 60 threads.
En pureBasic cela se fait avec les procédres de la bibliothèque "Thread"


À titre de comparaison on peut comparer les processus et les threads à des salles et aux personnes qui sont dedans :
-Chaque salle de réunion (processus) est indépendant. on peut donc faire plusieurs réunions/cours/... en parallèle sans se soucier des autres réunions. On peut éventuellement envoyer des messages entre les salles. En informatique cela correspond aux entrées et sorties standard.
-Dans une salle on peut avoir plusieurs personnes (threads). Chaque personne peut avoir ses propres notes et fonctionner indépendamment mais si on veut pouvoir travailler ensemble sur un média/mémoire commun(e) (tableau, discussion, ...) il faut pouvoir se synchroniser sinon c'est le bazar.
Mesa
Messages : 1093
Inscription : mer. 14/sept./2011 16:59

Re: TUTO Multi-Threads

Message par Mesa »

Très bon tuto, merci. :D

M.
doudouvs
Messages : 244
Inscription : jeu. 07/mars/2013 19:02
Localisation : France Alsace / Espagne Girona

Re: TUTO Multi-Threads

Message par doudouvs »

@microdevweb

Merci pour le Tuto même si j'utilise déjà les Treads, par contre Mutex je ne connaissais pas
GCC 7.4.0 / PureBasic 5.71 / Ubuntu 18.04.3 LTS
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Les coureurs (partie 1)
Scénario : Plusieurs coureurs courent sur plusieurs étapes à des vitesses variables, chaque coureur arrivé à son étape doit attendre que tous les coureurs soient arrivé avant d'exécuté une tache critique. Il repart ensuite ensuite ver l'étape suivante.

Voici une première solution avec un système de simple barrière.

Pseudo code

Code : Tout sélectionner

rendezvous
2
3 mutex.wait()
4 count = count + 1
5 mutex.signal()
6
7 if count == n: barrier.signal()
8
9 barrier.wait()
10 barrier.signal()
11
12 critical point
Il y a cependant un problème, après le passage de la première étape et l'arrivée de tous les coureurs. La barrière reste ouverte et cela ne fonctionne plus si vous ajouté une étape.

Pour tester cela modifier dans le code suivant la constante

Code : Tout sélectionner

#N_STAGE à 2
code purebasic

Code : Tout sélectionner

;-----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; STAGE       : runners part 1
; AUTHOR      : MicrodevWeb
; DATE        : 2020-01-18
; SCENARIO    : any runners run go some stages, but when a runner 
;               will on his stage he must be wait of all runners arrived.
; 
;-----------------------------------------------------------------------------
#N_RUNNERS = 10
#N_STAGE = 1

Structure _runner
  number.i        ; the number of runner
  thr.i           ; the id of the thread
  current_stage.i ; the current stage of runner
EndStructure

Global._runner Dim t_runners(#N_RUNNERS) 
Global.i barrier,mutex ; smaphore id
Global n_runner        ; number of runners used by thread

;-----------------------------------------------------------------------------
; THREADS FUNCTIONS
;-----------------------------------------------------------------------------
Procedure runner_thr(*p._runner)
  With *p
    Debug "runner "+Str(\number)+" is running"
    Repeat
      ; go to next stage
      \current_stage +1
      Delay(Random(2000,500)) ; the runner need some time to arrived on his stage
      Debug "runner "+Str(\number)+" is arrived on stage "+Str(\current_stage)+" and wait"
      LockMutex(mutex) ; mutual exclusion
      n_runner +1
      UnlockMutex(mutex)
      ; if all runner are arrived
      If n_runner = #N_RUNNERS
        SignalSemaphore(barrier)
      EndIf
      WaitSemaphore(barrier)
      SignalSemaphore(barrier)
      Debug "runner "+Str(\number)+" make his critical work"
    Until \current_stage >= #N_STAGE
  EndWith
EndProcedure
;-----------------------------------------------------------------------------
; MAIN 
;-----------------------------------------------------------------------------
; initiation
; -> mutex and semaphore
mutex = CreateMutex()
barrier = CreateSemaphore(0)

; create runners and threads
Define i
For i = 1 To #N_RUNNERS 
  With t_runners(i-1)
    \number = i
    \thr = CreateThread(@runner_thr(),@t_runners(i-1))
  EndWith
Next
; wait for all threads
For i = 1 To #N_RUNNERS 
  With t_runners(i-1)
    WaitThread(\thr)
  EndWith
Next  
Debug "END OF PROGRAM"

End
Dernière modification par microdevweb le sam. 18/janv./2020 11:57, modifié 1 fois.
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Les coureurs partie 2

Pour résoudre de le problème de pouvoir réutiliser la barrière, nous allons concevoir une seconde barrière qui sera ouverte au lancement.

Pseudo code

Code : Tout sélectionner

1 turnstile = Semaphore(0)
2 turnstile2 = Semaphore(1)

1 # rendezvous
2
3 mutex.wait()
4 count += 1
5 if count == n:
6 turnstile2.wait() # lock the second
7 turnstile.signal() # unlock the first
8 mutex.signal()
9
10 turnstile.wait() # first turnstile
11 turnstile.signal()
12
13 # critical point
14
15 mutex.wait()
16 count -= 1
17 if count == 0:
18 turnstile.wait() # lock the first
19 turnstile2.signal() # unlock the second
20 mutex.signal()
21
22 turnstile2.wait() # second turnstile
23 turnstile2.signal()
Code purebasic

Remarque : Comme vous aller le constater dans le code qui suit, cela devient nettement plus compliqué. A partir de ce moment cela voudrait la peine de faire une code réutilisable. C'est ce que nous verront dans la partie 3

Code : Tout sélectionner

;-----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; STAGE       : runners part 1
; AUTHOR      : MicrodevWeb
; DATE        : 2020-01-18
; SCENARIO    : any runners run go some stages, but when a runner 
;               will on his stage he must be wait of all runners arrived.
; 
;-----------------------------------------------------------------------------
#N_RUNNERS = 10
#N_STAGE = 5

Structure _runner
  number.i        ; the number of runner
  thr.i           ; the id of the thread
  current_stage.i ; the current stage of runner
EndStructure

Global._runner Dim t_runners(#N_RUNNERS) 
Global.i barrier_1,barrier_2,mutex ; smaphore id
Global n_runner                    ; number of runners used by thread

;-----------------------------------------------------------------------------
; THREADS FUNCTIONS
;-----------------------------------------------------------------------------
Procedure runner_thr(*p._runner)
  With *p
    Debug "runner "+Str(\number)+" is running"
    Repeat
      ; go to next stage
      \current_stage +1
      Delay(Random(2000,500)) ; the runner need some time to arrived on his stage
      Debug "runner "+Str(\number)+" is arrived on stage "+Str(\current_stage)+" and wait"
      LockMutex(mutex) ; mutual exclusion
      n_runner +1     
      ; if all runner are arrived
      If n_runner = #N_RUNNERS ; all runners arrived along barrier 1
        WaitSemaphore(barrier_2)   ; close barrier 2
        SignalSemaphore(barrier_1) ; open barrier 1
      EndIf
      UnlockMutex(mutex) ; end of mutual exclusion
      
      WaitSemaphore(barrier_1)    ; close barrier 1
      SignalSemaphore(barrier_1)  ; open barrier 1
      
      Debug "runner "+Str(\number)+" make his critical work"
      
      ; barrier 2 management
      LockMutex(mutex) ; mutual exclusion
      n_runner -1 
      If n_runner = 0   ; all runners arrived alonf barrier 2
        WaitSemaphore(barrier_1)   ; close barrier 1
        SignalSemaphore(barrier_2) ; open barrier 2
      EndIf
      UnlockMutex(mutex) ; end of mutual exclusio
      
      WaitSemaphore(barrier_2)   ; close barrier 2
      SignalSemaphore(barrier_2) ; open barrier 2
    Until \current_stage >= #N_STAGE
  EndWith
EndProcedure
;-----------------------------------------------------------------------------
; MAIN 
;-----------------------------------------------------------------------------
; initiation
; -> mutex and semaphore
mutex = CreateMutex()
barrier_1 = CreateSemaphore(0)
barrier_2 = CreateSemaphore(1)

; create runners and threads
Define i
For i = 1 To #N_RUNNERS 
  With t_runners(i-1)
    \number = i
    \thr = CreateThread(@runner_thr(),@t_runners(i-1))
  EndWith
Next
; wait for all threads
For i = 1 To #N_RUNNERS 
  With t_runners(i-1)
    WaitThread(\thr)
  EndWith
Next  
Debug "END OF PROGRAM"
End
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
djes
Messages : 4252
Inscription : ven. 11/févr./2005 17:34
Localisation : Arras, France

Re: TUTO Multi-Threads

Message par djes »

Très bonne initiative sur un sujet qui peut s'avérer très délicat à maîtriser. C'est vraiment utile et l'adaptation à PB n'a pas dû être si évidente que ça. Moi je m'étais bien gratté la tête avec les threads et la gestion des évènements...

Une petite remarque sur les textes qui présentent les codes, où l'on sent que l'enthousiasme l'emporte sur la grammaire :mrgreen: C'est important pour moi car comme c'est un sujet complexe, il faut être bien clair.

Merci en tous cas :)
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Les coureurs partie 3

Comme je vous l'avais dis dans la partie 2 , cela vaut la peine de faire une code générique pour gèrer une double barrières.

Comme je l'aurais fait en c, j'ai opter pour une approche objet .

code du module D_BARRIERS

Code : Tout sélectionner

;-----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; MODULE      : double barriers
; AUTHOR      : MicrodevWeb
; DATE        : 2020-01-18
;-----------------------------------------------------------------------------
DeclareModule D_BARRIERS
  Interface OBJECT
    ; --------------------------------------------------------------------------
    ; PUBLIC METHOD : wait
    ; PROCESS       : wait for all threads
    ; ARGUMENTS     : VOID
    ; RETURN        : VOID
    ; --------------------------------------------------------------------------
    wait()
    ; --------------------------------------------------------------------------
    ; PUBLIC METHOD : free
    ; PROCESS       : free objet
    ; ARGUMENTS     : VOID
    ; RETURN        : VOID
    ; --------------------------------------------------------------------------
    free()
  EndInterface
  ; --------------------------------------------------------------------------
  ; CONSTRUCTOR   : new
  ; ARGUMENTS     : number_of_threads.i -> number of threads 
  ; RETURN        : new instance of barriers
  ; --------------------------------------------------------------------------
  Declare new(number_of_threads.i)
EndDeclareModule
Module D_BARRIERS
  EnableExplicit
  Structure _BARRIERS
    *methods
    barrier_1.i  ; semaphore
    barrier_2.i 
    mutex.i      ; mutex
    n.i          ; n threads
    count.i      ; counter
  EndStructure
  ; --------------------------------------------------------------------------
  ; CONSTRUCTOR   : new
  ; ARGUMENTS     : number_of_threads.i -> number of threads 
  ; RETURN        : new instance of barriers
  ; --------------------------------------------------------------------------
  Procedure new(number_of_threads.i)
    Protected *this._BARRIERS = AllocateStructure(_BARRIERS)
    With *this
      \n = number_of_threads
      \count = 0
      \barrier_1 = CreateSemaphore(0)
      \barrier_2 = CreateSemaphore(1)
      \mutex = CreateMutex()
      \methods = ?S_MTH
      ProcedureReturn *this
    EndWith
  EndProcedure
  ; --------------------------------------------------------------------------
  ; PUBLIC METHOD : wait
  ; PROCESS       : wait for all threads
  ; ARGUMENTS     : VOID
  ; RETURN        : VOID
  ; --------------------------------------------------------------------------
  Procedure wait(*this._BARRIERS)
    With *this
      LockMutex(\mutex) ; mutual exclusion
      \count +1     
      ; if all runner are arrived
      If \count = \n ; all threads arrived along barrier 1
        WaitSemaphore(\barrier_2)   ; close barrier 2
        SignalSemaphore(\barrier_1) ; open barrier 1
      EndIf
      UnlockMutex(\mutex) ; end of mutual exclusion
      
      WaitSemaphore(\barrier_1)    ; close barrier 1
      SignalSemaphore(\barrier_1)  ; open barrier 1
      
      ; barrier 2 management
      LockMutex(\mutex) ; mutual exclusion
      \count -1
      If \count = 0   ; all threads arrived alonf barrier 2
        WaitSemaphore(\barrier_1)   ; close barrier 1
        SignalSemaphore(\barrier_2) ; open barrier 2
      EndIf
      UnlockMutex(\mutex) ; end of mutual exclusio
      
      WaitSemaphore(\barrier_2)   ; close barrier 1
      SignalSemaphore(\barrier_2) ; open barrier 2
    EndWith
  EndProcedure
  ; --------------------------------------------------------------------------
  ; PUBLIC METHOD : free
  ; PROCESS       : free objet
  ; ARGUMENTS     : VOID
  ; RETURN        : VOID
  ; --------------------------------------------------------------------------
  Procedure free(*this._BARRIERS)
    With *this
      FreeSemaphore(\barrier_1)
      FreeSemaphore(\barrier_2)
      FreeMutex(\mutex)
      FreeStructure(*this)
    EndWith
  EndProcedure
  
  DataSection
    S_MTH:
    Data.i @wait()
    Data.i @free()
    E_MTH:
  EndDataSection
EndModule
Cela simplifie évidement grandement le code des coureurs 8)

Code des courreurs

Code : Tout sélectionner

;-----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; STAGE       : runners part 2
; AUTHOR      : MicrodevWeb
; DATE        : 2020-01-18
; SCENARIO    : any runners run go some stages, but when a runner 
;               will on his stage he must be wait of all runners arrived.
; 
;-----------------------------------------------------------------------------
#N_RUNNERS = 10
#N_STAGE = 5

Structure _runner
  number.i        ; the number of runner
  thr.i           ; the id of the thread
  current_stage.i ; the current stage of runner
EndStructure

Global._runner Dim t_runners(#N_RUNNERS) 
Global.D_BARRIERS::OBJECT myBarriers = D_BARRIERS::new(#N_RUNNERS)
;-----------------------------------------------------------------------------
; THREADS FUNCTIONS
;-----------------------------------------------------------------------------
Procedure runner_thr(*p._runner)
  With *p
    Debug "runner "+Str(\number)+" is running"
    Repeat
      ; go to next stage
      \current_stage +1
      Delay(Random(2000,500)) ; the runner need some time to arrived on his stage
      Debug "runner "+Str(\number)+" is arrived on stage "+Str(\current_stage)+" and wait"
      myBarriers\wait()
      Debug "runner "+Str(\number)+" make his critical work"
    Until \current_stage >= #N_STAGE
  EndWith
EndProcedure
;-----------------------------------------------------------------------------
; MAIN 
;-----------------------------------------------------------------------------
; create runners and threads
Define i
For i = 1 To #N_RUNNERS 
  With t_runners(i-1)
    \number = i
    \thr = CreateThread(@runner_thr(),@t_runners(i-1))
  EndWith
Next
; wait for all threads
For i = 1 To #N_RUNNERS 
  With t_runners(i-1)
    WaitThread(\thr)
  EndWith
Next  
; free barriers object
myBarriers\free()
Debug "END OF PROGRAM"
End
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
venom
Messages : 3071
Inscription : jeu. 29/juil./2004 16:33
Localisation : Klyntar
Contact :

Re: TUTO Multi-Threads

Message par venom »

Merci microdevweb pour ce travail.
Il est vrai que les threads ne sont pas simple a utilisé. :?






@++
Windows 10 x64, PureBasic 5.73 x86 & x64
GPU : radeon HD6370M, CPU : p6200 2.13Ghz
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Producteur consommateur (module)

On part du principe q'un consommateur ne peut consommer le produit que si ce dernier a été produit :roll: Logique non?

Pour ce faire, on créer une liste de type FIFO (first IN, first OUT)(premier entré, premier sorti) circulaire avec une taille de buffer définie.

Voici un code objet pour gérer ce type de liste.
la méthode push :
Elle n'ajoute un produit que si il y a de la place disponible(cela dépend de la taille) du buffer,

Exemple : avec un buffer de 2 il existe 2 places, après cela il faudra attendre que le produit soit consommer et une place ainsi libérée.

la méthode pop

Cette méthode ne pourra chargé un produit que si il a été produit, elle libère également une place pour la méthode push.

Remarque : Le module ci-dessous traite des entiers, vous pouvez le modifier pour ce faire

Changer dans la structure le type de variable de

Code : Tout sélectionner

Array  buffer.i(0) ; you can change this buffer with any variable type
Ainsi le type reçu par push et le type retourné par pop si nécessaire

Code du module fifo

Code : Tout sélectionner

; -----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; MODULE      : fifo
; PROCESS     : manage a fifo list
; -----------------------------------------------------------------------------
DeclareModule FIFO
  Interface object
    ; ---------------------------------------------------------------------------
    ; PUBLIC METHOD : push
    ; PROCESS       : push a data into the fifo list
    ; ARGUMENTS     : *item -> adresse to item
    ; RETURN        : VOID
    ; ---------------------------------------------------------------------------
    push(item)
    ; ---------------------------------------------------------------------------
    ; PUBLIC METHOD : pop
    ; PROCESS       : pop a data from the fifo list
    ; ARGUMENTS     : VOID
    ; RETURN        : pointer of data adresse
    ; ---------------------------------------------------------------------------
    pop()
    ; ---------------------------------------------------------------------------
    ; PUBLIC METHOD : free
    ; PROCESS       : free fifo list
    ; ARGUMENTS     : VOID
    ; RETURN        : VOID
    ; ---------------------------------------------------------------------------
    free()
  EndInterface
  ; ---------------------------------------------------------------------------
  ; CONSTRUCTOR : new
  ; ARGUMENTS   : buffer_length.i -> length of buffer minimum 1
  ; RETURN      : instance of fifo
  ; ---------------------------------------------------------------------------
  Declare new(buffer_length.i)
EndDeclareModule
Module FIFO
  EnableExplicit
  Structure _FIFO
    *methods
    Array  buffer.i(0) ; you can change this buffer with any variable type
    buffer_length.i
    input.i
    output.i
    items.i
    spaces.i
    mutex.i
  EndStructure
  ; ---------------------------------------------------------------------------
  ; CONSTRUCTOR : new
  ; ARGUMENTS   : buffer_length.i -> length of buffer minimum 1
  ; RETURN      : instance of fifo
  ; ---------------------------------------------------------------------------
  Procedure new(buffer_length.i)
    Protected *this._FIFO = AllocateStructure(_FIFO)
    With *this
      \methods = ?S_MTH
      \input = 0
      \output = 0
      \buffer_length = buffer_length
      ; initiation of semaphores and mutex
      \items = CreateSemaphore(0)
      \spaces = CreateSemaphore(buffer_length)
      \mutex = CreateMutex()
      ReDim \buffer(buffer_length)
      ProcedureReturn *this
    EndWith
  EndProcedure
  ; ---------------------------------------------------------------------------
  ; PUBLIC METHOD : push
  ; PROCESS       : push a data into the fifo list
  ; ARGUMENTS     : *item -> adresse to item
  ; RETURN        : VOID
  ; ---------------------------------------------------------------------------
  Procedure push(*this._FIFO,item)
    With *this
      WaitSemaphore(\spaces)      ; wait for space into the buffer
      LockMutex(\mutex)           ; mutual exclusion
      \buffer(\input) = item     ; push into the list
      \input +1                   ; next list position
      ; manage circular list
      If \input >= \buffer_length ; uper to buufer length
        \input = 0 
      EndIf
      UnlockMutex(\mutex)         ; end of mutual exclusion
      SignalSemaphore(\items)     ; tel data available
    EndWith
  EndProcedure
  ; ---------------------------------------------------------------------------
  ; PUBLIC METHOD : pop
  ; PROCESS       : pop a data from the fifo list
  ; ARGUMENTS     : VOID
  ; RETURN        : pointer of data adresse
  ; ---------------------------------------------------------------------------
  Procedure pop(*this._FIFO)
    With *this
      Protected returned_value = 0
      WaitSemaphore(\items)               ; wait for data available
      LockMutex(\mutex)                   ; mutual exclusion
      returned_value = \buffer(\output)  ; load from the List
      \output +1                          ; next list position
      ; manage circular list
      If \output >= \buffer_length        ; uper to bufer length
        \output = 0 
      EndIf
      UnlockMutex(\mutex)                 ; End of mutual exclusion
      SignalSemaphore(\spaces)            ; tel space available
      ProcedureReturn  returned_value
    EndWith
  EndProcedure
  ; ---------------------------------------------------------------------------
  ; PUBLIC METHOD : free
  ; PROCESS       : free fifo list
  ; ARGUMENTS     : VOID
  ; RETURN        : VOID
  ; ---------------------------------------------------------------------------
  Procedure free(*this._FIFO)
    With *this
      FreeSemaphore(\spaces)
      FreeSemaphore(\items)
      FreeMutex(\mutex)
      FreeStructure(*this)
    EndWith
  EndProcedure
  DataSection
    S_MTH:
    Data.i @push()
    Data.i @pop()
    Data.i @free()
    E_MTH:
  EndDataSection
EndModule
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
microdevweb
Messages : 1798
Inscription : mer. 29/juin/2011 14:11
Localisation : Belgique

Re: TUTO Multi-Threads

Message par microdevweb »

Producteur consommateur exemple

:roll: Bon c'est juste un exemple ,il ne sert à rien, si ce n'est de comprendre le principe.

L'exemple ci-dessous, lit une chaîne de caractère composée de chiffres séparé par "-" et une chaîne END signale la fin.

Un premier thread:
lit la chaîne et converti le string en entier, il stocke le chiffre dans une liste fifo 1.

Le second thread
lit les entiers depuis la liste fifo 1 et effectue un calcul de puissance 3 et stocke ensuite le résultat dans la liste fifo 2 :roll: Quand je vous disais que cela ne sert à rien.

Le troisième thread
lit les entiers depuis la liste fifo 2 et les affiche à l'écran.

ATTENTION : il faut activer la gestion des threads dans les option du compilateur.

Amuser vous à changer #BUFFER_LENGTH, pour constater les changements (minimum 1)

Code : Tout sélectionner

; -----------------------------------------------------------------------------
; TUTORIAL    : MULTI THREADS
; STAGE       : producer consumer
; AUTHOR      : MicrodevWeb
; DATE        : 2020-01-18
; SCENARIO    : a producer thread reads a character from a varaible
;               after turn it into integer he pushs it into a
;               fifo list
;               a consumer/producer loads a integer from the fifo list when the producer 
;               had build it
;               he turns at ¨3 and pushs it into a another fifo list
;               a display consumer loads the integer from a fifo list when the consumer/producer
;               had push this integer into the list and display it.
; -----------------------------------------------------------------------------
Global tab.s = "2-4-6-8-9-71-30-60-40-END"
#BUFFER_LENGTH = 2
Structure _FIF
  fi_1.FIFO::object
  fi_2.FIFO::object
EndStructure
Global par._FIF
Global THR_PRO,THR_CUS,THR_DIS


; --------------------------------------------------------------------------------
; Threads functions
; --------------------------------------------------------------------------------
; Read a text character by character and push it into a fifo list
Procedure read_thr(*p._FIF)
  Static pos = 1
  Protected v.s
  Debug "READ IS RUNNING"
  Repeat
    v = StringField(tab,pos,"-")
    If v = "END"        ; end of list of value
      *p\fi_1\push(-1)  ; Tel work is finiched
      Debug  "END OF READ"
      ProcedureReturn 
    EndIf
    Debug "READ : "+v
    *p\fi_1\push(Val(v))
    pos +1
  ForEver
EndProcedure
; turn character on upercase and push it into displaying fifo list
Procedure manage_thr(*p._FIF)
  Protected v,v2
  Debug "MANAGE IS RUNNING"
  Repeat 
    v = *p\fi_1\pop()
    If v = -1
      *p\fi_2\push(-1) ; tel end of work
      Debug "END OF MANAGE"
      ProcedureReturn 
    EndIf
    ; make operation ^3
    v2 = Pow(v,3)
    Debug "MANAGE : "+v+" INTO : "+v2
    *p\fi_2\push(v2)
  ForEver
EndProcedure
; display character
Procedure display_thr(*p._FIF)
  Protected vs.s,v.i
  Debug "DISPLAY IS RUNNING"
  Repeat 
    v = *p\fi_2\pop()
    If v = -1
      Debug "END OF DISPLAY"
      ProcedureReturn 
    EndIf
    vs = Str(v)
    Debug "DYSPLAY VALUE : "+vs
  ForEver
EndProcedure

; --------------------------------------------------------------------------------
; Main
; --------------------------------------------------------------------------------
; Iniation
;   create fifo list
par\fi_1 = FIFO::new(#BUFFER_LENGTH)
par\fi_2 = FIFO::new(#BUFFER_LENGTH)

THR_PRO = CreateThread(@read_thr(),par)
THR_CUS = CreateThread(@manage_thr(),par)
THR_DIS = CreateThread(@display_thr(),par)

; wait for threads
WaitThread(THR_PRO)
WaitThread(THR_CUS)
WaitThread(THR_DIS)

par\fi_1\free()
par\fi_2\free()

Debug "END OF PROGRAM"
Windows 10 64 bits PB: 5.70 ; 5.72 LST
Work at Centre Spatial de Liège
Avatar de l’utilisateur
blendman
Messages : 2017
Inscription : sam. 19/févr./2011 12:46

Re: TUTO Multi-Threads

Message par blendman »

salut

J'ai récemment vu un post de freak qui expliquait qu'un thread devait être terminé avant de fermer le programme :
You need to shutdown all threads before ending the program. Otherwise such conditions are bound to happen because the thread is still accessing resources that the main program is in the process of cleaning up.
(source : https://www.purebasic.fr/english/viewto ... 63#p576624)

C'est ce que tu fais dans ton code avec le "procedurereturn" dans chaque thread et c'est une façon de terminer un thread pour éviter d'avoir une IMA ou autre (invalid memory access) ;).

Mais je pense qu'il faut bien le préciser, car on pourrait croire que le programme termine lui-même les threads bien comme il faut (comme il ferme les images ou autre).
Or, s'il le fait bien, on ne peut pas savoir à l'avance si les thread sont bien fermés avant de fermer le programme ni dans quel ordre, s'ils utilisent encore des ressources (qui n'existent plus par exemple), etc.

A ce sujet, Stargate, sur le forum anglais, dans le lien précédent, nous donne un exemple d'un thread mal géré qui provoque parfois une IMA. Cela démontre qu'il faut bien gérer la fermeture des threads avant de fermer le programme comme nous l'a expliqué Freaks.

Personnellement, lorsque j'utilise un thread, avant de fermer le programme (avec "end" ou autre), il m'arrive de vérifier si les thread existent toujours avec une simple boucle, qui leur laisse le temps pour se fermer.
Autre remarque : pour éviter qu'un thread prenne tout le cpu, il m'arrive d'y mettre un delay(1), pour libérer les ressources, sinon, parfois, le programme prend 90% du cpu.

Un exemple très simple (pas de mutex ni semaphore) :

Code : Tout sélectionner


; Creation des thread dont on garde l'identifiant dans un tableau
nbThread=5
Global Dim MyThread(nbThread) ; mon tableau pour les identifiants des threads.
Global quitThread =0

Procedure DoTheThread(variablePourlethread)
  Protected Valeur
  Repeat
    ; le thread fait des trucs ici par exemple
    ; je mets un délai pour laisser du temps au cpu sinon, le programme prend 90% du cpu sur mon ordi.
    Delay(1)
    
    ; on l'arrête par exemple avec une variable globale
  Until quitThread =1
EndProcedure

; On crée des threads
For i = 0 To nbThread
  MyThread(i) = CreateThread(@DoTheThread(), i)
Next


; ici, le programme principal
If OpenWindow(0,0,0,600,400,"Gestion de threads",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)
  
  Repeat
    event = WaitWindowEvent(1)
  Until event = #PB_Event_CloseWindow
  
EndIf


; je dis au thread de s'arrêter
quitThread = 1

; on vérifie si chaque thread est arrêté avant d'appeler "end", sinon, on risque d'avoir des plantages, des erreurs, etc... 
; si le thread utilise une ressource que le programme a fermé avant de fermer un thread (une image ou une zone mémoire, etc).
Repeat
  ok = 1
  For i=0 To nbThread
    If IsThread(MyThread(i))<>0
      ok = 0
      Debug "le thread "+Str(i)+" existe"
      ; Break
    Else
      Debug "le thread "+Str(i)+" n'existe plus"
    EndIf
  Next
  
Until ok = 1


; les threads sont arrêtés et fermés
; on peut enfin arrêter le programme
; end va supprimer tout le reste (fenêtre gadget, image, mémoire, etc...)
End
LA documentation devrait d'ailleurs préciser ça : qu'on doit fermer les threads nous-mêmes si on utilise des ressources dans les threads, avant de fermer définitivement le programme.
Répondre