Настольный теннис без замков в C11

Я новичок в параллелизме в C и пытаюсь кое-что понять, чтобы понять, как это работает.

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

#if ATOMIC_INT_LOCK_FREE != 2
    #error atomic int should be always lock-free
#else
    static _Atomic int flag;
#endif

static void *ping(void *ignored){
    while(1){
        int val = atomic_load_explicit(&flag, memory_order_acquire);
        if(val){
            printf("ping\n");
            atomic_store_explicit(&flag, !val, memory_order_release);
        }
    }
    return NULL;
}
static void *pong(void *ignored){
    while(1){
        int val = atomic_load_explicit(&flag, memory_order_acquire);
        if(!val){
            printf("pong\n");
            atomic_store_explicit(&flag, !val, memory_order_release);
        }
    }
    return NULL;
}

int main(int args, const char *argv[]){
    pthread_t pthread_ping;
    pthread_create(&pthread_ping, NULL, &ping, NULL);

    pthread_t pthread_pong;
    pthread_create(&pthread_pong, NULL, &pong, NULL);
}

Я тестировал его несколько раз, и он работал, но есть вещи, которые кажутся странными:

  1. Он либо заблокирован, либо не компилируется

Поскольку Стандарт определяет свойство lock-free равным 2, чтобы все операции с атомарным типом всегда были свободными от блокировки. В частности, я проверил код компиляции, и он выглядит как

sub    $0x8,%rsp
nopl   0x0(%rax)
mov    0x20104e(%rip),%eax        # 0x20202c <flag>
test   %eax,%eax
je     0xfd8 <ping+8>
lea    0xd0(%rip),%rdi        # 0x10b9
callq  0xbc0 <puts@plt>
movl   $0x0,0x201034(%rip)        # 0x20202c <flag>
jmp    0xfd8 <ping+8>

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

  1. Использование stdatomics с pthreads

Я застрял в glibc 2.27, где threads.h еще не реализован. Вопрос в том, строго ли это соответствует требованиям? В любом случае это как-то странно, если у нас есть атомики, но нет потоков. В чем же тогда согласованное использование stdatomics в многопоточном приложении?


person Some Name    schedule 10.04.2019    source источник
comment
Вопрос только в том, полезны ли атомики без нитей? Ответ на этот вопрос: возможно, поскольку у вас может быть общая память даже между однопоточными процессами.   -  person Useless    schedule 10.04.2019
comment
@Useless Собственно 2 вопроса. 1. Можно ли сделать такую ​​программу гарантированно разблокированной? 2. Соответствует ли N1570 ISO/IEC 9899:201x (или хорошо, если мы примем во внимание, что мы используем pthreads на Linux) для использования stdatomics с pthreads (или любой другой реализации потоковой передачи)?   -  person Some Name    schedule 10.04.2019
comment
Это соответствует .... Соответствует чему? ISO / IEC 9899: 2011?   -  person 4386427    schedule 10.04.2019
comment
@ 4386427 Да. Я рассматриваю N1570 ISO/IEC 9899:2011 и на самом деле пытаюсь понять, как использовать stdatomic без threads.h. Гарантирует ли Стандарт, что атомарное поведение должно быть четко определено со всеми реализациями потоков (например, pthreads, erlier LinuxThreads и т. Д.)   -  person Some Name    schedule 10.04.2019
comment
@SomeName Мне это непонятно. Вы говорите, что хотите знать, соответствует ли код стандарту C11, но в коде используется pthread, которого нет в стандарте. Также я не понимаю, насколько важны ассемблерный код и какое-то конкретное поведение Intel, если вы хотите знать, соответствует ли какой-либо C стандарту.   -  person 4386427    schedule 10.04.2019
comment
@SomeName: существует иерархия памяти (отношения между кешами, шинами / связями, контроллерами памяти, микросхемами RAM, ...). Операция является атомарной, если ничто другое, разделяющее (любая часть) иерархии памяти, не может мешать работе (включая устройства, выполняющие управление DMA / шиной, обработчики прерываний, код ядра, различные функции ЦП, вызывающие чтение / запись, другие ЦП, выполняющие потоки в тот же или другой процесс, ...).   -  person Brendan    schedule 10.04.2019
comment
@ 4386427 Наверное, в вопросе странная формулировка. Насколько я понимаю, что указано в 5.1.2.3: При размещенной реализации программа может иметь более одного потока выполнения (или потока), выполняющихся одновременно. Выполнение каждого потока продолжается, как определено в остальной части этого стандарта. не упоминается, что потоки точно из threads.h, но как определено в размещенной среде. Таким образом, гарантии видимости, предоставляемые stdatomic, также должны согласовываться с pthreads. Разве такое рассуждение не верно?   -  person Some Name    schedule 10.04.2019
comment
@ 4386427 Так как мне нужно было точное поведение без блокировки, я посмотрел на фрагмент сборки, чтобы убедиться ...   -  person Some Name    schedule 10.04.2019
comment
@Brendan Итак, кажется, stdatomics можно использовать в любое время, когда мне нужны гарантии атомарности / порядка памяти (даже если threads.h не реализованы) ...   -  person Some Name    schedule 10.04.2019
comment
@SomeName: если компилятор / библиотека не сломана и не работает; stdatomic должен гарантировать, что все атомарное является атомарным с точки зрения любого возможного внешнего наблюдателя (независимо от того, реализованы или используются потоки и какой тип потоков). Обратите внимание, что только для потоков вам понадобится атомарный элемент с точки зрения другого потока, который значительно слабее, чем атомарный с точки зрения любого возможного наблюдателя, который должен предоставить stdatomic.   -  person Brendan    schedule 11.04.2019


