web-dev-qa-db-fra.com

Comment faire facilement std :: cout thread-safe?

J'ai une application multi-thread, qui utilise fortement std::cout pour la journalisation sans aucun verrouillage. Dans ce cas, comment puis-je facilement ajouter un mécanisme de verrouillage pour rendre std::cout thread-safe?

Je ne veux pas rechercher chaque occurrence de std::cout et ajouter une ligne de code de verrouillage. C'est trop fastidieux.

Une meilleure pratique?

23
xmllmx

Je suppose que vous pourriez implémenter votre propre classe qui enveloppe cout et associe un mutex à celle-ci. Le operator << de cette nouvelle classe ferait trois choses:

  1. crée un verrou pour le mutex, bloquant éventuellement d'autres threads
  2. faire la sortie, c.-à-d. l'opérateur << pour le flux enveloppé et l'argument passé
  3. construire une instance de différent class en passant le verrou à celui

Cette classe différente conserverait le verrou et déléguerait l'opérateur << au flux encapsulé. Le destructeur de cette seconde classe finirait par détruire le verrou et libérerait le mutex.

Ainsi, toute sortie écrite en tant qu’instruction unique, c’est-à-dire sous la forme d’une séquence unique d’appels <<, sera imprimée de manière atomique tant que toute votre sortie passe par cet objet avec le même mutex.

Appelons les deux classes synchronized_ostream et locked_ostream. Si sync_cout est une instance de synchronized_ostream qui enveloppe std::cout, la séquence

sync_cout << "Hello, " << name << "!" << std::endl;

entraînerait les actions suivantes:

  1. synchronized_ostream::operator<< obtiendrait le verrou
  2. synchronized_ostream::operator<< déléguerait l'impression de "Hello" à cout
  3. operator<<(std::ostream&, const char*) afficherait "Bonjour"
  4. synchronized_ostream::operator<< construirait un locked_ostream et passerait le verrou à celui
  5. locked_ostream::operator<< déléguerait l'impression de name à cout
  6. operator<<(std::ostream&, std::string) afficherait le nom
  7. La même délégation à cout se produit pour le point d'exclamation et le manipulateur de ligne de fond
  8. Le locked_ostream temporaire est détruit, le verrou est relâché
17
MvG

Bien que je ne puisse pas être sûr que cela s'applique à chaque compilateur/version de std libs , Mais dans la base de code, j'utilise std :: cout :: operator << (), il est déjà thread-safe.

Je suppose que ce que vous essayez vraiment de faire empêche std :: cout de mélanger chaîne lors de la concaténation avec l'opérateur << plusieurs fois par chaîne, sur plusieurs threads.

La raison pour laquelle les chaînes sont brouillées est qu’il existe une course "externe" sur l’opérateur << Cela peut entraîner de tels événements.

//Thread 1
std::cout << "the quick brown fox " << "jumped over the lazy dog " << std::endl;

//Thread 2
std::cout << "my mother washes" << " seashells by the sea shore" << std::endl;

//Could just as easily print like this or any other crazy order.
my mother washes the quick brown fox seashells by the sea sure \n
jumped of the lazy dog \n

Si tel est le cas, il existe une solution beaucoup plus simple que de créer votre propre compte sécurisé ou de mettre en place un verrou à utiliser avec cout. 

Composez simplement votre chaîne avant de la passer au cout

Par exemple.

//There are other ways, but stringstream uses << just like cout.. 
std::stringstream msg;
msg << "Error:" << Err_num << ", " << ErrorString( Err_num ) << "\n"; 
std::cout << msg.str();

De cette façon, vos piqûres ne peuvent pas être brouillées car elles sont déjà entièrement formées. De plus, il est également préférable de former vos cordes de toute façon avant de les expédier.

24
Nickolas George

J'aime beaucoup le truc de Nicolás donné dans cette question de créer un objet temporaire et de placer le code de protection sur le destructeur.

/** Thread safe cout class
  * Exemple of use:
  *    PrintThread{} << "Hello world!" << std::endl;
  */
class PrintThread: public std::ostringstream
{
public:
    PrintThread() = default;

    ~PrintThread()
    {
        std::lock_guard<std::mutex> guard(_mutexPrint);
        std::cout << this->str();
    }

private:
    static std::mutex _mutexPrint;
};

std::mutex PrintThread::_mutexPrint{};

Vous pouvez ensuite l'utiliser comme un std::cout normal, à partir de n'importe quel thread:

