Tutorial 15: Programmation Multi-liens

Dans ce Tutorial, nous allons voir comment créer un programme multi-liens. On va étudier aussi les méthodes de communication entre les liens.

Downloadez l'exemple ici.

Théorie:

Dans le Tutorial précédent, vous avez appris ce qu'était le lien minimum pour un Process: le lien primaire. Un lien est une chaîne d'exécution. Mais vous pouvez aussi créer des liens supplémentaires dans votre programme. Vous pouvez vous représenter le multi-liens comme une multigestion d'un programme. En terme d'exécution, un lien est une fonction qui tourne en parallèle avec le programme principal. Vous pouvez ouvrir puis commander plusieurs fois la même fonction ou bien vous pouvez diriger plusieurs fonctions simultanément selon vos exigences. Le multi-liens est spécifique à Win32, ça n'existe pas sous Win16.
Par exemple, le multi-liens c'est quand un Process ouvre plusieurs fois une même MessageBox (en changent son contenu si on le souhaite) ou bien quand une MessageBox est appelée par plusieurs process différents.

Les liens sont dirigés par un même process donc ils peuvent avoir accès à n'importe quelles ressources de ce process, telles que les variables globales, les Handles etc. Cependant, chaque lien a sa propre délimitation dans le programme, donc les variables locales de chaque liens sont privées vis à vis des autres liens. Chaque lien possède aussi son jeu de registre privé, donc quand Windows se sert d'autres liens, le lien peut "se rappeler" son statut précédant et peut "reprendre" son ancienne tâche quand il regagne son contrôle. Tout ça est traité intérieurement par Windows.
Voilà ce que permet le multi-liens. Dans les deux programmes 'Thread.exe' et 'Thread1.exe' on leur fait exécuter un calcul très lourd qui les occupe. Pour 'Thread.exe' qui ne possède qu'un lien simple ne peut plus rien faire d'autre que son calcul quand il est commencé, il ne répond plus aux actions de l'utilisateur comme déplacer sa fenêtre pendant le calcul.
'Thread1.exe' qui lui possède un système multi-liens, se sert de lien de travail pour faire le calcul mais laisse un lien principale pour répondre aux sollicitations de l'utilisateur. Comme ça, il répond encore, même pendant une lourde tache.

Nous pouvons diviser les liens en deux catégories :
  1. Les liens d'interface pour l'utilisateur : Ce type de liens crée sa propre fenêtre donc il reçoit des messages spécifiques aux fenêtres. Il peut répondre à l'utilisateur via sa propre fenêtre, de là son nom. Ce type de liens est soumis à la règle(autorité) Mutex de Win16 qui permet un seul lien d'interface d'utilisateur, en utilisation 16 bits avec le noyau gdi. alors qu'un lien d'interface d'utilisateur exécute le code d'utilisateur 16 bits avec le noyau gdi, les autres liens UI ne peuvent pas utiliser le service d'utilisateur 16 bits avec noyau gdi. Notez que ce Mutex Win16 est spécifique à Windows 95. Windows NT ne possède pas de Mutex Win16, c'est pourquoi les liens d'interface d'utilisateur de Windows NT s'effectuent plus doucement que sous le Windows 95.
  2. Les liens de travail : Ce type de liens ne créent pas de fenêtre donc ils ne peuvent pas recevoir de message spécifiques aux fenêtres. Ils existent principalement pour faire le travail assigné en arrière-plan, de là leur nom.
Je conseille la stratégie suivante pour utiliser la pleine capacité des multi-liens sous Win32 : Laissez le lien primaire faire le truc d'interface d'utilisateur et les autres liens faire le travail complexe en arrière-plan. De cette façon, le lien primaire servira de 'Gouverneur', et les autres liens seront un peu comme le 'personnel du Gouverneur'. Le Gouverneur délègue ses responsabilités à son personnel tandis qu'il maintient le contact avec le public(l'utilisateur). Le personnel du Gouverneur exécute avec obéissance le travail et l'annonce de ses résultats. Si le Gouverneur (le lien primaire) devait exécuter chaque tâche lui-même, il ne serait pas capable de donner beaucoup d'attention au public ou à la presse. Tout ça s'apparente à une fenêtre qui a pour action de donner un long travail pour occupée son lien primaire : il ne répond pas à l'utilisateur avant que le travail ne soit achevé. Un tel programme peut en profiter pour créer un lien additionel qui sera responsable du long travail, permettant ainsi au lien primaire de répondre aux commandes de l'utilisateur.
Nous pouvons créer un lien en appelant la fonction CreateThread, laquelle a la syntaxe suivante :

CreateThread proto lpThreadAttributes:DWORD,\
                                dwStackSize:DWORD,\
                                lpStartAddress:DWORD,\
                                lpParameter:DWORD,\
                                dwCreationFlags:DWORD,\
                                lpThreadId:DWORD

