Sobes.tech
Middle

How do you work with multithreading in your projects?

sobes.tech AI

Answer from AI

In C++11 and above, I use <thread> for creating and managing threads. For synchronization, I apply primitives from <mutex>, <condition_variable>, and <atomic>.

Main approaches:

  • std::thread: Creating and starting new threads.

    #include <thread>
    #include <iostream>
    
    void worker_function() {
        std::cout << "Worker thread started\n";
        // ... some work ...
        std::cout << "Worker thread finished\n";
    }
    
    int main() {
        std::thread worker(worker_function);
        // ... main thread work ...
        worker.join(); // Wait for the worker thread to finish
        return 0;
    }
    
  • std::mutex: Protecting shared data from concurrent access.

    #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); // RAII lock
        shared_data.push_back(value);
    }
    
    // ... Threads calling add_to_data ...
    
  • std::lock_guard and std::unique_lock: RAII wrappers for mutexes, ensuring automatic release of locks.

    • std::lock_guard: Simple lock guard, not allowing transfer of ownership or deferred locking.
    • std::unique_lock: More flexible, supporting deferred locking, transfer of ownership, recursive locking (when used with std::recursive_mutex).
  • std::condition_variable: Signaling between threads, allowing threads to wait for a certain 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() {
        // ... produce data ...
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            data_queue.push(/* data */);
        }
        data_available.notify_one(); // Notify a waiting consumer
    }
    
    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(); // Unlock before processing data
    
            // ... process data ...
        }
    }
    
    // ... Threads running producer and consumer ...
    
  • std::atomic: For simple atomic operations without using mutexes.

    #include <atomic>
    #include <thread>
    
    std::atomic<int> counter(0);
    
    void increment_counter() {
        counter++; // Atomic increment
    }
    
    // ... Threads calling increment_counter ...
    
  • Thread pools: I often use the thread pool pattern to manage thread resources and reduce overhead of creation/deletion. Implemented with std::vector<std::thread>, task queues, and synchronization primitives (std::mutex, std::condition_variable).

  • std::future and std::async: For executing asynchronous tasks and obtaining results.

    #include <future>
    #include <iostream>
    
    int calculate_result(int input) {
        // ... complex calculation ...
        return input * 2;
    }
    
    int main() {
        std::future<int> future_result = std::async(std::launch::async, calculate_result, 10);
        // ... do other work ...
        int result = future_result.get(); // Wait for the result
        std::cout << "Result: " << result << std::endl;
        return 0;
    }
    

When working with multithreading, I pay special attention to the following issues:

  • Race conditions: Detecting and preventing situations where the outcome depends on unpredictable operation order. Using mutexes, atomic operations.
  • Deadlock: Analyzing lock dependencies and applying strategies to avoid them (e.g., strict lock acquisition order).
  • Livelock, starvation: Ensuring "fair" distribution of CPU time and resource access.
  • Thread load: Evenly distributing work among threads.
  • Debugging: Multithreaded programs are harder to debug due to unpredictability. I use specialized debugging tools and logging.

For lower-level control or specific tasks, I can use POSIX Threads (pthread) on Unix-like systems or Windows API for multithreading. Overall, I prefer sticking to standard C++ tools for portability.