PrintThread{} << "my_val=" << val << std::endl;

L'objet collecte les données sous forme d'une ostringstream régulière. Dès que le coma est atteint, l'objet est détruit et efface toutes les informations collectées.

9
Conchylicultor

Depuis C++20, vous pouvez utiliser std::osyncstream wrapper: 

http://fr.cppreference.com/w/cpp/io/basic_osyncstream

{
  std::osyncstream bout(std::cout); // synchronized wrapper for std::cout
  bout << "Hello, ";
  bout << "World!";
  bout << std::endl; // flush is noted, but not yet performed
  bout << "and more!\n";
} // characters are transferred and std::cout is flushed

Il fournit la garantie que toutes les sorties sont effectuées vers la même finale Le tampon de destination (std :: cout dans les exemples ci-dessus) sera exempt de courses de données et ne sera ni entrelacé ni brouillé de quelque manière que ce soit, aussi longtemps. chaque écriture dans la mémoire tampon de destination finale est effectuée via (éventuellement différentes) instances de std :: basic_osyncstream.

5
lilezek

Pour un débogage rapide des applications c ++ 11 et éviter les sorties entrelacées, j'écris simplement de petites fonctions comme celles-ci:

...
#include <mutex>
...
mutex m_screen;
...
void msg(char const * const message);
...
void msg(char const * const message)
{
  m_screen.lock();
  cout << message << endl;
  m_screen.unlock();
}

J'utilise ces types de fonctions pour les sorties et si des valeurs numériques sont nécessaires, j'utilise simplement quelque chose comme ceci:

void msgInt(char const * const message, int const &value);
...
void msgInt(char const * const message, int const &value)
{
  m_screen.lock();
  cout << message << " = " << value << endl;
  m_screen.unlock();
}

C'est facile et cela me convient, mais je ne sais pas vraiment si c'est techniquement correct. Donc, je serais heureux d'entendre vos opinions.


Eh bien, je n'ai pas lu ceci:

Je ne veux pas rechercher chaque occurrence de std :: cout et ajouter une ligne de code de verrouillage.

Je suis désolé. Cependant, j'espère que cela aidera quelqu'un.

3
Germinx

Une solution réalisable utilise un tampon de ligne pour chaque thread. Vous pouvez obtenir des lignes entrelacées, mais pas des caractères entrelacés. Si vous associez cela au stockage local des threads, vous éviterez également les problèmes de conflit de verrous. Ensuite, lorsqu'une ligne est pleine (ou à la chasse, si vous le souhaitez), vous l'écrivez sur stdout. Cette dernière opération doit bien sûr utiliser un verrou. Vous insérez tout cela dans un extincteur, que vous mettez entre std :: cout et son amortisseur d'origine.

Le problème que cela ne résout pas concerne par exemple les indicateurs de format (par exemple hex/dec/oct pour les nombres), qui peuvent parfois percoler entre les threads, car ils sont attachés au flux. Ce n'est pas grave, à supposer que vous vous connectiez uniquement et que vous ne l'utilisiez pas pour des données importantes. Cela aide simplement à ne pas formater les choses spécialement. Si vous avez besoin d'une sortie hexadécimale pour certains nombres, essayez ceci:

template<typename integer_type>
std::string hex(integer_type v)
{
    /* Notes:
    1. using showbase would still not show the 0x for a zero
    2. using (v + 0) converts an unsigned char to a type
       that is recognized as integer instead of as character */
    std::stringstream s;
    s << "0x" << std::setfill('0') << std::hex
        << std::setw(2 * sizeof v) << (v + 0);
    return s.str();
}

Des approches similaires fonctionnent également pour d'autres formats.

3
Ulrich Eckhardt

Dans le sens de la réponse proposée par Conchylicultor, mais sans hériter de std::ostringstream:


EDIT: Type de retour fixe pour l'opérateur surchargé et surcharge ajoutée pour std::endl.


EDIT 1: J'ai étendu cela à une bibliothèque simple en-tête uniquement pour la journalisation/débogage de programmes multi-threadés.


#include <iostream>
#include <mutex>
#include <thread>
#include <vector>    
#include <chrono>

static std::mutex mtx_cout;

// Asynchronous output
struct acout
{
        std::unique_lock<std::mutex> lk;
        acout()
            :
              lk(std::unique_lock<std::mutex>(mtx_cout))
        {

        }