Le fonctionnent de CreateThread ressemble beaucoup à celui de CreateProcess.
lpThreadAttributes  Vous pouvez le mettre à 0 (NULL) si vous souhaitez que le lien soit le descripteur de sécurité par défaut.
dwStackSize Ça spécifie la taille à réserver sur la pile pour le lien. Si vous voulez que le lien ait la même taille de pile que le lien primaire, employez le 0 (NULL) pour ce paramètre.
lpStartAddress Adresse de la fonction qui fait le 'lien'. C'est la fonction qui exécutera le travail (qui fera le lien). Cette fonction DOIT recevoir un et seulement un paramètre 32 bits et renvoyer une valeur sur 32 bits.
lpParameter C'est le paramètre que vous devez passer à la fonction 'lien'.
dwCreationFlags 0 signifie que le lien prend le commandement immédiatement après qu'il est été créé. L'opposé c'est le flag 'CREATE_SUSPENDED'.
lpThreadId La fonction 'CreateThread' donnera l'ID du nouveau lien créé à cette adresse.

Si l'appel de CreateThread s'est déroulé avec succès, il rend l'handle du lien nouvellement créé. Sinon, il renvoie le NULL.
La fonction de lien en en cours aussitôt après que l'appel de CreateThread soit un succès, à moins que vous ne mettez le flag 'CREATE_SUSPENDED' dans dwCreationFlags. Dans ce cas, le lien est suspendu jusqu'à ce que la fonction ResumeThread soit appelée.
Après les retours de la 'fonction de lien' (avec l'instruction ret), Windows appelle la fonction ExitThread tout seul. Mais vous pouvez appeler la fonction ExitThread dans votre 'fonction de lien' par vous-même, mais là je développerai pas !
Vous pouvez retrouver le code de sortie d'un lien en appelant la fonction GetExitCodeThread.
Si vous voulez terminer un lien, vous pouvez appeler la fonction TerminateThread. Mais vous devez employer cette fonction seulement dans des circonstances extrêmes puisque cette fonction termine le lien immédiatement sans lui donner aucune chance de se fermer proprement, sans laissez derrière lui des valeurs qui lui sont spécifiques dans certaines variables.


Maintenant on va vous montrer les méthodes de communications entre les liens.
Il y en a trois:

Les liens partagent les ressources du process en incluant des variables globales donc les liens peuvent utiliser ces varibles globales pour communiquer les unes avec les autres. Cependant cette méthode doit être utilisée avec grand soin. La synchronisation des liens doit être prise en compte. Par exemple, si deux liens emploient la même structure de 10 membres, ce qui arrive quand Windows prend soudainement le contrôle d'un lien lorsqu'il est en pleine remise à jour de sa structure, l'autre lien sera alors abandonné avec des données incomplètes ou sans significations pour sa structure! Ne faites pas d'erreur, les programmes multi-liens sont bien plus durs à mettre au point et à maintenir. Cette sorte de bogue(défaut) semble bien arriver au hasard, ce qui fait que c'est très difficile à traquer.
Vous pouvez aussi employer des messages de fenêtre pour communiquer entre les liens. Si les liens sont tous des interfaces d'utilisateur, il n'y a aucun problème : cette méthode peut être employée en tant que communication bilatérale. Tout ce que vous devez faire c'est définit un ou plus messages de fenêtres sur mesure lesquels seront spécifiques aux liens. Vous définissez un message sur mesure en employant le message WM_USER comme valeur de base. Voilà, vous pouvez le définir comme ceci :

        WM_MYCUSTOMMSG equ WM_USER+100h

Windows n'emploiera jamais aucune valeur WM_USER vers ses propres messages donc vous pouvez employer la valeur WM_USER pour votre propres messages faits sur mesure.
Si un des lien est un lien d'interface utilisateur (le lien primaire, le Gouverneur) et un autre est un des 'personnel du gouverneur', vous ne pouvez pas employer cette méthode en tant que communication bilatérale puisque un lien 'personnel' ne possède pas sa propre fenêtre. Donc il n'a y pas de file d'attente de message. Vous pouvez employer le schéma suivant :

                            User interface Thread ------> global variable(s)----> Worker thread
                            Worker Thread  ------> custom window message(s) ----> User interface Thread

Soit en Français:

                            Lien d'interface d'utilisateur (primaire)------> variable(s) globale(s)----> lien de travail (personnel)
                     lien de travail (personnel)------>'message(s) de fenêtre' conçu sur mesure----> lien d'interface d'utilisateur

En fait, nous employons cette méthode dans notre exemple.
La dernière sorte de communication c'est l'événement objet. Vous pouvez vous représenter l'événement' comme une sorte de flag. Si l'événement est dans l'état "non signalé", le lien est inerte ou en sommeil, dans cet état, le lien ne reçoit pas d'ordre du CPU. Quand l'événement est dans l'état "signalé", Windows "réveille" le lien et il commence à exécuter la tâche assignée.

Exemple:

Vous devez Downloader le fichier *.Zip en exemple et lancer 'thread1.exe'. Cliquez sur le sous-menu (le menu item) "Savage Calculation". Ça ordonnera au programme d'exécuter l'instruction "add eax, eax" 600,000,000 fois. Remarquez que pendant ce temps-là, vous ne pouvez rien faire dans la fenêtre principale : vous ne pouvez pas la déplacer, vous ne pouvez pas activer son menu, etc. Quand le calcul est achevé, une boîte de message apparaît. Après ça, la fenêtre accepte de nouveau vos commandes normalement.
Pour éviter à l'utilisateur ce type d'inconvénient, nous pouvons déplacer la routine "de calcul" dans un lien de travail (personnel du gouverneur) séparé et laisser le lien primaire (le lien gouverneur) continuer avec sa tâche d'interface utilisateur. Vous pouvez voir que bien que la fenêtre principale réponde plus lentement que d'habitude, elle répond quand même toujours.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName  db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,OFFSET MenuName
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,300,200,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    .WHILE TRUE
            invoke GetMessage, ADDR msg,NULL,0,0
            .BREAK .IF (!eax)
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
    .ENDW
    mov     eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .IF uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .ELSEIF uMsg==WM_COMMAND
        mov eax,wParam
        .if lParam==0
            .if ax==IDM_CREATE_THREAD
                mov  eax,OFFSET ThreadProc
                invoke CreateThread,NULL,NULL,eax,\
                                        0,\
                                        ADDR ThreadID
                invoke CloseHandle,eax
            .else
                invoke DestroyWindow,hWnd
            .endif
        .endif
    .ELSEIF uMsg==WM_FINISH
        invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .ENDIF
    xor    eax,eax
    ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
        mov  ecx,600000000
Loop1:
        add  eax,eax
        dec  ecx
        jz   Get_out
        jmp  Loop1
Get_out:
        invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
        ret
ThreadProc ENDP

end start
 

Analyse:

Le programme principal se présente à l'utilisateur en tant qu'une fenêtre normale avec un menu. Si l'utilisateur choisit le menu item "Create Thread", le programme crée un lien comme indiqué ci-dessous :

            .if ax==IDM_CREATE_THREAD
                mov  eax,OFFSET ThreadProc
                invoke CreateThread,NULL,NULL,eax,\
                                        NULL,0,\
                                        ADDR ThreadID
                invoke CloseHandle,eax
 
La susdite fonction crée un lien qui contrôlera une procédure nommée ThreadProc en parallèle avec le lien primaire. Après que cet appel soit couronné de succès, CreateThread revoie ses valeurs de retours immédiatement et ThreadProc se lance. Puisque nous n'employons pas d'handle de lien, nous devons le fermer sinon il y aura des fuites de mémoire. Notez que la fermeture de l'handle du lien ne termine pas ce lien. Son seul effet est que désormais nous ne pourrons plus employer l'handle du lien.

ThreadProc PROC USES ecx Param:DWORD
        mov  ecx,600000000
Loop1:
        add  eax,eax
        dec  ecx
        jz   Get_out
        jmp  Loop1
Get_out:
        invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
        ret
ThreadProc ENDP

Comme vous pouvez le voir, ThreadProc exécute un calcul sauvage (un calcul immense qui occupe le système) qui prend un longue période de temps avant de se terminer, et quand il est finit il envoie enfin le message WM_FINISH vers la fenêtre principale. WM_FINISH est notre message (celui qu'on a créé sur mesure). Il est ainsi défini :

Vous n'êtres pas obligez d'ajouter WM_USER et 100h mais c'est plus sûr quand même de faire ça.
Le message WM_FINISH n'a de signification uniquement que dans notre programme. Quand la fenêtre principale reçoit le message WM_FINISH, ça fait apparaître une boîte de message disant que le calcul est achevé.
Vous pouvez créer plusieurs liens en même temps en cliquant sur "Create Thread" plusieurs fois.
Dans cet exemple, la communication est à sens unique, c'est pour ça que c'est le lien principal uniquement, qui peut informer la fenêtre principale. Si vous voulez que le lien principal envoie des commandes au lien de travail, voilà comment faire :
Quand l'utilisateur choisit le sous-menu "Kill Thread", le programme principal mettra la valeur TRUE dans la commande flag. Quand ThreadProc voit que la valeur de la commande flag est TRUE(VRAI), on sort de la boucle et on rompt ainsi les liens.

[Iczelion's Win32 Assembly HomePage]



Traduit par Morgatte