Что такое "Линеаризуемость"?

может кто-нибудь там помогите мне понять, что линеаризуемости - это? Мне нужно простое и понятное объяснение. Я читаю искусство многопроцессорного программирования Маруис Херилихи и Нир Шавит и я пытаюсь понять Главу 3 о параллельных объектах.

Я понимаю, что метод Линеаризуем, если он имеет точку, где он, кажется," вступает в силу " мгновенно с точки зрения других потоков. Что имеет смысл, но также говорится, что Линеаризуемость на самом деле является свойством истории выполнения. Что означает, что история выполнения Линеаризуема, почему меня это волнует и как она связана с Линеаризуемым методом или объектом?

спасибо!

4 ответов


один объект является линеаризуемых если

(a) каждый из его методов является атомарным. Представьте их как синхронизированные методы java, но более подробно ниже.

(b) может быть не более одной ожидающей операции от любого данного потока/процессора.

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

теперь, (a) можно ослабить намного больше. Линеаризуемости требует эффект от этой операции атомарный. Таким образом, операция добавления в связанном списке без блокировки будет иметь одну точку в ее выполнении ("точка линеаризации"), до которой этот элемент не был добавлен, и после которой элемент определенно находится. Это лучше, чем получение блокировок, потому что блокировки могут блокировать бесконечно.

теперь, когда несколько потоков одновременно вызывают линеаризуемый объект, объект ведет себя так, как будто методы вызываются в некоторой линейной последовательности (из-за атомарности требование); два перекрывающихся вызова могут быть сделаны линейными в некотором произвольном порядке.

и потому, что они вынуждены иметь эффект когда-то во время вызова метода (стеки должны нажимать/pop, наборы должны добавлять/удалять и т. д.), объект может быть рассуждал об известных последовательных методов спецификаций (пред-и пост условия и т. д.).

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


может быть путаница между линеаризуемости против Сериализуемость.

с эта статья (П. Бейлис):

линеаризуемости является гарантией одиночных операций на отдельных объектах [...] Линеаризуемость для операций чтения и записи синонимична термину "атомарная согласованность" и является "C" или "согласованностью" в доказательстве Гилберта и Линча теоремы CAP.

Сериализуемость гарантия о транзакциях или группах одной или нескольких операций над одним или несколькими объектами. Он гарантирует, что выполнение набора транзакций (обычно содержащих операции чтения и записи) по нескольким элементам эквивалентно некоторому последовательному выполнению (полному упорядочению) транзакций [...] Сериализуемость-это традиционное "я", или изоляция, в кислоте.


ну, я думаю, что могу ответить на этот вопрос кратко.

когда мы собираемся сказать, является ли параллельный объект правильным, мы всегда пытаемся найти способ расширить частичный порядок до полного порядка.

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

во-первых, давайте отложим параллельный объект в сторону. Мы обсудим это позже. Теперь рассмотрим последовательную историю H_S, последовательная история-это последовательность события (i.e вызывает и отвечает), из которых каждый вызов следует мгновенно соответствующий ответ.(Хорошо," мгновенно " можно запутать, рассмотрим выполнение однопоточной программы, конечно, есть интервал между каждым вызовом и его ответом, но методы выполняются один за другим. Таким образом, "мгновенно" означает, что никакой другой Invoke/Respone не может вставляться в пару Invoke_i~Response_i)

h_s может выглядеть так:

H_S : I1 R1 I2 R2 I3 R3 ... In Rn
(Ii means the i-th Invoke, and Ri means the i-th Response) 

это будет очень легко рассуждать о правильности истории H_S, потому что нет никакого параллелизма, что нам нужно сделать, это проверить выполнение работ, а также то, что мы ожидаем(соответствует условиям последовательная спецификация). Другими словами, это правовые последовательная история.

хорошо, реальность такова, что мы работаем с параллельной программой. Например, мы запускаем два потока A и B в нашей программе. Каждый раз мы бежим программы, мы получим H_C истории(History_Concurrent) из exection. Нам нужно рассмотреть вызов метода Как Ii~Ri, как указано выше в H_S. Конечно, должно быть много перекрытий между вызовами метода, вызываемыми потоком A и потоком B. Но каждое событие (i.e вызывает и отвечает) имеет порядок в реальном времени. Таким образом, вызовы и ответ всех методов, вызываемых A и B, могут быть сопоставлены последовательному порядку, порядок может выглядеть так:

H_C : IA1 IB1 RA1 RB1 IB2 IA2 RB2 RA2

порядок кажется запутанным, это просто сортировка событий каждого метода вызывает:

thread A:         IA1----------RA1               IA2-----------RA2
thread B:          |      IB1---|---RB1    IB2----|----RB2      |
                   |       |    |    |      |     |     |       |
                   |       |    |    |      |     |     |       |
real-time order:  IA1     IB1  RA1  RB1    IB2   IA2   RB2     RA2
                ------------------------------------------------------>time

и мы получили H_C. Итак, как мы можем проверить правильность выполнения H_C? Мы можем!--19-->упорядочить H_C к H_RO следуя правилу:

правила: если один вызов метода M1 предшествует другому m2, то m1 должен предшествовать m2 в переупорядоченной последовательности. (это означает, что если Ri находится перед Ij в H_C, вы должны гарантировать, что Ri все еще находится перед Ij в переупорядоченная последовательность, i и j не имеют своих порядков, мы также можем использовать a,b, c...) Мы говорим, что H_C эквивалентно в H_RO (history_reorder) по такому правилу.

H_RO будет иметь 2 свойства:

  1. он уважает порядок программы.
  2. он сохраняет поведение в реальном времени.

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

H_S1: IA1 RA1 IB1 RB1 IB2 RB2 IA2 RA2
H_S2: IB1 RB1 IA1 RA1 IB2 RB2 IA2 RA2
H_S3: IB1 RB1 IA1 RA1 IA2 RA2 IB2 RB2
H_S4: IA1 RA1 IB1 RB1 IA2 RA2 IB2 RB2

однако мы не можем получить H_S5:

H_S5: IA1 RA1 IA2 RA2 IB1 RB1 IB2 RB2

поскольку IB1~RB1 полностью предшествует IA2~RA2 в H_C, он не может быть переупорядочен.

теперь, с этими последовательными историями, как мы могли бы подтвердить, является ли наше исполнение история H_C верна?(Я выделяю историю H_C, это означает, что мы просто заботимся о правильности истории h_c сейчас, а не о правильности параллельного программа)

ответ прост, если хотя бы одна из последовательных историй верна(юридическая последовательная история соответствует условиям последовательной спецификации), то история H_C linearizable, мы называем юридический H_S линеаризации H_C. И H_C-правильное выполнение. Другими словами, это законное исполнение, которого мы ожидали. Если у вас есть опыт параллельного программирования, необходимо написать такую программу, которая иногда выглядит вполне хорошо, но иногда совершенно неправильно.

теперь мы знаем что такое линеаризуемых истории выполнения параллельной программы. Так что насчет самой параллельной программы?


основная идея линейности заключается в том, что каждая параллельная история эквивалентна, в следующем смысле, некоторым последовательным история. [искусство многопроцессорного программирования 3.6.1: Линеаризуемость] ("следующий смысл" - это правило переупорядочения, о котором я говорил выше)

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

теперь мы поняли, что такое линеаризуемости. Если мы говорим, что наша параллельная программа линеаризуема, другими словами, она обладает свойством линеаризуемости. Это означает, что каждый раз, когда мы запускаем его, история линеаризуема(история-это то, что мы ожидаем).

так что очевидно, что линеаризуемости это безопасность(правильность) собственность.


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

обычный способ показать, что параллельная реализация объекта линеаризуемых заключается в определении для каждого метода с точки линеаризации где метод такс эффект. [искусство многопроцессорного программирования 3.5.1: точки линеаризации]

мы обсудим этот вопрос в условиях "параллельного объекта". В сущности, это то же самое, что и выше. Реализация параллельного объекта имеет некоторые методы для доступа к данным параллельного объекта. И multi-threads будут совместно использовать параллельный объект. Поэтому, когда они обращаются к объекту одновременно, вызывая методы объекта, реализующий concurrent object должен обеспечивать корректность одновременных вызовов методов.

он будет идентифицировать для каждого метода a точка линеаризации. Самое главное-понять значение точки линеаризации. Утверждение "где метод вступает в силу" действительно трудно понять. У меня есть несколько примеров:

во-первых, давайте посмотрим на неправильную случае:

//int i = 0; i is a global shared variable.
int inc_counter() {
    int j = i++;
    return j;
}