        template<typename T>
        acout& operator<<(const T& _t)
        {
            std::cout << _t;
            return *this;
        }

        acout& operator<<(std::ostream& (*fp)(std::ostream&))
        {
            std::cout << fp;
            return *this;
        }
};

int main(void)
{


    std::vector<std::thread> workers_cout;
    std::vector<std::thread> workers_acout;

    size_t worker(0);
    size_t threads(5);


    std::cout << "With std::cout:" << std::endl;

    for (size_t i = 0; i < threads; ++i)
    {
        workers_cout.emplace_back([&]
        {
            std::cout << "\tThis is worker " << ++worker << " in thread "
                      << std::this_thread::get_id() << std::endl;
        });
    }
    for (auto& w : workers_cout)
    {
        w.join();
    }

    worker = 0;

    std::this_thread::sleep_for(std::chrono::seconds(2));

    std::cout << "\nWith acout():" << std::endl;

    for (size_t i = 0; i < threads; ++i)
    {
        workers_acout.emplace_back([&]
        {
            acout() << "\tThis is worker " << ++worker << " in thread "
                    << std::this_thread::get_id() << std::endl;
        });
    }
    for (auto& w : workers_acout)
    {
        w.join();
    }

    return 0;
}

Sortie:

With std::cout:
        This is worker 1 in thread 139911511856896
        This is worker  This is worker 3 in thread 139911495071488
        This is worker 4 in thread 139911486678784
2 in thread     This is worker 5 in thread 139911503464192139911478286080


With acout():
        This is worker 1 in thread 139911478286080
        This is worker 2 in thread 139911486678784
        This is worker 3 in thread 139911495071488
        This is worker 4 in thread 139911503464192
        This is worker 5 in thread 139911511856896
1
cantordust

Je sais que c'est une vieille question, mais cela m'a beaucoup aidé avec mon problème. J'ai créé une classe utilitaire basée sur ce post réponses et j'aimerais partager mon résultat.

Considérant que nous utilisons les versions C++ 11 ou C++, cette classe fournit les fonctions print et println pour composer des chaînes avant d'appeler le flux de sortie standard et éviter les problèmes de simultanéité. Ce sont des fonctions variées qui utilisent des modèles pour imprimer différents types de données.

Vous pouvez vérifier son utilisation dans un problème producteur-consommateur sur mon github: https://github.com/eloiluiz/threadsBar

Alors, voici mon code:

class Console {
private:
    Console() = default;

    inline static void innerPrint(std::ostream &stream) {}

    template<typename Head, typename... Tail>
    inline static void innerPrint(std::ostream &stream, Head const head, Tail const ...tail) {
        stream << head;
        innerPrint(stream, tail...);
    }

public:
    template<typename Head, typename... Tail>
    inline static void print(Head const head, Tail const ...tail) {
        // Create a stream buffer
        std::stringbuf buffer;
        std::ostream stream(&buffer);
        // Feed input parameters to the stream object
        innerPrint(stream, head, tail...);
        // Print into console and flush
        std::cout << buffer.str();
    }

    template<typename Head, typename... Tail>
    inline static void println(Head const head, Tail const ...tail) {
        print(head, tail..., "\n");
    }
};
1
eloiluiz

Outre la synchronisation, cette solution fournit des informations sur le thread à partir duquel le journal a été écrit.

DISCLAIMER: C’est une façon assez naïve de synchroniser les journaux, bien que cela puisse s’appliquer à certains petits cas d’utilisation du débogage.

thread_local int thread_id = -1;
std::atomic<int> thread_count;

struct CurrentThread {

  static void init() {
    if (thread_id == -1) {
      thread_id = thread_count++;
    }
  }

  friend std::ostream &operator<<(std::ostream &os, const CurrentThread &t) {
    os << "[Thread-" << thread_id << "] - ";
    return os;
  }
};

CurrentThread current_thread;
std::mutex io_lock;
#ifdef DEBUG
#define LOG(x) {CurrentThread::init(); std::unique_lock<std::mutex> lk(io_lock); cout << current_thread; x << endl;}
#else
#define LOG(x)
#endif

Cela peut être utilisé comme ça. 

LOG(cout << "Waiting for some event");

Et cela donnera une sortie de journal 

[Thread-1] - Entering critical section 
[Thread-2] - Waiting on mutex
[Thread-1] - Leaving critical section, unlocking the mutex
0
samvel1024