J'ai besoin de paralléliser certaines tâches dans un programme C++ et je suis complètement nouveau dans la programmation parallèle. J'ai fait des progrès grâce aux recherches sur Internet jusqu'à présent, mais je suis un peu bloqué maintenant. J'aimerais réutiliser certains threads en boucle, mais je ne sais clairement pas comment faire ce que j'essaie de faire.
J'achète des données à partir de deux cartes ADC sur l'ordinateur (acquises en parallèle), puis je dois effectuer certaines opérations sur les données collectées (traitées en parallèle) lors de la collecte du prochain lot de données. Voici un pseudocode pour illustrer
//Acquire some data, wait for all the data to be acquired before proceeding
std::thread acq1(AcquireData, boardHandle1, memoryAddress1a);
std::thread acq2(AcquireData, boardHandle2, memoryAddress2a);
acq1.join();
acq2.join();
while(user doesn't interrupt)
{
//Process first batch of data while acquiring new data
std::thread proc1(ProcessData,memoryAddress1a);
std::thread proc2(ProcessData,memoryAddress2a);
acq1(AcquireData, boardHandle1, memoryAddress1b);
acq2(AcquireData, boardHandle2, memoryAddress2b);
acq1.join();
acq2.join();
proc1.join();
proc2.join();
/*Proceed in this manner, alternating which memory address
is written to and being processed until the user interrupts the program.*/
}
C'est l'essentiel. La prochaine exécution de la boucle écrirait dans les adresses mémoire "a" lors du traitement des données "b" et continuerait à alterner (je peux obtenir le code pour le faire, je l'ai simplement retiré pour éviter d'encombrer le problème).
Quoi qu'il en soit, le problème (comme je suis sûr que certaines personnes le savent déjà) est que la deuxième fois que j'essaie d'utiliser acq1 et acq2, le compilateur (VS2012) dit "IntelliSense: appel d'un objet d'un type de classe sans opérateur approprié ( ) ou des fonctions de conversion en type pointeur vers fonction ". De même, si je remets std :: thread devant acq1 et acq2, il dit "erreur C2374: 'acq1': redéfinition; initialisation multiple".
La question est donc la suivante: puis-je réaffecter des threads à une nouvelle tâche lorsqu'ils ont terminé leur tâche précédente? J'attends toujours la fin de l'utilisation précédente du thread avant de l'appeler à nouveau, mais je ne sais pas comment réaffecter le thread, et comme il est en boucle, je ne peux pas créer un nouveau thread à chaque fois (ou si je pourrait, cela semble inutile et inutile, mais je peux me tromper).
Merci d'avance
Le moyen le plus simple consiste à utiliser une file d'attente de std::function
objets. Comme ça:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <chrono>
class ThreadPool
{
public:
ThreadPool (int threads) : shutdown_ (false)
{
// Create the specified number of threads
threads_.reserve (threads);
for (int i = 0; i < threads; ++i)
threads_.emplace_back (std::bind (&ThreadPool::threadEntry, this, i));
}
~ThreadPool ()
{
{
// Unblock any threads and tell them to stop
std::unique_lock <std::mutex> l (lock_);
shutdown_ = true;
condVar_.notify_all();
}
// Wait for all threads to stop
std::cerr << "Joining threads" << std::endl;
for (auto& thread : threads_)
thread.join();
}
void doJob (std::function <void (void)> func)
{
// Place a job on the queu and unblock a thread
std::unique_lock <std::mutex> l (lock_);
jobs_.emplace (std::move (func));
condVar_.notify_one();
}
protected:
void threadEntry (int i)
{
std::function <void (void)> job;
while (1)
{
{
std::unique_lock <std::mutex> l (lock_);
while (! shutdown_ && jobs_.empty())
condVar_.wait (l);
if (jobs_.empty ())
{
// No jobs to do and we are shutting down
std::cerr << "Thread " << i << " terminates" << std::endl;
return;
}
std::cerr << "Thread " << i << " does a job" << std::endl;
job = std::move (jobs_.front ());
jobs_.pop();
}
// Do the job without holding any locks
job ();
}
}
std::mutex lock_;
std::condition_variable condVar_;
bool shutdown_;
std::queue <std::function <void (void)>> jobs_;
std::vector <std::thread> threads_;
};
void silly (int n)
{
// A silly job for demonstration purposes
std::cerr << "Sleeping for " << n << " seconds" << std::endl;
std::this_thread::sleep_for (std::chrono::seconds (n));
}
int main()
{
// Create two threads
ThreadPool p (2);
// Assign them 4 jobs
p.doJob (std::bind (silly, 1));
p.doJob (std::bind (silly, 2));
p.doJob (std::bind (silly, 3));
p.doJob (std::bind (silly, 4));
}
La classe std::thread
Est conçue pour exécuter exactement une tâche (celle que vous lui donnez dans le constructeur), puis se termine. Si vous voulez faire plus de travail, vous aurez besoin d'un nouveau fil. Depuis C++ 11, c'est tout ce que nous avons. Les pools de threads ne sont pas devenus la norme. (Je ne sais pas ce que C++ 14 a à dire à leur sujet.)
Heureusement, vous pouvez facilement implémenter vous-même la logique requise. Voici l'image à grande échelle:
La partie la plus difficile ici (qui est encore assez facile) est de bien concevoir la file d'attente de travail. Habituellement, une liste chaînée synchronisée (de la STL) fera l'affaire. Synchronisé signifie que tout thread qui souhaite manipuler la file d'attente ne doit le faire qu'après avoir acquis un std::mutex
Afin d'éviter les conditions de concurrence. Si un thread de travail trouve la liste vide, il doit attendre qu'il y ait à nouveau du travail. Vous pouvez utiliser un std::condition_variable
Pour cela. Chaque fois qu'une nouvelle tâche est insérée dans la file d'attente, le thread d'insertion notifie un thread qui attend la variable de condition et arrêtera donc le blocage et finira par commencer le traitement de la nouvelle tâche.
La deuxième partie pas si banale est de savoir comment signaler aux threads de travail qu'il n'y a plus de travail à faire. De toute évidence, vous pouvez définir un indicateur global, mais si un travailleur est bloqué en attente dans la file d'attente, il ne s'en rendra pas compte de sitôt. Une solution pourrait être de notify_all()
threads et de les faire vérifier l'indicateur chaque fois qu'ils sont informés. Une autre option consiste à insérer un élément "toxique" distinct dans la file d'attente. Si un travailleur rencontre cet objet, il se quitte.
La représentation d'une file d'attente de tâches est simple à l'aide de vos objets task
auto-définis ou simplement de lambdas.
Tous les éléments ci-dessus sont des fonctionnalités C++ 11. Si vous êtes bloqué avec une version antérieure, vous devrez recourir à des bibliothèques tierces qui fournissent le multithread pour votre plate-forme particulière.
Bien que rien de tout cela ne soit sorcier, il est toujours facile de se tromper la première fois. Et malheureusement, les bogues liés à la concurrence sont parmi les plus difficiles à déboguer. Commencer par passer quelques heures à lire les sections pertinentes d'un bon livre ou à parcourir un didacticiel peut rapidement porter ses fruits.
Eh bien, cela dépend si vous envisagez de déplacer une réaffectation ou non. Vous pouvez déplacer un thread mais pas en faire une copie.
Le code ci-dessous créera une nouvelle paire de threads à chaque itération et les déplacera à la place des anciens threads. J'imagine que cela devrait fonctionner, car les nouveaux objets thread
seront temporaires.
while(user doesn't interrupt)
{
//Process first batch of data while acquiring new data
std::thread proc1(ProcessData,memoryAddress1a);
std::thread proc2(ProcessData,memoryAddress2a);
acq1 = std::thread(AcquireData, boardHandle1, memoryAddress1b);
acq2 = std::thread(AcquireData, boardHandle2, memoryAddress2b);
acq1.join();
acq2.join();
proc1.join();
proc2.join();
/*Proceed in this manner, alternating which memory address
is written to and being processed until the user interrupts the program.*/
}
Ce qui se passe, c'est que l'objet ne termine pas sa durée de vie à la fin de l'itération, car il est déclaré dans la portée externe en ce qui concerne la boucle. Mais un nouvel objet est créé à chaque fois et move
a lieu. Je ne vois pas ce qui peut être épargné (je pourrais être stupide), donc j'imagine que c'est exactement la même chose que de déclarer acq
s à l'intérieur de la boucle et de simplement réutiliser le symbole. Dans l'ensemble ... oui, il s'agit de la façon de classer un create temporaire et move
.
De plus, cela démarre clairement un nouveau thread à chaque boucle (bien sûr, en terminant le thread précédemment attribué), cela ne fait pas attendre un thread pour de nouvelles données et le nourrir comme par magie dans le tube de traitement. Vous auriez besoin de l'implémenter différemment. Par exemple: pool de threads de travail et communication sur les files d'attente.
Références: operator=
, (ctor)
.
Je pense que les erreurs que vous obtenez sont explicites, donc je vais sauter de les expliquer.
Ce
std::thread acq1(...)
est l'appel d'un constructeur. la construction d'un nouvel objet appelé acq1
Ce
acq1(...)
est l'application de l'opérateur () sur l'objet existant aqc1. S'il n'y a pas un tel opérateur défini pour std :: thread, le compilateur se plaint.
Pour autant que je sache, vous ne pouvez pas réutiliser les threads std ::. Vous les construisez et les démarrez. Rejoignez-les et jetez-les,