Мы уже умеем делать “многопоточные программы”: mmap(MAP_SHARED); fork().

Неожиданности на уровне ассемблера

Мы привыкли думать про компьютер как про машину фон Неймана: есть отдельные инструкции, которые выполняются в определённой последовательности, и результаты выполнения каждой “видны” всем последующим:

mov $3, x
mov x, %eax  // очевидно, теперь eax == 3

Так мы всегда писали программы, так видели их в отладчике.

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

CPU    1            CPU    2            Однопоточное исполнение
                                        Sequential consistency

mov $3, x ----------------------------> mov $3, x
                    mov $4, y  -------> mov $4, y
mov y, %eax --------------------------> mov y, %eax
                    mov x, %ecx ------> mov x, %ecx

Но оказывается, что на реальном процессоре может быть вот так:

// Thread 1        // Thread 2

// start with x == 0, y == 0

mov $1, x          mov $2, y
mov y, %eax        mov x, %ecx

// %eax == 0, %ecx == 0

Как ни перемежай эти инструкции, в однопоточной программе такой результат невозможен.

Дело в том, что мы для скорости добавляем в процессоры кэши:

Untitled

Нашу любимую архитектуру x86 можно представить так (Total Store Order):

Untitled

Возможно, один процессор записал в свой write buffer x=1 , другой y=2, но пока не записали это в общую память, а прочитали из общей памяти старые значения.

Тем не менее на x86 не может быть такого:

// start with x=0, y=0

// Thread 1           // Thread 2
x = 1                 r1 = y
y = 1                 r2 = x

                      // **r1 = 1, r2 = 0**
                      // более поздняя запись произошла,
                      // а более ранняя нет

Но такое может быть на архитектуре ARM:

Untitled