Многопоточность

Глава 8. Многопоточность #

1. Основы многопоточного программирования #

Многопоточность позволяет выполнять несколько потоков выполнения одновременно, повышая эффективность использования многоядерных процессоров.

1.1 Преимущества многопоточности #

  • Параллельное выполнение задач
  • Эффективное использование ресурсов процессора
  • Улучшение отзывчивости приложений
  • Возможность разделения сложных задач на подзадачи

1.2 Потенциальные проблемы многопоточности #

  • Гонки данных (race conditions)
  • Взаимные блокировки (deadlocks)
  • Сложность синхронизации
  • Неопределенное поведение при неправильной синхронизации

2. Класс thread #

2.1 Создание и запуск потоков #

#include <thread>
#include <iostream>

// Функция для выполнения в потоке
void threadFunction() {
    std::cout << "Выполнение в отдельном потоке" << std::endl;
}

// Функция с параметрами
void threadFunctionWithArgs(int x, std::string msg) {
    std::cout << "Параметр: " << x 
              << ", Сообщение: " << msg << std::endl;
}

int main() {
    // Создание потоков
    std::thread t1(threadFunction);
    
    // Поток с параметрами
    std::thread t2(threadFunctionWithArgs, 42, "Привет");
    
    // Лямбда-функция в потоке
    std::thread t3([]() {
        std::cout << "Лямбда в потоке" << std::endl;
    });
    
    // Ожидание завершения потоков
    t1.join();
    t2.join();
    t3.join();
}

2.2 Основные методы класса thread #

std::thread t1(threadFunction);

// Проверка, можно ли присоединить поток
if (t1.joinable()) {
    t1.join();  // Ожидание завершения
}

// Отсоединение потока
std::thread t2(threadFunction);
t2.detach();  // Поток продолжит выполнение независимо

3. Синхронизация потоков #

3.1 Базовые проблемы синхронизации #

#include <thread>
#include <iostream>

// Пример гонки данных
int counter = 0;

void incrementCounter() {
    for (int i = 0; i < 1000000; ++i) {
        // Небезопасное параллельное увеличение
        counter++;
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);
    
    t1.join();
    t2.join();
    
    // Результат будет непредсказуемым из-за гонки данных
    std::cout << "Счетчик: " << counter << std::endl;
}

4. Мьютексы #

4.1 Базовое использование мьютексов #

#include <thread>
#include <mutex>
#include <iostream>

class SafeCounter {
private:
    mutable std::mutex mtx;
    int counter = 0;

public:
    // Потокобезопасное увеличение
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }

    // Потокобезопасное чтение
    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return counter;
    }
};

void incrementCounter(SafeCounter& counter) {
    for (int i = 0; i < 1000000; ++i) {
        counter.increment();
    }
}

int main() {
    SafeCounter counter;
    
    std::thread t1(incrementCounter, std::ref(counter));
    std::thread t2(incrementCounter, std::ref(counter));
    
    t1.join();
    t2.join();
    
    std::cout << "Счетчик: " << counter.get() << std::endl;
}

4.2 Виды блокировок #

std::mutex mtx;

// Простая блокировка
{
    std::lock_guard<std::mutex> lock(mtx);
    // Критическая секция
}

// Более гибкая блокировка
{
    std::unique_lock<std::mutex> lock(mtx);
    // Можно разблокировать досрочно
    lock.unlock();
    // Повторная блокировка
    lock.lock();
}

5. Условные переменные #

#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

class ThreadSafeQueue {
private:
    std::queue<int> queue;
    std::mutex mtx;
    std::condition_variable cv;

public:
    void push(int value) {
        std::unique_lock<std::mutex> lock(mtx);
        queue.push(value);
        cv.notify_one();  // Уведомление ожидающего потока
    }

    int pop() {
        std::unique_lock<std::mutex> lock(mtx);
        // Ожидание, пока очередь не станет непустой
        cv.wait(lock, [this]() { return !queue.empty(); });
        
        int value = queue.front();
        queue.pop();
        return value;
    }
};

void producer(ThreadSafeQueue& q) {
    for (int i = 0; i < 10; ++i) {
        q.push(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer(ThreadSafeQueue& q) {
    for (int i = 0; i < 10; ++i) {
        int value = q.pop();
        std::cout << "Получено: " << value << std::endl;
    }
}

6. Атомарные операции #

#include <atomic>
#include <thread>

std::atomic<int> atomicCounter(0);

void incrementAtomic() {
    for (int i = 0; i < 1000000; ++i) {
        // Атомарное увеличение
        atomicCounter++;
        
        // Другие атомарные операции
        atomicCounter.fetch_add(1);  // Атомарное добавление
        atomicCounter.compare_exchange_weak(
            expectedValue, newValue  // Атомарное сравнение и замена
        );
    }
}

Заключение #

Многопоточность в C++ предоставляет мощные инструменты для параллельного программирования:

Ключевые моменты:

  • Создание и управление потоками
  • Синхронизация через мьютексы
  • Использование условных переменных
  • Атомарные операции для безопасной работы с данными

Важные правила:

  • Избегайте гонок данных
  • Минимизируйте критические секции
  • Используйте высокоуровневые средства синхронизации
  • Тщательно продумывайте архитектуру многопоточного приложения

Многопоточность требует глубокого понимания и аккуратного подхода. Практика и внимательность - ключ к созданию надежных многопоточных приложений.