Практическая работа №18. Многопоточность в C++
Цель работы
Section titled “Цель работы”- Изучить основные понятия многопоточности
- Познакомиться с созданием и запуском потоков в C++
- Научиться использовать
std::threadдля запуска параллельных задач - Изучить проблемы совместного доступа к данным
- Научиться применять
std::mutexиstd::lock_guardдля синхронизации
1. Теоретические сведения
Section titled “1. Теоретические сведения”1.1 Что такое многопоточность
Section titled “1.1 Что такое многопоточность”Многопоточность — это способ организации программы, при котором несколько потоков выполняются одновременно или псевдопараллельно.
Поток — это отдельная линия выполнения программы.
В одном процессе может работать несколько потоков. Они используют общую память, поэтому могут обращаться к одним и тем же данным.
Зачем нужна многопоточность
Section titled “Зачем нужна многопоточность”- чтобы не блокировать интерфейс программы;
- чтобы выполнять несколько задач одновременно;
- чтобы ускорять обработку данных;
- чтобы разделять работу между потоками.
1.2 Процесс и поток
Section titled “1.2 Процесс и поток”Процесс — это запущенная программа.
Поток — это часть процесса, которая выполняет код.
В простом случае программа работает в одном потоке — в основном потоке.
Если задача занимает много времени, например:
- обработка большого массива данных;
- чтение файла;
- вычисления в цикле;
то лучше вынести её в отдельный поток.
1.3 Основные проблемы многопоточности
Section titled “1.3 Основные проблемы многопоточности”При работе нескольких потоков могут возникать ошибки:
Race condition (гонка данных) — ситуация, когда два или более потока одновременно изменяют одни и те же данные.
Deadlock (взаимная блокировка) — ситуация, когда потоки ждут друг друга и не могут продолжить работу.
Data race — одновременный доступ к общей памяти, при котором хотя бы один поток изменяет данные без синхронизации.
Чтобы избежать ошибок, используют средства синхронизации.
1.4 Потоки в C++
Section titled “1.4 Потоки в C++”Для работы с потоками в 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.
2. Основные термины
Section titled “2. Основные термины”Поток — отдельная линия выполнения программы.
Многопоточность — одновременная работа нескольких потоков.
Синхронизация — согласование работы потоков.
Mutex — механизм, который разрешает доступ к ресурсу только одному потоку.
Race condition — ошибка из-за одновременного доступа к данным.
Join — ожидание завершения потока.
Detach — запуск потока без последующего ожидания.
3. Типы задач в многопоточности
Section titled “3. Типы задач в многопоточности”Вычислительные задачи
Section titled “Вычислительные задачи”Примеры:
- подсчёт суммы;
- обработка массива;
- поиск максимального элемента;
- генерация данных.
Задачи ввода-вывода
Section titled “Задачи ввода-вывода”Примеры:
- чтение файлов;
- сетевые запросы;
- работа с базами данных.
Интерфейсные задачи
Section titled “Интерфейсные задачи”Если программа имеет графический интерфейс, тяжёлые вычисления нельзя выполнять в главном потоке, иначе интерфейс будет зависать.
4. Представление общих данных в памяти
Section titled “4. Представление общих данных в памяти”Если несколько потоков используют одну и ту же переменную, возможна ошибка.
Пример без защиты
Section titled “Пример без защиты”int counter = 0;
void work() { for (int i = 0; i < 100000; i++) { counter++; }}Если запустить несколько потоков с этой функцией, результат может быть неверным.
Пример с защитой
Section titled “Пример с защитой”#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++; }}5. Работа потоков
Section titled “5. Работа потоков”Основные действия с потоком:
| Действие | Назначение |
|---|---|
std::thread | запуск потока |
join() | ожидание завершения |
detach() | отделение потока |
std::mutex | защита данных |
std::lock_guard | автоматическая блокировка |
6. Практическое задание
Section titled “6. Практическое задание”Необходимо разработать консольную программу на C++, которая демонстрирует работу нескольких потоков и защиту общих данных.
Программа должна выполнять следующие действия:
- Запустить несколько потоков.
- Каждый поток должен выполнять одинаковую задачу.
- Потоки должны работать с общей переменной.
- Необходимо показать результат без синхронизации и с синхронизацией.
- Вывести итоговое значение переменной.
7. Исходные данные
Section titled “7. Исходные данные”Используйте следующий сценарий:
- количество потоков: 4
- каждый поток увеличивает счётчик на 100000
- общая переменная:
counter
8. Задание №1
Section titled “8. Задание №1”Реализуйте программу, в которой несколько потоков увеличивают одну общую переменную без mutex.
Требования
Section titled “Требования”- использовать
std::thread - использовать общую переменную
counter - запустить 4 потока
- вывести итоговое значение
Ожидаемый результат
Section titled “Ожидаемый результат”Значение может отличаться от ожидаемого из-за ошибки синхронизации.
Пример:
Итоговое значение counter: 287451Ожидаемое значение при корректной работе:
4000009. Задание №2
Section titled “9. Задание №2”Реализуйте ту же программу, но с использованием mutex.
Требования
Section titled “Требования”- использовать
std::mutex - использовать
std::lock_guard - обеспечить корректное увеличение счётчика
- вывести итоговое значение
Ожидаемый результат
Section titled “Ожидаемый результат”Итоговое значение counter: 40000010. Дополнительные задания
Section titled “10. Дополнительные задания”Задание №3
Section titled “Задание №3”Реализуйте программу, в которой каждый поток вычисляет сумму чисел от 1 до 100000.
Требования
Section titled “Требования”- создать 4 потока;
- каждый поток должен вычислять сумму;
- вывести номер потока и результат;
- дождаться завершения всех потоков.
Задание №4
Section titled “Задание №4”Реализуйте программу, в которой несколько потоков записывают сообщения в консоль.
Требования
Section titled “Требования”- создать 3 потока;
- каждый поток должен вывести сообщение 10 раз;
- проверить, что без mutex сообщения могут перемешиваться;
- добавить
std::mutexдля корректного вывода.
Пример вывода:
Поток 1 выполняетсяПоток 2 выполняетсяПоток 3 выполняетсяЗадание №5
Section titled “Задание №5”Реализуйте программу, в которой каждый поток обрабатывает часть массива.
Требования
Section titled “Требования”- создать массив из 100 элементов;
- разделить массив на части между потоками;
- каждый поток должен вычислить сумму своей части массива;
- после завершения потоков вывести общую сумму.
Задание №6
Section titled “Задание №6”Реализуйте программу, в которой один поток увеличивает значение переменной, а второй поток уменьшает её.
Требования
Section titled “Требования”- использовать общую переменную;
- запустить два потока;
- первый поток должен увеличивать значение 100000 раз;
- второй поток должен уменьшать значение 100000 раз;
- проверить результат без mutex и с mutex.
Ожидаемый результат:
011. Рекомендации по выполнению
Section titled “11. Рекомендации по выполнению”При выполнении работы рекомендуется:
- сначала реализовать программу без синхронизации;
- проверить, что итоговое значение счётчика отличается от ожидаемого;
- затем добавить
std::mutexиstd::lock_guard; - повторно запустить программу и сравнить результаты;
- использовать несколько потоков и изменять количество итераций для проверки работы программы.
Работа рассчитана на одну пару (90 минут), поэтому рекомендуется выполнять задания по порядку:
- Создать программу с несколькими потоками.
- Реализовать общий счётчик без синхронизации.
- Добавить mutex.
- Проверить корректность результата.
- Выполнить дополнительное задание, если осталось время.
12. Тестовые данные для проверки
Section titled “12. Тестовые данные для проверки”Тест 1
Section titled “Тест 1”Параметры:
- потоков: 2
- увеличений в каждом потоке: 1000
Ожидаемый результат:
2000Тест 2
Section titled “Тест 2”Параметры:
- потоков: 4
- увеличений в каждом потоке: 100000
- без mutex
Ожидаемый результат:
Значение может быть меньше 400000.
Тест 3
Section titled “Тест 3”Параметры:
- потоков: 4
- увеличений в каждом потоке: 100000
- с mutex
Ожидаемый результат:
400000Тест 4
Section titled “Тест 4”Параметры:
- потоков: 3
- каждый поток считает сумму чисел от 1 до 100000
Ожидаемый результат:
Каждый поток должен вывести одинаковый результат.
13. Требования к программе
Section titled “13. Требования к программе”Программа должна:
- корректно создавать потоки;
- использовать
join(); - демонстрировать работу без синхронизации и с синхронизацией;
- использовать
std::mutexиstd::lock_guard; - корректно компилироваться;
- выводить понятный результат.
14. Контрольные вопросы
Section titled “14. Контрольные вопросы”- Что такое поток?
- Чем поток отличается от процесса?
- Для чего используется
std::thread? - Что делает метод
join()? - Что такое race condition?
- Зачем нужен
std::mutex? - Чем
std::lock_guardудобнее ручного вызоваlock()иunlock()? - Почему несколько потоков могут изменять результат некорректно без синхронизации?
- Какие задачи удобно выполнять в отдельных потоках?
- Почему нельзя допускать одновременный доступ к общим данным без защиты?