Ответы (1)


Термин без блокировки имеет два значения:

  1. в информатике означает: застревание одного потока не может помешать другим. Эту задачу невозможно сделать без блокировки, вам нужно, чтобы потоки ожидали друг друга. (https://en.wikipedia.org/wiki/Non-blocking_algorithm)

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

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


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

Более интересным тестом было бы использование разблокированных операций stdio, таких как
fputs_unlocked("ping\n", stdio);, чтобы воспользоваться преимуществом (и зависеть от) того факта, что вы уже гарантировали взаимное исключение между потоками. См. unlocked_stdio (3).

И протестируйте с перенаправлением вывода в файл, поэтому stdio полностью буферизуется вместо строчной буферизации. (Системный вызов типа write() в любом случае полностью сериализуется, например atomic_thread_fence(mo_seq_cst).)


Он либо блокируется, либо не компилируется

Хорошо, почему это странно? Вы выбрали это. Это необязательно; алгоритм по-прежнему работал бы в реализациях C без постоянной блокировки atomic_int.

atomic_bool может быть лучшим выбором, поскольку он свободен от блокировок на большем количестве платформ, включая 8-битные платформы, где int принимает 2 регистра (потому что он должен быть как минимум 16-битным). Реализации могут сделать atomic_bool 4-байтовый тип на тех платформах, где это более эффективно, но IDK, если таковой вообще существует. (На некоторых платформах, отличных от x86, загрузка / сохранение байтов требует дополнительного цикла задержки для чтения / записи в кеш. Здесь пренебрежимо мало, потому что вы всегда имеете дело со случаем пропуска межъядерного кеша.)

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

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

Да, но эта беспрепятственная генерация asm-кода происходит только при компиляции для x86. Компиляторы могут и должны применять правило «как если бы» для создания asm, который запускается на целевом объекте компиляции , как если бы источник C был запущен на абстрактной машине C.


Использование stdatomics с pthreads

Гарантирует ли стандарт ISO C четкое определение поведения атома со всеми реализациями потоков (например, pthreads, более ранние LinuxThreads и т. Д.)

Нет, ISO C ничего не говорит о языковых расширениях, таких как POSIX.

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

Это единственный случай, когда ISO C или C ++ пытается предписать поведение для расширений.

Но стандарт POSIX, надеюсь, что-то говорит о stdatomic! Вот куда вам следует смотреть; он расширяет ISO C, а не наоборот, поэтому pthreads - это стандарт, который должен указывать, что его потоки работают как C11 thread.h и что работают атомики.

На практике, конечно, stdatomic на 100% подходит для любой реализации потоков, где все потоки используют одно и то же виртуальное адресное пространство. Это включает в себя такие вещи, как _Atomic my_large_struct foo;, не требующие блокировки.

person Peter Cordes    schedule 10.04.2019