Skip to content

Практическая работа №18. Многопоточность в C++


  • Изучить основные понятия многопоточности
  • Познакомиться с созданием и запуском потоков в C++
  • Научиться использовать std::thread для запуска параллельных задач
  • Изучить проблемы совместного доступа к данным
  • Научиться применять std::mutex и std::lock_guard для синхронизации

1. Теоретические сведения

Section titled “1. Теоретические сведения”

1.1 Что такое многопоточность

Section titled “1.1 Что такое многопоточность”

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

Поток — это отдельная линия выполнения программы.

В одном процессе может работать несколько потоков. Они используют общую память, поэтому могут обращаться к одним и тем же данным.

Зачем нужна многопоточность

Section titled “Зачем нужна многопоточность”
  • чтобы не блокировать интерфейс программы;
  • чтобы выполнять несколько задач одновременно;
  • чтобы ускорять обработку данных;
  • чтобы разделять работу между потоками.

Процесс — это запущенная программа.

Поток — это часть процесса, которая выполняет код.

В простом случае программа работает в одном потоке — в основном потоке.

Если задача занимает много времени, например:

  • обработка большого массива данных;
  • чтение файла;
  • вычисления в цикле;

то лучше вынести её в отдельный поток.


1.3 Основные проблемы многопоточности

Section titled “1.3 Основные проблемы многопоточности”

При работе нескольких потоков могут возникать ошибки:

Race condition (гонка данных) — ситуация, когда два или более потока одновременно изменяют одни и те же данные.

Deadlock (взаимная блокировка) — ситуация, когда потоки ждут друг друга и не могут продолжить работу.

Data race — одновременный доступ к общей памяти, при котором хотя бы один поток изменяет данные без синхронизации.

Чтобы избежать ошибок, используют средства синхронизации.


Для работы с потоками в C++ используется библиотека <thread>.

Основной класс:

  • std::thread — создание и запуск потока

Для синхронизации используются:

  • std::mutex — взаимное исключение;
  • std::lock_guard — автоматическая блокировка и разблокировка mutex;
  • std::atomic — атомарные операции над переменными.

1.5 Пример создания потока

Section titled “1.5 Пример создания потока”
#include <iostream>
#include <thread>
void task() {
std::cout << "Hello from thread\n";
}
int main() {
std::thread t(task);
t.join();
return 0;
}

В этом примере функция task() выполняется в отдельном потоке.


1.6 Синхронизация с помощью mutex

Section titled “1.6 Синхронизация с помощью mutex”

Если несколько потоков обращаются к одной переменной, её нужно защищать.

Пример:

#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex m;
void increment() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> lock(m);
counter++;
}
}

Здесь lock_guard автоматически захватывает и освобождает mutex.


Поток — отдельная линия выполнения программы.

Многопоточность — одновременная работа нескольких потоков.

Синхронизация — согласование работы потоков.

Mutex — механизм, который разрешает доступ к ресурсу только одному потоку.

Race condition — ошибка из-за одновременного доступа к данным.

Join — ожидание завершения потока.

Detach — запуск потока без последующего ожидания.


3. Типы задач в многопоточности

Section titled “3. Типы задач в многопоточности”

Примеры:

  • подсчёт суммы;
  • обработка массива;
  • поиск максимального элемента;
  • генерация данных.

Примеры:

  • чтение файлов;
  • сетевые запросы;
  • работа с базами данных.

Если программа имеет графический интерфейс, тяжёлые вычисления нельзя выполнять в главном потоке, иначе интерфейс будет зависать.


4. Представление общих данных в памяти

Section titled “4. Представление общих данных в памяти”

Если несколько потоков используют одну и ту же переменную, возможна ошибка.

int counter = 0;
void work() {
for (int i = 0; i < 100000; i++) {
counter++;
}
}

Если запустить несколько потоков с этой функцией, результат может быть неверным.

#include <mutex>
int counter = 0;
std::mutex m;
void work() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> lock(m);
counter++;
}
}

Основные действия с потоком:

ДействиеНазначение
std::threadзапуск потока
join()ожидание завершения
detach()отделение потока
std::mutexзащита данных
std::lock_guardавтоматическая блокировка

6. Практическое задание

Section titled “6. Практическое задание”

Необходимо разработать консольную программу на C++, которая демонстрирует работу нескольких потоков и защиту общих данных.

