Введение

Свойство некоторых операций в параллельном программировании

В параллельном программировании операция (или набор операций) является линеаризуемой, если она состоит из упорядоченного списка событий вызова и ответа, который можно расширить добавлением событий ответа таким образом, чтобы:

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

История линеаризации

Линеаризуемость была впервые представлена как модель согласованности Херлихи и Вингом в 1987 году. Она включала в себя более строгие определения атомарности, такие как "атомарная операция — это операция, которая не может быть (или не прерывается) другими выполняющимися одновременно операциями", которые обычно расплывчаты в отношении того, когда операция считается начатой и завершенной. Атомарный объект можно понять немедленно и полностью, исходя из его последовательного определения, как набор операций, выполняемых параллельно, которые всегда кажутся происходящими последовательно друг за другом; никаких противоречий возникнуть не должно. В частности, линеаризуемость гарантирует, что инварианты системы наблюдаются и поддерживаются всеми операциями: если каждая операция по отдельности сохраняет инвариант, то и система в целом будет его сохранять.

Высокоуровневые атомные операции

Самый простой способ добиться линеаризуемости — выполнять группы примитивных операций в критической секции. Строго говоря, независимые операции затем могут аккуратно перекрывать свои критические секции, при условии, что это не нарушает линеаризуемость. Такой подход должен балансировать между стоимостью большого количества блокировок и преимуществами повышенного параллелизма. Другой подход, предпочитаемый исследователями (но пока не получивший широкого распространения в индустрии программного обеспечения), заключается в разработке линеаризуемого объекта с использованием встроенных атомарных примитивов, предоставляемых аппаратным обеспечением. Это может максимизировать доступный параллелизм и минимизировать затраты на синхронизацию, но требует математических доказательств, подтверждающих корректность поведения объектов. Многообещающим гибридом этих двух подходов является предоставление абстракции транзакционной памяти. Как и в случае с критическими секциями, пользователь помечает последовательный код, который должен выполняться изолированно от других потоков. Реализация затем обеспечивает атомарное выполнение этого кода. Такой стиль абстракции часто используется при взаимодействии с базами данных; например, при использовании Spring Framework аннотация метода с помощью @Transactional гарантирует, что все операции с базой данных, заключенные в этот метод, будут выполнены в рамках единой транзакции базы данных. Транзакционная память идет еще дальше, обеспечивая атомарность всех операций с памятью. Как и в случае с транзакциями базы данных, возникают вопросы, касающиеся композиции транзакций, особенно транзакций базы данных и операций в памяти. Общей чертой при проектировании линеаризуемых объектов является предоставление интерфейса типа «все или ничего»: либо операция выполняется полностью успешно, либо она завершается неудачей и не оказывает никакого эффекта. (В базах данных ACID этот принцип называется атомарностью). Если операция не удается (обычно из-за конкурентных операций), пользователь должен повторить попытку, как правило, выполнив другую операцию. Например:
Compare and swap записывает новое значение в ячейку памяти только в том случае, если ее текущее содержимое соответствует предоставленному старому значению. Это часто используется в последовательности «чтение-модификация-CAS»: пользователь читает ячейку памяти, вычисляет новое значение для записи и записывает его с помощью CAS (compare and swap); если значение изменяется одновременно, CAS завершится неудачей, и пользователь попытается снова. Load link/store conditional кодирует эту схему более прямолинейно: пользователь читает ячейку памяти с помощью load link, вычисляет новое значение для записи и записывает его с помощью store conditional; если значение изменилось одновременно, SC (store conditional) завершится неудачей, и пользователь попытается снова. В транзакции базы данных, если транзакцию невозможно завершить из-за конкурентной операции (например, из-за взаимоблокировки), транзакция будет прервана, и пользователь должен повторить попытку.