Введение

В вычислительной технике, ограничение доступа к данным одной нитью за раз – это концепция, используемая в логике и теории вероятностей. В информатике взаимное исключение является свойством управления конкурентным доступом, которое применяется для предотвращения гонок данных. Оно заключается в требовании, чтобы один поток выполнения никогда не входил в критическую секцию, пока другой поток выполнения уже получает к ней доступ. Критическая секция – это интервал времени, в течение которого поток выполнения обращается к общему ресурсу или общей памяти. Общий ресурс – это объект данных, который пытаются изменить два или более конкурирующих потока (при этом разрешены две одновременные операции чтения, но не разрешены две одновременные операции записи или одна операция чтения и одна операция записи, поскольку это приводит к несогласованности данных). Алгоритмы взаимного исключения гарантируют, что если процесс уже выполняет операцию записи в объект данных [критическую секцию], то ни одному другому процессу/потоку не разрешается получать доступ к этому объекту или изменять его, пока первый процесс не завершит запись в объект данных [критическую секцию] и не освободит объект для чтения и записи другими процессами. Требование взаимного исключения впервые было определено и решено Эдсгером В. Дейкстрой в его основополагающей статье 1965 года «Решение проблемы управления конкурентным программированием», которая считается первой темой в изучении конкурентных алгоритмов. Простой пример важности взаимного исключения на практике можно представить с помощью односвязного списка из четырех элементов, из которого необходимо удалить второй и третий элементы. Удаление узла, находящегося между двумя другими узлами, выполняется путем изменения указателя `next` предыдущего узла, чтобы он указывал на следующий узел (то есть, если удаляется узел `i`, то указатель `next` узла `i-1` изменяется, чтобы он указывал на узел `i+1`, тем самым удаляя из списка любую ссылку на узел `i`). Если такой список используется совместно несколькими потоками выполнения, два потока могут одновременно попытаться удалить два разных узла: один поток изменяет указатель `next` узла `i-1`, чтобы он указывал на узел `i+1`, а другой поток изменяет указатель `next` узла `i`, чтобы он указывал на узел `i+2`. Хотя обе операции удаления завершаются успешно, желаемое состояние списка не достигается: узел `i+1` остается в списке, поскольку указатель `next` узла `i-1` указывает на узел `i+1`. Эту проблему (называемую гонкой данных) можно избежать, используя требование взаимного исключения, чтобы гарантировать, что одновременные обновления одной и той же части списка не могут произойти. Термин «взаимное исключение» также используется для обозначения одновременной записи в адрес памяти одним потоком, в то время как к этому же адресу памяти обращаются (манипулируют или читают) один или несколько других потоков.

Аппаратные решения

На системах с одним процессором, самым простым способом достижения взаимного исключения является отключение прерываний во время критической секции процесса. Это предотвратит выполнение любых обработчиков прерываний (фактически предотвращая вытеснение процесса). Хотя это решение эффективно, оно приводит ко многим проблемам. Если критическая секция занимает много времени, системные часы будут сбиваться каждый раз при выполнении критической секции, поскольку прерывание таймера не обслуживается, что делает отслеживание времени в критической секции невозможным. Кроме того, если процесс остановится во время критической секции, управление не вернется другому процессу, что фактически приведет к остановке всей системы. Более изящный способ достижения взаимного исключения – это занятое ожидание. Занятое ожидание эффективно как для однопроцессорных, так и для многопроцессорных систем. Взаимное исключение обеспечивается использованием общей памяти и атомарной инструкции "проверка и установка". Процесс может выполнить проверку и установку в ячейке общей памяти, и поскольку операция атомарна, только один процесс может установить флаг одновременно. Любой процесс, которому не удалось установить флаг, может либо перейти к выполнению других задач и попробовать снова позже, либо освободить процессор для другого процесса и попробовать снова позже, либо продолжать циклически проверять флаг, пока не сможет его получить. Вытеснение все еще возможно, поэтому этот метод позволяет системе продолжать функционировать, даже если процесс останавливается, удерживая блокировку. Для обеспечения взаимного исключения структур данных можно использовать несколько других атомарных операций, наиболее заметной из которых является "сравнить и заменить" (CAS). CAS можно использовать для достижения взаимоисключения без ожидания для любой общей структуры данных, создав связанный список, в котором каждый узел представляет желаемую операцию. Затем CAS используется для изменения указателей в связанном списке при вставке нового узла. Только один процесс может успешно выполнить CAS; все остальные процессы, пытающиеся добавить узел одновременно, должны будут повторить попытку. Каждый процесс может хранить локальную копию структуры данных и, проходя по связанному списку, выполнять каждую операцию из списка на своей локальной копии.

Ограниченная проблемой взаимного исключения

Один бинарный регистр "test&set" достаточен для обеспечения решения проблемы взаимного исключения, свободного от взаимных блокировок. Однако решение, построенное с использованием регистра "test&set", может привести к голоданию некоторых процессов, которые окажутся в бесконечном цикле попыток.

Восстанавливаемое взаимное исключение

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