Nouveautés C++ de la Preview de Visual Studio 11 Partie I
Vous retrouverez dans la documentation MSDN https://msdn.microsoft.com/en-us/library/hh409293(v=VS.110).aspx toutes les nouvelles fonctionnalités de la preview de Visual Studio 11. Ce que je me propose dans ce 1er billet c’est d’en reprendre la structure mais si possible d’y associer un exemple de code.
Téléchargement de la CTP de Visual Studio 11
Amélioration du support du Standard C++11
Nous supportons désormais dans cette preview de Visual Studio de nouveaux fichiers d’entête pour la STL, concernant d’ailleurs pour une bonne part la gestion du multi-thread et d’opérations asynchrones.
<thread>, <future>, <atomic>, <chrono>, <mutex>, <condition_variable>, <ratio>, <filesystem>
L’entête <thread> comme son nom l’indique permet de créer et de manipuler des threads
Code Snippet
- thread t([]()
- {
- cout << "ThreadID : " << std::this_thread::get_id() << endl;
- });
- t.join();
En lieu et place d’une méthode à passer au constructeur de la class thread, nous utilisons ici une expression Lambda introduit avec C++11. La méthode join() qui est un appel bloquant, permet au thread principal d’attendre que le thread ai fini son travail. Si on souhaite découpler la variable t de type thread du thread Windows il existe la méthode detach() pour le faire. La méthode detach() en arrière plan, ne fait que fermer le handle windows (CloseHandle) associé au thread. Il est donc possible d'’utiliser nos bonnes vieilles API Windows avec la variable t de type thread, en récupérant le handle natif, mais le code devient bien moins portable.
Code Snippet
- WaitForSingleObject(t.native_handle()._Hnd ,INFINITE);
- t.detach();
La méthode join() sur le thread d’ailleurs, fait sensiblement la même chose que le code ci-dessus (Sur la plate-forme Windows bien sur).
Il est possible également à l’aide du thread de récupérer le nombre de processeurs virtuels disponible à l'aide de la méthode hardware_concurrency() ,
Code Snippet
- unsigned numLogicalProc=t.hardware_concurrency();
La manipulation de threads, va toujours de paire avec la synchronisation et la protection d’une zone critique. l’entête <mutex> fournit à cette effet des objets de synchronisation à exclusion mutuelle exemple.
Code Snippet
- mutex m;
- thread t1([&m]()
- {
- std::this_thread::sleep_for (chrono::seconds(1));
- for(int i=0;i<10;i++)
- {
- m.lock();
- cout << "ThreadID : " << std::this_thread::get_id() << ":" << i << endl;
- m.unlock ();
- }
- });
- thread t2([&m]()
- {
- std::this_thread::sleep_for (chrono::seconds(1));
- for(int i=0;i<10;i++)
- {
- m.lock ();
- cout << "ThreadID : " << std::this_thread::get_id() << ":" << i << endl;
- m.unlock();
- }
- });
- t1.join();
- t2.join();
Attention, l’utilisation de verrous à toujours une incidence sur les performances !!!
Vous noterez également l’introduction de l’espace de nom this_thread pour récupérer le numéro d’identification du thread courant ou pour créer des points d’attentes en conjonction avec la classe chrono.
Il est possible également de contrôler le flow d’exécution de plusieurs threads à l’aide de l’entête <condition_variable>, comme sur l’exemple Producteur/Consommateur ci-dessous.
Code Snippet
mutex lockBuffer;
volatile BOOL ArretDemande=FALSE;
queue<long> buffer;
condition_variable_any cndNotifierConsommateurs;
condition_variable_any cndNotifierProducteur;
thread ThreadConsommateur([&]()
{
while(true)
{
lockBuffer.lock ();
while(buffer.empty () && ArretDemande==FALSE)
{
cndNotifierConsommateurs.wait(lockBuffer);
}
if (ArretDemande==TRUE && buffer.empty ())
{
lockBuffer.unlock();
cndNotifierProducteur.notify_one ();
break;
}
long element=buffer.front();
buffer.pop ();
cout << "Consommation element :" << element << " Taille de la file :" << buffer.size() << endl;
lockBuffer.unlock ();
cndNotifierProducteur.notify_one ();
}
});
thread ThreadProducteur([&]()
{
//Operation atomic sur un long
std::atomic<long> interlock;
interlock=1;
while(true)
{
////Simule une charge
std::this_thread::sleep_for (chrono::milliseconds (15));
long element=interlock.fetch_add (1);
lockBuffer.lock ();
while(buffer.size()==10 && ArretDemande ==FALSE)
{
cndNotifierProducteur.wait (lockBuffer);
}
if (ArretDemande==TRUE)
{
lockBuffer.unlock ();
cndNotifierConsommateurs.notify_one ();
break;
}
buffer.push(element);
cout << "Production unlement :" << element << " Taille de la file :" << buffer.size() << endl;
lockBuffer.unlock ();
cndNotifierConsommateurs.notify_one ();
}
});
std::cout << "Pour arreter pressez [ENTREZ]" << std::endl;
getchar();
std::cout << "Arret demande" << endl;
ArretDemande=TRUE;
ThreadProducteur.join();
ThreadConsommateur.join();
Vous noterez ici que nous utilisons pour notre consommateur et notre producteur un mutex que nous passons à la méthode wait() de la variable de type condition_variable_any (il est possible également d’utiliser le type condition_variable avec un unique_lock<mutex> , dans ce dernier cas le mutex est directement passé à l’état non signalé lors de l’initialisation du type unique_lock. L’état non signalé indique que le mutex ne peut être acquis par un autre thread que celui qui en est actuellement le propriétaire..
Dans notre exemple, le mutex passe à l’état non signalé à l’aide de la méthode lock(). néanmoins si la file est vide (c’est à dire si le producteur n’a encore rien produit, le thread ce met en attente à l’aide de la méthode wait() de notre condition_variable_any en libérant le mutex, afin que le thread du producteur qui est lui même en attente d’acquisition de ce mutex (méthode lock()), puisse commencer à produire des éléments dans la file.
Ce mutex, n’est utilisé que pour protéger le type queue<int> buffer. La méthode wait() utilise un autre mécanisme pour ce mettre en attente et ne sera liberée, par le thread du producteur que lorsqu’il il appellera la méthode notify_one().
Vous noterez également la manière dont nous avons utiliser ici le type atomic, afin d’incrémenter de 1 en une seule opération atomique notre élément de la file. Une opération atomique garantie que dans un contexte fortement multi-thread, une addition par exemple sera toujours juste.
Je finirai cette 1er partie, par la notion de future et donc de l’entête <future>. Un future sert à exécuter des opérations asynchrones qui retournent un résultat, que nous voulons récupérer plus tard, mais sans se préoccuper d’aucun mécanisme de synchronisation ou de contrôle de flow de thread. Dans les exemples précédant, nous utilisions, la méthode join() comme point de rendez-vous de plusieurs threads des mutex et des objets de controle de flow, avec le future ce n’est plus la peine.
En effet, imaginons que vous souhaitiez faire une simple addition de deux entiers A+B, mais qui proviennent de résultats retournés par deux threads différentes. Allez-y, vous avez 5 minutes pour faire l’exercice avec les objets que je viens de vous montrer !!!!
Si vous n’avez pas le temps, vous utiliserez alors la notion de future comme dans l’exemple ci-dessous
Code Snippet
- std::cout << "Thread Principale : ID : " << std::this_thread::get_id() << endl;
- future<int> f1(async([]()->int
- {
- //Simule une charge
- std::this_thread::sleep_for (chrono::milliseconds (2000));
- std::cout << "Future 1 ID : " << std::this_thread::get_id() << endl;
- return 42;
- }));
- future<int> f2(async([]()->int
- {
- std::cout << "Future 2 ID : " << std::this_thread::get_id() << endl;
- return 84;
- }));
- std::cout << "Resultat : " << f1.get () + f2.get() << endl ;
Ici je déclare deux futures de type int, qui prennent comme paramètre dans leur constructeur, un type async permettant comme son nom l’indique d’exécuter des opérations asynchrones dans des threads différents.
Les deux futures retourneront un résultat, mais je ne sais pas quand dans le future !!. La méthode get() qui est un appel bloquant me garantie que l’addition de mes deux entiers sera correcte.
Avec les futures, nous utilisons une syntaxe pour de l’exécution asynchrone qui se rapproche fortement de la syntaxe synchrone.
A bientôt
Eric Vernié