Занятие 11. Взаимодействие потоков

План занятия:

· Основные принципы взаимодействия потоков

· Основные проблемы взаимодействия потоков. Проблема соревнований

· Критические секции

· Блокирование

Основные принципы взаимодействия потоков

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

Поток является независимым, если он не влияет на выполнение других потоков процесса, не подвергается воздействию с их стороны, и не имеет с ними никаких общих данных. Его исполнение однозначно зависит от входных данных и он называется детерминированным.

Все остальные потоки взаимодействуют друг с другом. Эти потоки имеют данные, общие с другими потоками (они находятся в адресном пространстве их процесса). Их выполнение зависит не только от входных данных, но и от выполнения других потоков, то есть они являются недетерминированными. Результаты выполнения независимого потока всегда можно повторить, чего нельзя сказать о потоках, взаимодействующих.

Данные, которые являются общими для нескольких потоков, называют совместно используемыми данными (shared data). Это - важнейшая концепция программирования. Всякий поток может в любой момент времени изменить такие данные. Механизмы обеспечения корректного доступа к совместно используемым данным называют механизмами синхронизации потоков.

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

Однако обойтись без реализации взаимодействия потоков невозможно по нескольким причинам:

  • Необходимо организовывать обмен информацией во время работы с потоками. Например, пользователи базы данных или веб-сервера могут захотеть одновременно выполнить запросы на получение одной и той же информации, и система должна обеспечить ее параллельное получения потоками, которые обслуживают этих пользователей.
  • Корректная реализация такого взаимодействия и использование соответствующих алгоритмов могут значительно ускорить вычислительный процесс на многопроцессорных системах. При этом задачи разделяют на подзадачи, которые выполняются параллельно на разных процессорах, а затем их результаты собирают вместе для получения окончательного решения. Такую технологию называют технологией параллельных вычислений.
  • В задачах, требующих параллельного выполнения вычислений и операций ввода-вывода, потоки, выполняющие ввод-вывод, должны иметь возможность подавать сигналы другим потокам с завершением своих операций.
  • Подобная организация позволяет разбивать задачи на отдельные исполняемые модули, оформленые как отдельные потоки, при этом выход одного модуля может быть входом для другого, а также повышается гибкость системы, поскольку отдельные модули можно менять, не трогая других.

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

Основные проблемы взаимодействия потоков. Проблема соревнований

В связи с тем, что все потоки в системе выполняются последовательно, это приводит к следующему: в одной ситуации код может работать, в другой - нет, и предсказать появление ошибки в общем случае невозможно. Такую ситуацию называют состоянием гонок или соревнованием (Rасе condition), что является одной из наиболее трудно улавливаемых ошибок, с которыми сталкиваются программисты. Она практически не поддается традиционному налаживанию (поскольку невозможно взять в отладчик все возможные комбинации последовательностей выполнения потоков, особенно если их много).

Попытки решать подобные проблемы вызвали необходимость синхронизации потоков. Сразу же отметим, что проблемы синхронизации и организации параллельных вычислений являются одними из самых сложных в практическом программировании. Поэтому разработку и особенно налаживания многопоточных программ часто воспринимают как своеобразное «искусство», что доступно далеко не всем программистам.

На самом деле такая разработка и отладка - это отнюдь не искусство, а строгая дисциплина, подлежит одному главному принципу: поскольку для многопоточных программ традиционное налаживания не пригодно, программист должен писать код таким образом, чтобы уже на этапе разработки не оставить места для ошибок синхронизации. В этом разделе ознакомимся с правилами, которые необходимо соблюдать, чтобы созданный код соответствовал этому принципу.

Рассмотрим основные подходы к решению проблемы соревнований:

  • Иногда (но довольно редко) можно просто игнорировать такие ошибки. Это может иметь смысл, когда нас интересует не точная регистрация тех или иных данных, а сбор статистики о них, поэтому некоторые ошибки не сказываться на общем результате. Например, глобальным счетчиком является величина, на базе которой рассчитывают среднее количество запросов к системе в сутки и можно проигнорировать ошибки регистрации таких запросов, которые случаются раз в несколько часов. К сожалению, в большинстве случаев такой подход не приемлем.
  • Иногда использование глобальных данных не диктуется спецификой задачи. В этом случае используется однозначное решение потоков. Это и является основной задачей синхронизации.

Критические секции

Рассмотрим использование простейшей идеи для решения проблемы соревнований. Нетрудно заметить, как источником нашей ошибки является то, что внешне простая операция возложение денег на счет в действительности распадается на несколько операций, при этом всегда остается шанс вмешательства между ними какого-то другого потока. В этом случае говорят, что исходная операция не является атомарной.

Для решения проблемы соревнования используется превращение фрагмента кода, который вызывает проблему, в атомарную операцию, то есть в такую, которая гарантированно будет выполняться полностью без вмешательства других потоков. Такой фрагмент кода называют критической секцией (critical section)

Рассмотрим свойства, которыми должна обладать критическая секция:

  • взаимного исключения (mutual exclusion): в конкретный момент времени код критической секции может выполнять только один поток.
  • прогресса: если несколько потоков одновременно пригласили на вход в критическую секцию, один из них должен обязательно в нее войти (они не могут все заблокировать друг друга).
  • ограниченности ожидания - процесс, пытается войти в критическую секцию, рано или поздно обязательно в нее войдет.

Остается ответить на далеко не простой вопрос: «Как нам заставить систему воспринимать несколько операций как одну единую операцию?»

Самым простым решением такой задачи было бы запретить прерывание на время исполнения кода критической секции. Такой подход, хотя и решает задачи в принципе, на практике не может быть применяемый, поскольку вследствие зацикливания или аварии программы в критической секции вся система может остаться с заблокированными прерываниями, а следовательно, в неработоспособном состоянии.

Блокирование

Рациональным решением является использование блокировок (locks). Блокировка - это механизм, который не позволяет более чем одному потоку выполнять код критической секции. Использование блокировки сводится к двум действиям: введение и снятие блокировки. В случае блокирования проверяют, не было ли оно уже сделано другим потоком, и если это так, этот поток переходит в состояние ожидания, иначе он вводит блокировки и входит в критическую секцию. После выхода из критической секции поток снимает блокировку.

Так реализуют свойство взаимного исключения, отсюда происходит другое название для блокировки - мьютекс (mutex, сокращение от mutual exclusion). Впрочем, чаще это название обозначает конкретный механизм ОС, реализующей блокировки.



Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: