План занятия:
· Основные принципы взаимодействия потоков
· Основные проблемы взаимодействия потоков. Проблема соревнований
· Критические секции
· Блокирование
Основные принципы взаимодействия потоков
Потоки, которые выполняются в рамках процесса параллельно, могут быть независимыми или взаимодействовать между собой.
Поток является независимым, если он не влияет на выполнение других потоков процесса, не подвергается воздействию с их стороны, и не имеет с ними никаких общих данных. Его исполнение однозначно зависит от входных данных и он называется детерминированным.
Все остальные потоки взаимодействуют друг с другом. Эти потоки имеют данные, общие с другими потоками (они находятся в адресном пространстве их процесса). Их выполнение зависит не только от входных данных, но и от выполнения других потоков, то есть они являются недетерминированными. Результаты выполнения независимого потока всегда можно повторить, чего нельзя сказать о потоках, взаимодействующих.
|
|
Данные, которые являются общими для нескольких потоков, называют совместно используемыми данными (shared data). Это - важнейшая концепция программирования. Всякий поток может в любой момент времени изменить такие данные. Механизмы обеспечения корректного доступа к совместно используемым данным называют механизмами синхронизации потоков.
Работать с независимыми потоками проще, чем с теми, которые взаимодействуют. Программист может не учитывать того, что одновременно с таким потоком выполняются другие, а также не обращать внимания на состояние совместно используемых данных, с которыми работает поток.
Однако обойтись без реализации взаимодействия потоков невозможно по нескольким причинам:
- Необходимо организовывать обмен информацией во время работы с потоками. Например, пользователи базы данных или веб-сервера могут захотеть одновременно выполнить запросы на получение одной и той же информации, и система должна обеспечить ее параллельное получения потоками, которые обслуживают этих пользователей.
- Корректная реализация такого взаимодействия и использование соответствующих алгоритмов могут значительно ускорить вычислительный процесс на многопроцессорных системах. При этом задачи разделяют на подзадачи, которые выполняются параллельно на разных процессорах, а затем их результаты собирают вместе для получения окончательного решения. Такую технологию называют технологией параллельных вычислений.
- В задачах, требующих параллельного выполнения вычислений и операций ввода-вывода, потоки, выполняющие ввод-вывод, должны иметь возможность подавать сигналы другим потокам с завершением своих операций.
- Подобная организация позволяет разбивать задачи на отдельные исполняемые модули, оформленые как отдельные потоки, при этом выход одного модуля может быть входом для другого, а также повышается гибкость системы, поскольку отдельные модули можно менять, не трогая других.
Необходимость организации параллельного выполнения взаимодействующих потоков, требует наличия механизмов обмена данными между ними и обеспечение их синхронизации.
|
|
Основные проблемы взаимодействия потоков. Проблема соревнований
В связи с тем, что все потоки в системе выполняются последовательно, это приводит к следующему: в одной ситуации код может работать, в другой - нет, и предсказать появление ошибки в общем случае невозможно. Такую ситуацию называют состоянием гонок или соревнованием (Rасе condition), что является одной из наиболее трудно улавливаемых ошибок, с которыми сталкиваются программисты. Она практически не поддается традиционному налаживанию (поскольку невозможно взять в отладчик все возможные комбинации последовательностей выполнения потоков, особенно если их много).
Попытки решать подобные проблемы вызвали необходимость синхронизации потоков. Сразу же отметим, что проблемы синхронизации и организации параллельных вычислений являются одними из самых сложных в практическом программировании. Поэтому разработку и особенно налаживания многопоточных программ часто воспринимают как своеобразное «искусство», что доступно далеко не всем программистам.
На самом деле такая разработка и отладка - это отнюдь не искусство, а строгая дисциплина, подлежит одному главному принципу: поскольку для многопоточных программ традиционное налаживания не пригодно, программист должен писать код таким образом, чтобы уже на этапе разработки не оставить места для ошибок синхронизации. В этом разделе ознакомимся с правилами, которые необходимо соблюдать, чтобы созданный код соответствовал этому принципу.
Рассмотрим основные подходы к решению проблемы соревнований:
- Иногда (но довольно редко) можно просто игнорировать такие ошибки. Это может иметь смысл, когда нас интересует не точная регистрация тех или иных данных, а сбор статистики о них, поэтому некоторые ошибки не сказываться на общем результате. Например, глобальным счетчиком является величина, на базе которой рассчитывают среднее количество запросов к системе в сутки и можно проигнорировать ошибки регистрации таких запросов, которые случаются раз в несколько часов. К сожалению, в большинстве случаев такой подход не приемлем.
- Иногда использование глобальных данных не диктуется спецификой задачи. В этом случае используется однозначное решение потоков. Это и является основной задачей синхронизации.
Критические секции
Рассмотрим использование простейшей идеи для решения проблемы соревнований. Нетрудно заметить, как источником нашей ошибки является то, что внешне простая операция возложение денег на счет в действительности распадается на несколько операций, при этом всегда остается шанс вмешательства между ними какого-то другого потока. В этом случае говорят, что исходная операция не является атомарной.
Для решения проблемы соревнования используется превращение фрагмента кода, который вызывает проблему, в атомарную операцию, то есть в такую, которая гарантированно будет выполняться полностью без вмешательства других потоков. Такой фрагмент кода называют критической секцией (critical section)
Рассмотрим свойства, которыми должна обладать критическая секция:
- взаимного исключения (mutual exclusion): в конкретный момент времени код критической секции может выполнять только один поток.
- прогресса: если несколько потоков одновременно пригласили на вход в критическую секцию, один из них должен обязательно в нее войти (они не могут все заблокировать друг друга).
- ограниченности ожидания - процесс, пытается войти в критическую секцию, рано или поздно обязательно в нее войдет.
Остается ответить на далеко не простой вопрос: «Как нам заставить систему воспринимать несколько операций как одну единую операцию?»
|
|
Самым простым решением такой задачи было бы запретить прерывание на время исполнения кода критической секции. Такой подход, хотя и решает задачи в принципе, на практике не может быть применяемый, поскольку вследствие зацикливания или аварии программы в критической секции вся система может остаться с заблокированными прерываниями, а следовательно, в неработоспособном состоянии.
Блокирование
Рациональным решением является использование блокировок (locks). Блокировка - это механизм, который не позволяет более чем одному потоку выполнять код критической секции. Использование блокировки сводится к двум действиям: введение и снятие блокировки. В случае блокирования проверяют, не было ли оно уже сделано другим потоком, и если это так, этот поток переходит в состояние ожидания, иначе он вводит блокировки и входит в критическую секцию. После выхода из критической секции поток снимает блокировку.
Так реализуют свойство взаимного исключения, отсюда происходит другое название для блокировки - мьютекс (mutex, сокращение от mutual exclusion). Впрочем, чаще это название обозначает конкретный механизм ОС, реализующей блокировки.