Программа должна выполнять следующие действия:

  1. Запустить несколько потоков.
  2. Каждый поток должен выполнять одинаковую задачу.
  3. Потоки должны работать с общей переменной.
  4. Необходимо показать результат без синхронизации и с синхронизацией.
  5. Вывести итоговое значение переменной.

Используйте следующий сценарий:

  • количество потоков: 4
  • каждый поток увеличивает счётчик на 100000
  • общая переменная: counter

Реализуйте программу, в которой несколько потоков увеличивают одну общую переменную без mutex.

  • использовать std::thread
  • использовать общую переменную counter
  • запустить 4 потока
  • вывести итоговое значение

Значение может отличаться от ожидаемого из-за ошибки синхронизации.

Пример:

Итоговое значение counter: 287451

Ожидаемое значение при корректной работе:

400000

Реализуйте ту же программу, но с использованием mutex.

  • использовать std::mutex
  • использовать std::lock_guard
  • обеспечить корректное увеличение счётчика
  • вывести итоговое значение
Итоговое значение counter: 400000

10. Дополнительные задания

Section titled “10. Дополнительные задания”

Реализуйте программу, в которой каждый поток вычисляет сумму чисел от 1 до 100000.

  • создать 4 потока;
  • каждый поток должен вычислять сумму;
  • вывести номер потока и результат;
  • дождаться завершения всех потоков.

Реализуйте программу, в которой несколько потоков записывают сообщения в консоль.

  • создать 3 потока;
  • каждый поток должен вывести сообщение 10 раз;
  • проверить, что без mutex сообщения могут перемешиваться;
  • добавить std::mutex для корректного вывода.

Пример вывода:

Поток 1 выполняется
Поток 2 выполняется
Поток 3 выполняется

Реализуйте программу, в которой каждый поток обрабатывает часть массива.

  • создать массив из 100 элементов;
  • разделить массив на части между потоками;
  • каждый поток должен вычислить сумму своей части массива;
  • после завершения потоков вывести общую сумму.

Реализуйте программу, в которой один поток увеличивает значение переменной, а второй поток уменьшает её.

  • использовать общую переменную;
  • запустить два потока;
  • первый поток должен увеличивать значение 100000 раз;
  • второй поток должен уменьшать значение 100000 раз;
  • проверить результат без mutex и с mutex.

Ожидаемый результат:

0

11. Рекомендации по выполнению

Section titled “11. Рекомендации по выполнению”

При выполнении работы рекомендуется:

  • сначала реализовать программу без синхронизации;
  • проверить, что итоговое значение счётчика отличается от ожидаемого;
  • затем добавить std::mutex и std::lock_guard;
  • повторно запустить программу и сравнить результаты;
  • использовать несколько потоков и изменять количество итераций для проверки работы программы.

Работа рассчитана на одну пару (90 минут), поэтому рекомендуется выполнять задания по порядку:

  1. Создать программу с несколькими потоками.
  2. Реализовать общий счётчик без синхронизации.
  3. Добавить mutex.
  4. Проверить корректность результата.
  5. Выполнить дополнительное задание, если осталось время.

12. Тестовые данные для проверки

Section titled “12. Тестовые данные для проверки”

Параметры:

  • потоков: 2
  • увеличений в каждом потоке: 1000

Ожидаемый результат:

2000

Параметры:

  • потоков: 4
  • увеличений в каждом потоке: 100000
  • без mutex

Ожидаемый результат:

Значение может быть меньше 400000.


Параметры:

  • потоков: 4
  • увеличений в каждом потоке: 100000
  • с mutex

Ожидаемый результат:

400000

Параметры:

  • потоков: 3
  • каждый поток считает сумму чисел от 1 до 100000

Ожидаемый результат:

Каждый поток должен вывести одинаковый результат.


13. Требования к программе

Section titled “13. Требования к программе”

Программа должна:

  • корректно создавать потоки;
  • использовать join();
  • демонстрировать работу без синхронизации и с синхронизацией;
  • использовать std::mutex и std::lock_guard;
  • корректно компилироваться;
  • выводить понятный результат.

  1. Что такое поток?
  2. Чем поток отличается от процесса?
  3. Для чего используется std::thread?
  4. Что делает метод join()?
  5. Что такое race condition?
  6. Зачем нужен std::mutex?
  7. Чем std::lock_guard удобнее ручного вызова lock() и unlock()?
  8. Почему несколько потоков могут изменять результат некорректно без синхронизации?
  9. Какие задачи удобно выполнять в отдельных потоках?
  10. Почему нельзя допускать одновременный доступ к общим данным без защиты?