Nous attachons beaucoup de soins à nos librairies de programmation, particulièrement à leur facilité d'utilisation. Il y a cependant un point où nos librairies peuvent être piégeuses: l'utilisation de nos librairies depuis plusieurs threads. Cette semaine, nous clarifions et améliorons la situation.
Nous avons conçu notre API pour qu'il soit possible d'utiliser nos librairies efficacement depuis un seul thread, dans le but que les développeurs novices puissent utiliser facilement et efficacement notre API sans avoir à gérer des threads.
C'est pour cette raison que nos librairies n'étaient pas "thread safe", c'est-à-dire que si plusieurs threads utilisaient simultanément notre librairie, il était possible que notre code ne fonctionne pas correctement. Notre idée était que si un développeur sait utiliser correctement les threads, il sait aussi comment éviter les accès concurrents à la librairie Yoctopuce.
Pour certains langages, notamment C++, C# ou Java, l’utilisation des threads est très courante et une partie des développeurs préfère utiliser plusieurs threads plutôt qu'une machine à états pour réaliser plusieurs tâches. Dans ce cas, le développeur devait utiliser des mutex pour sérialiser les accès à la librairie Yoctopuce. Pour faciliter l'utilisation de notre API, nous avons décidé de rendre "thread safe" ces trois langages.
Un peu de théorie
Le problème avec la programmation multithread est lié à la concurrence. En effet, si plusieurs threads utilisent en même temps une ressource (variable, fichier, base de données, accès à un périphérique, etc) sans précaution, il y de grandes chances que cela se passe mal. Prenons l'exemple de la fonction suivante qui effectue un retrait sur un compte bancaire:
1: bool withdraw(int withdrawal) 2: { 3: if (balance >= withdrawal) { 4: balance = balance - withdrawal; 5: return true; 6: } 7: return false; 8: }
La fonction withdraw vérifie que le solde est suffisant avant de soustraire le montant du retrait du total du compte. Ce code fonctionne parfaitement si un seul thread l'utilise à la fois, mais si plusieurs threads appellent cette fonction en même temps le résultat peut être faux. Imaginons que le solde soit de 100$. Le premier thread effectue un retrait de 60$ et le deuxième thread effectue un retrait de 70$. Si les deux threads exécutent la ligne 3 en même temps, le deux retraits seront autorisés alors que la somme des deux retraits dépasse les 100$.
Heureusement, il existe un mécanisme pour régler cette situation: les exclusions mutuelles ou mutex. Sans rentrer dans les détails, les mutex sont un mécanisme qui permet de garantir qu'une ressource est utilisée par un seul thread à la fois. Tous les langages de programmation possèdent un mécanisme d'exclusion mutuelle.
Hélas, les mutex sont compliqués à utiliser et peuvent être la source d'autre bugs. Le cas le plus connu est le bug présent sur la sonde Mars Pathfinder.
L'impact sur nos librairies
Dans notre cas, la ressource partagée à protéger est la librairie Yoctopuce, et si deux threads appellent simultanément notre librairie il était possible que la librairie ne fonctionne pas correctement. Le code de notre librairie étant beaucoup plus long que notre précédent exemple, la probabilité que deux threads utilisent simultanément une même portion de code est plus petite. Toutefois, sur une application qui fonctionne 24h/24h, tôt ou tard la loi de Murphy s'appliquera.
C'est pour cette raison que le développeur devait implémenter son propre mécanisme de mutex pour s'assurer qu'il n'y ait pas d'appels simultanés à la librairie Yoctopuce. Ce travail étant pénible et compliqué, nous avons décidé d'inclure une protection directement dans la librairie.
Nouvelles librairies thread safe C++, C# et Java
Nous publions aujourd'hui un nouveau build qui rend thread safe les librairies C++, C#, Java et Android. C'est-à-dire que dorénavant la librairie supporte d'être appelée simultanément depuis plusieurs threads.
En interne, la librairie se protège contre les accès concurrent à l'aide de mutex. L’intérêt est double, d'une part cela simplifie le travail du développeur car il n'a plus besoin d'écrire son propre mécanisme de protection, d'autre part le code est plus efficace car seules les parties problématiques du code sont protégées.
Exemple
Voici deux manières de réaliser la même application.
La première sans utiliser de thread:
{
Console.WriteLine("Device arrival : " + m.get_serialNumber());
}
static void deviceRemoval(YModule m)
{
Console.WriteLine("Device removal : " + m.get_serialNumber());
}
static void Main(string[] args)
{
string errmsg = "";
if (YAPI.RegisterHub("usb", ref errmsg) != YAPI.SUCCESS)
{
Console.WriteLine("RegisterHub error: " + errmsg);
Environment.Exit(0);
}
YAPI.RegisterDeviceArrivalCallback(deviceArrival);
YAPI.RegisterDeviceRemovalCallback(deviceRemoval);
YTemperature temp = YTemperature.FirstTemperature();
Console.WriteLine("Hit Ctrl-C to Stop ");
while (true)
{
YAPI.UpdateDeviceList(ref errmsg);
YAPI.Sleep(3000, ref errmsg);
Console.WriteLine("Temp : " + temp.get_currentValue()
+ " " + temp.get_unit());
}
}
}
La deuxième version utilise un deuxième thread pour afficher la température:
{
Console.WriteLine("Device arrival : " + m.get_serialNumber());
}
static void deviceRemoval(YModule m)
{
Console.WriteLine("Device removal : " + m.get_serialNumber());
}
static void PollingThread()
{
while (true)
{
YTemperature temp = YTemperature.FirstTemperature();
while (true)
{
Console.WriteLine("Temp : " + temp.get_currentValue()
+ " " + temp.get_unit());
System.Threading.Thread.Sleep(3000);
}
}
}
static void Main(string[] args)
{
string errmsg = "";
if (YAPI.RegisterHub("usb", ref errmsg) != YAPI.SUCCESS)
{
Console.WriteLine("RegisterHub error: " + errmsg);
Environment.Exit(0);
}
Console.WriteLine("Start second thread that will log the temperature");
Thread pollThread = new Thread(new ThreadStart(PollingThread));
pollThread.Start();
YAPI.RegisterDeviceArrivalCallback(deviceArrival);
YAPI.RegisterDeviceRemovalCallback(deviceRemoval);
Console.WriteLine("Hit Ctrl-C to Stop ");
while (true)
{
YAPI.UpdateDeviceList(ref errmsg);
YAPI.Sleep(1000, ref errmsg);
}
}
Les appels à la librairie sont identiques et il n'est pas nécessaire de protéger les appels à la librairie Yoctopuce à l'aide de mutex.
Conclusion
L'utilisation de threads est une pratique très courante dans les langages C++, C# et Java et c'est pour cette raison que nous avons modifié ces librairies. Dorénavant, il n'est plus nécessaire de protéger la librairie contre les accès concurrents. Toutefois, gardez à l'esprit que l'utilisation de threads n'est pas toujours la manière la plus efficace de résoudre un problème, si votre application a 15 threads qui utilisent la librairie Yoctopuce, il est peut être temps de réfléchir à une manière plus efficace d'écrire votre code.
Note: Merci à Rob Krakora qui nous a aidé à identifier et fixer une condition de course particulièrement sournoise.