довольно легко найти ошибку. Мы можем перевести i++ в:

#Pseudo-asm-code
Load   register, address of i
Add    register, 1
Store  register, address of i

таким образом, два потока могут выполнять один "i++;" одновременно, и результат i кажется увеличенным только один раз. Мы могли бы получить такой H_C:

thread A:         IA1----------RA1(1)                  IA2------------RA2(3)
thread B:          |      IB1---|------RB1(1)    IB2----|----RB2(2)    |
                   |       |    |        |        |     |     |        |
                   |       |    |        |        |     |     |        |
real-time order:  IA1     IB1  RA1(1)  RB1(1)    IB2   IA2   RB2(2)   RA2(3)
                ---------------------------------------------------------->time

независимо от того, что вы пытаетесь изменить порядок в реальном времени, вы не должны найти последовательную историю legel, эквивалентную H_C.

мы должны переписать программу:

//int i = 0; i is a global shared variable.
int inc_counter(){
    //do some unrelated work, for example, play a popular song.
    lock(&lock);
    i++;
    int j = i;
    unlock(&lock);
    //do some unrelated work, for example, fetch a web page and print it to the screen.
    return j;
}

хорошо, какова точка линеаризации inc_counter ()? Ответ - весь критический раздел. Потому что когда многие потоки повторно вызывают inc_counter (), критический раздел будет выполняться атомарно. И это может гарантировать правильность метода. Ответом метода является приращенное значение global i. Рассмотрим H_C как:

thread A:         IA1----------RA1(2)                 IA2-----------RA2(4)
thread B:          |      IB1---|-------RB1(1)    IB2--|----RB2(3)    |
                   |       |    |        |         |   |     |        |
                   |       |    |        |         |   |     |        |
real-time order:  IA1     IB1  RA1(2)  RB1(1)     IB2 IA2   RB2(3)   RA2(4)

очевидно, что эквивалентная последовательная история является законной:

IB1 RB1(1) IA1 RA1(2) IB2 RB2(3) IA2 RA2(4)  //a legal sequential history

мы переупорядочили IB1~RB1 и IA1~RA1, потому что они перекрываются в режиме реального времени, они могут быть переупорядочены неоднозначно. В случае из H_C мы можем предположить, что критический раздел IB1~RB1 вводится первым.

пример слишком простой. Давайте рассмотрим еще один:

//top is the tio
void push(T val) {
    while (1) {
        Node * new_element = allocte(val);
        Node * next = top->next;
        new_element->next = next;
        if ( CAS(&top->next, next, new_element)) {  //Linearization point1
            //CAS success!
            //ok, we can do some other work, such as go shopping.
            return;
        }
        //CAS fail! retry!
    }
}

T pop() {
    while (1) {
        Node * next = top->next;
        Node * nextnext = next->next;
        if ( CAS(&top->next, next, nextnext)) { //Linearization point2
            //CAS succeed!
            //ok, let me take a rest.
            return next->value;
        }
        //CAS fail! retry!
    }
}

это алгоритм стека без блокировки полно ошибок! но не заботьтесь о деталях. Я просто хочу показать точку линеаризации push () и pop (). Я показал их в комментариях. Рассмотрим, что многие потоки повторно вызывают push() и pop (), они будут заказаны в CAS шаг. И другие шаги, по-видимому, не имеют значения, потому что независимо от того, что они одновременно выполняются, конечный эффект, который они будут принимать на стеке(точно верхняя переменная), обусловлен порядком шага CAS(точка линеаризации). Если мы можем убедиться, что точка линеаризации действительно работает, то параллельный стек является правильным. Изображение H_C слишком длинное, но мы можем подтвердить, что должен быть юридический последовательный эквивалент H_C.


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

тогда вы можете сказать, что ваш параллельный объект верен.


Как я объяснил в в этой статье картинка стоит 1000 слов.

enter image description here

первая инструкция SELECT считывает значение 50, в то время как вторая SELECT считывает значение 10, так как между двумя операциями чтения была выполнена операция записи.

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

что произойдет, если у вас нет линеаризуемости?

enter image description here

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

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

одно подключение к базе данных изменяет баланс счета с 50 на 10 и фиксирует транзакцию. Сразу после этого вторая транзакция считывается с узла-подписчика, но поскольку репликация не применяла изменение баланса, считывается значение 50.

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