Sobes.tech
Middle

Comment travaillez-vous avec la multitâche dans vos projets?

sobes.tech IA

Réponse de l'IA

Dans C++11 et versions supérieures, j’utilise <thread> pour créer et gérer des threads. Pour la synchronisation, j’emploie des primitives de <mutex>, <condition_variable> et <atomic>.

Approches principales:

  • std::thread: Création et lancement de nouveaux threads.

    #include <thread>
    #include <iostream>
    
    void worker_function() {
        std::cout << "Thread de travail démarré\n";
        // ... travail ...
        std::cout << "Thread de travail terminé\n";
    }
    
    int main() {
        std::thread worker(worker_function);
        // ... travail principal ...
        worker.join(); // Attendre la fin du thread
        return 0;
    }
    
  • std::mutex: Protection des données partagées contre l’accès concurrent.

    #include <mutex>
    #include <thread>
    #include <vector>
    
    std::mutex data_mutex;
    std::vector<int> shared_data;
    
    void add_to_data(int value) {
        std::lock_guard<std::mutex> lock(data_mutex); // verrou RAII
        shared_data.push_back(value);
    }
    
    // ... Threads appelant add_to_data ...
    
  • std::lock_guard et std::unique_lock: Enveloppes RAII pour mutex, assurant la libération automatique.

    • std::lock_guard: Verrou simple, ne permettant pas le transfert de propriété ni le verrouillage différé.
    • std::unique_lock: Plus flexible, supporte le verrouillage différé, le transfert de propriété, le verrouillage récursif (avec std::recursive_mutex).
  • std::condition_variable: Signaux entre threads, permettant d’attendre une condition.

    #include <condition_variable>
    #include <mutex>
    #include <thread>
    #include <queue>
    
    std::queue<int> data_queue;
    std::mutex queue_mutex;
    std::condition_variable data_available;
    bool stop_processing = false;
    
    void producer() {
        // ... produire des données ...
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            data_queue.push(/* données */);
        }
        data_available.notify_one(); // Notifier un consommateur en attente
    }
    
    void consumer() {
        while (!stop_processing) {
            std::unique_lock<std::mutex> lock(queue_mutex);
            data_available.wait(lock, []{ return !data_queue.empty() || stop_processing; });
    
            if (stop_processing && data_queue.empty()) {
                break;
            }
    
            int data = data_queue.front();
            data_queue.pop();
            lock.unlock(); // Déverrouiller avant de traiter
    
            // ... traiter les données ...
        }
    }
    
    // ... Threads exécutant producteur et consommateur ...
    
  • std::atomic: Pour opérations atomiques simples sans mutex.

    #include <atomic>
    #include <thread>
    
    std::atomic<int> counter(0);
    
    void increment_counter() {
        counter++; // incrément atomique
    }
    
    // ... Threads appelant increment_counter ...
    
  • Pools de threads: J’utilise souvent le pattern de pool de threads pour gérer les ressources et réduire la surcharge de création/suppression. Je les implémente avec std::vector<std::thread>, des queues de tâches et des primitives de synchronisation (std::mutex, std::condition_variable).

  • std::future et std::async: Pour exécuter des tâches asynchrones et récupérer les résultats.

    #include <future>
    #include <iostream>
    
    int calculate_result(int input) {
        // ... calcul complexe ...
        return input * 2;
    }
    
    int main() {
        std::future<int> future_result = std::async(std::launch::async, calculate_result, 10);
        // ... faire d’autres travaux ...
        int result = future_result.get(); // Attendre le résultat
        std::cout << "Résultat: " << result << std::endl;
        return 0;
    }
    

En travaillant avec la concurrence, je prête une attention particulière à:

  • Conditions de course (Race conditions): Identification et prévention des situations où le résultat dépend de l’ordre d’exécution non déterminé. Utilisation de mutex et d’opérations atomiques.
  • Interblocage (Deadlock): Analyse des dépendances entre verrouillages et application de stratégies pour les éviter (par exemple, ordre strict d’acquisition des mutex).
  • États de vivacité (Livelock, Starvation): Assurer une distribution "équitable" du temps CPU et de l’accès aux ressources.
  • Charge de travail sur les threads: Répartition uniforme du travail.
  • Débogage: Les programmes multithread sont plus difficiles à déboguer à cause de l’imprévisibilité de l’exécution. J’utilise des outils spécialisés et des logs.

Pour un contrôle de bas niveau ou des tâches spécifiques, je peux également utiliser POSIX Threads (pthread) sur les systèmes Unix ou l’API Windows pour le multithreading. En général, je m’efforce de suivre les outils standard de C++ pour leur portabilité.