Bonjour à tous,
Je fais aujourd’hui face à la gestion des Threads en Go, qui m’apporte quelques mésaventures, notamment dues au fait qu’on ne sait pas vraiment ce qu’il se cache là derrière.
Pour les besoins de mon projet, je dois faire des interactions avec des machines à commande numérique (CNC) au travers du protocole Focas. (Il s’agit d’un protocole de communication développé par Fanuc, qui développe des commandes numériques pour les machines)
Pour celles et ceux qui ne sauraient pas, voici de quoi je parle:
J’ai à ma disposition une DLL
(Fwlib) me permettant d’interagir avec ces commandes numériques, dont on retrouve la documentation ici.
Dans mon projet en Go, j’interagis ainsi avec cette DLL.
Tout se passait bien, jusqu’au moment où … je commence à travailler avec les Goroutines, à ce moment-là, plus rien n’a fonctionné. Je me dis que la librairie ne supporte pas les accès concurrents, ce qui ne me poserait pas de problème.
Je me mis donc à mettre des lock (mutex) sur toutes les opérations faisant appel à la DLL. Et la, au moment de Run mon programme, toujours le même problème !
Je me dis que c’est des problèmes de temporisation, qu’il faut un peu calmer le jeu, je rajoute donc quelques sleep
.
Et bien, toujours pas ! Quand ça ne veut pas, ça ne veut pas. Du coup, je retire mes Goroutine
tout en laissant les sleeps
, et la, stupeur, ça fonctionne parfois.
Me disant que c’est une bibliothèque très mal faite, je vais tout de même faire un essai en C#
pour voir ce que ça donne et étrangement, ça fonctionne (uniquement les sleep
, pas les tâches asynchrones).
Recherche d’informations dans la documentation du protocole
Once the library handle number is acquired, it must be held until the application program terminates, because it is necessary to pass the number as an argument to each CNC/PMC Data window library function.
The library handle is owned by the thread that got it. Even if the thread-A which has already got a library handle shows the library handle to another thread-B, the thread-B cannot use that library handle.
Je tiens enfin une piste ! C’est donc de là que viens le problème avec les tâches exécutées en parallèle.
Je comprends aussi rapidement que le problème avec les sleep
viens du fait que le runtime Go
peut changer de Thread à tout moment. Pour confirmer cela, j’ai fait appel à la méthode runtime.LockOSThread()
, qui permet de bloquer l’exécution en cours sur le même Thread jusqu’à l’appel de runtime.UnlockOSThread()
. Et effectivement, ça fonctionne !
Sur le code suivant, qui ne fonctionnait pas, fonctionne après l’ajout de runtime.LockOSThread
. (Précision: focas.Channel()
est une méthode que j’ai implémentée qui va faire un appel avec l'handle
à la DLL).
runtime.LockOSThread()
for i := 0; i < 200; i++ {
_, e := focas.Channel()
if e != nil {
log.Fatalln(i, e)
}
time.Sleep(time.Millisecond * 10)
}
runtime.UnlockOSThread()
Comme vous le devinez surement, on en arrive enfin à mon problème.
- Ma première idée pour résoudre ce problème est de demander un
handle
à chaque appel à une méthode, le problème ? La demande d’unhandle
prend du temps (~250ms), le temps d’initialiser la connexion TCP/IP à la machine CNC. - Mon idée suivante, c’est de récupérer l’ID du Thread en cours, et de vérifier dans un
map
si unhandle
a déjà été généré pour ce Thread, si ce n’est pas le cas on en génère un, sinon on utilise l’existant.
La deuxième idée me paraît être idéale, mais un petit problème se pose. Il n’est pas possible en Go de savoir sur quel Thread l’exécution se fait ! Selon l’issue suivante, les développeurs du langage Go ne souhaitent pas rendre cela accessible, pour éviter que les développeurs en fassent n’importe quoi.
Finalement, ma dernière idée est d’ouvrir un handle
au démarrage dans une Goroutine, locké à son Thread. Ensuite à chaque appel à la DLL, je fais la demande d’un nouvel handle
que je libère ensuite.
Et le résultat est sans appel. Si j’ouvre un handle
que je maintiens, 100 exécutions de ma méthode me prennent 150ms
, si j’enlève le handle
maintenu, l’exécution passe à 22s ! L’exécution prend environ 150x plus de temps.
Vous trouverez ici le code source de mon essai. https://gist.github.com/WinXaito/c4eba0e808c8587dbdbd2a2687260bd5 (Pour passer de 150ms à 22s, j’ai simplement commenter les lignes 38 et 39)
Et finalement mes questions,
Concernant la dernière solution, qui semble fonctionner, je ne sais pas si elle est idéale. Qu’en pensez-vous ?
Concernant la deuxième proposition (récupérer un ID sur le Thread et maintenir une correspondance ID<->Handle), savez-vous si c’est faisable (malgré mes trouvailles je n’ai peut-être pas cherché assez loin) et surtout, est-ce que même si ça avait été possible, est-ce que ça aurait été une bonne idée ?
Auriez-vous peut-être d’autres idées plus idéales à me proposer ?
Je vous remercie, et vous souhaite d’avance de belles fêtes de fin d’années à toutes et tous !