Глава 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++ предоставляет мощные инструменты для параллельного программирования:
Ключевые моменты:
- Создание и управление потоками
- Синхронизация через мьютексы
- Использование условных переменных
- Атомарные операции для безопасной работы с данными
Важные правила:
- Избегайте гонок данных
- Минимизируйте критические секции
- Используйте высокоуровневые средства синхронизации
- Тщательно продумывайте архитектуру многопоточного приложения
Многопоточность требует глубокого понимания и аккуратного подхода. Практика и внимательность - ключ к созданию надежных многопоточных приложений.