pthread_create(3) и гарантия синхронизации памяти в SMP-архитектуре

Я просматриваю раздел 4.11 документа Основных спецификаций Open Group, выпуск 7 (IEEE Std 1003.1, 2013 Edition), раздел 4.11 документа, в котором изложены правила синхронизации памяти. Это наиболее конкретный стандарт POSIX, который мне удалось найти для детализации модели памяти POSIX/C.

Вот цитата

4.11 Синхронизация памяти

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

fork() pthread_barrier_wait() pthread_cond_broadcast() pthread_cond_signal() pthread_cond_timedwait() pthread_cond_wait() pthread_create() pthread_join() pthread_mutex_lock() pthread_mutex_timedlock()

pthread_mutex_trylock() pthread_mutex_unlock() pthread_spin_lock() pthread_spin_trylock() pthread_spin_unlock() pthread_rwlock_rdlock() pthread_rwlock_timedrdlock() pthread_rwlock_timedwrlock() pthread_rwlock_tryrdlock() pthread_rwlock_trywrlock()

pthread_rwlock_unlock() pthread_rwlock_wrlock() sem_post() sem_timedwait() sem_trywait() sem_wait() semctl() semop() wait() waitpid()

(исключения из требования опущены).

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

Я понимаю, что в основном это означает, что должен быть какой-то барьер памяти, подразумеваемый каждой из функций. (хотя стандарт, похоже, не использует эту концепцию). Так, например, возвращаясь из pthread_create(), мы гарантируем, что изменения памяти, сделанные этим потоком до вызова, появятся в других потоках (возможно, с другим процессором/ядром) после того, как они также синхронизируют память. Но как насчет только что созданного потока - подразумевается ли барьер памяти перед тем, как поток начнет выполнять функцию потока, чтобы он безошибочно видел изменения памяти, синхронизированные pthread_create()? Это предусмотрено стандартом? Или мы должны явно обеспечить синхронизацию памяти, чтобы иметь возможность доверять правильности любых данных, которые мы читаем в соответствии со стандартом POSIX?

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

Пример:

Поток #1 создает постоянный объект, выделенный из кучи. Поток #1 создает новый поток #2, который считывает данные из объекта. Если предположить, что новый поток #2 запускается с синхронизированной памятью, то все в порядке. Однако, если ядро ​​ЦП, выполняющее новый поток, имеет копию ранее выделенного, но отбрасывает данные в своей кэш-памяти вместо нового значения, то у него может быть неправильное представление о состоянии, и приложение может работать некорректно.

Более конкретно...

  1. Ранее в программе (это значение в кэш-памяти ЦП №1)

     int i = 0;        
    
  2. Поток T0 работает на CPU #0:

     pthread_mutex_lock(...);
     int tmp = i;
     pthread_mutex_unlock(...);
    
  3. Поток T1 работает на ЦП №1:

     i = 42;
     pthread_create(...);
    
  4. Недавно созданный поток T2, работающий на CPU #0:

     printf("i=%d\n", i);    /* First step in the thread function */
    

Без барьера памяти, без синхронизации памяти потока T2 может случиться так, что вывод будет

     i=0

(ранее кэшированное, несинхронизированное значение).

Обновление: многие приложения, использующие библиотеку потоков POSIX, не были бы потокобезопасными, если бы эта безумная реализация была разрешена.


person FooF    schedule 12.03.2014    source источник
comment
Я отозвал отрицательный голос, так как вы извинились, и ошибки действительно случаются. нет большой проблемы :) Но мой ответ, как и ответ Дэвида Шварца, теперь не имеет смысла - поэтому я удалил свой.   -  person Sigi    schedule 12.03.2014
comment
@nos - насколько я понимаю, pthread_create синхронизирует только вызывающий поток. Хотя было бы безумием предполагать, что вновь созданному потоку требуется синхронизация памяти, прежде чем он сможет надежно получить доступ к каким-либо данным.   -  person FooF    schedule 12.03.2014
comment
разница в два слова дает два разных ответа... эффект бабочки в действии :)   -  person Sigi    schedule 12.03.2014


Ответы (3)


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

да. В противном случае не было бы смысла использовать pthread_create в качестве синхронизации памяти (барьера).

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

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

Нет, переключение контекста не действует как барьер.

Поток № 1 создает постоянный объект, выделенный из кучи. Поток №1 создает новый поток №2, который считывает данные из объекта. Если предположить, что новый поток №2 запускается с синхронизированной памятью, то все в порядке. Однако, если ядро ​​ЦП, выполняющее новый поток, имеет копию ранее выделенного, но отбрасывает данные в своей кэш-памяти вместо нового значения, то у него может быть неправильное представление о состоянии, и приложение может работать некорректно.

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

Теперь, если вы измените свой объект после создания 2. потока, вам снова потребуется синхронизация памяти, чтобы все стороны могли видеть изменения и иначе избежать состояния гонки. Для этого обычно используются мьютексы pthread.

person nos    schedule 12.03.2014
comment
Иначе не было бы смысла... Вот именно! При чтении стандартов необходимо также интерпретировать, используя здравый смысл. - person FooF; 12.03.2014
comment
Поскольку pthread_join также находится в списке библиотечных функций POSIX, обеспечивающих синхронизацию памяти, здравый смысл также подразумевает, что, когда поток возвращается, он также обеспечивает синхронизацию памяти, так что pthread_join получает возвращаемые данные в хорошей и актуальной форме. - person FooF; 13.03.2014

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

Это происходит с важным штрафом, но приложение будет работать правильно.

Поток № 1 работает на CPU0 и хранит память объектов в кеше L1. Когда поток № 2 на CPU1 считывает тот же адрес памяти (или, точнее: ту же строку кэша — ищите ложное совместное использование для получения дополнительной информации), он вызывает промах кэша на CPU0 перед загрузкой этого строка кэша.

person Sigi    schedule 12.03.2014
comment
Хотя это само по себе интересно, насколько на самом деле распространены системы ccNUMA? В статье Википедии о NUMA говорится о ccNUMA: на процессоре AMD Opteron, который может быть реализован без внешней логики, и процессоре Intel Itanium, для которого требуется, чтобы чипсет поддерживал NUMA. [...] Более ранние системы ccNUMA, такие как системы Silicon Graphics, были основаны на процессорах MIPS и процессоре DEC Alpha 21364 (EV7). - person FooF; 13.03.2014
comment
Думаю, если бы системы ccNUMA не были исключением, мы бы не говорили так много о барьерах памяти и прочем в Интернете. Например, программистам ядра не нужно будет переваривать это: kernel.org/ doc/Documentation/memory-barriers.txt - person FooF; 13.03.2014
comment
ccNUMA вовсе не исключение ... это архитектура многопроцессорных архитектур Intel, а также от westmeres для процессоров XF86 (примерно с 2008 года): как только AMD продемонстрировала эту превосходную более масштабируемую архитектуру по сравнению с SMP на основе FSB Intel, Интел ухватился за это. Я думаю, что все современные многопроцессорные системы Intel являются ccNUMA (конечно, вы найдете какое-то исключение из этого утверждения :)). Я упомянул об этом, потому что это расширение модели когерентности внутрипроцессорного/межъядерного кеша, которая используется в кешах современных микропроцессоров. - person Sigi; 13.03.2014
comment
возьмите вашу ссылку (+1) и найдите CACHE COHERENCY (все в верхнем регистре). Вы увидите, в чем проблема со связной системой кеша (порядок работы). С другой стороны, эта проблема связана со спин-блокировками/барьерами памяти в ядре, где у вас нет доступа к функциональным возможностям более высокого уровня, предоставляемым libc (ОС не может использовать ОС). Интерфейсы pthreads имеют механизм, гарантирующий правильную реализацию в соответствии со спецификацией, прозрачно используя такую ​​инфраструктуру более низкого уровня. - person Sigi; 13.03.2014
comment
См. этот: intel. com/content/dam/doc/white-paper/ с объяснением QPI (AMD HyperTransport, версия Intel :)). - person Sigi; 13.03.2014
comment
опечатка в моем предыдущем комментарии ... westmere -> nehalem - это название первой архитектуры Intel XF86 на основе QPI. - person Sigi; 13.03.2014
comment
речь идет не о неравномерности (NUMA) - а о кэш-когерентности (CC). То же самое относится и к SMP. С MIPS, я думаю, у вас есть когерентность кеша ... некоторое время назад я использовал системы SGI Origin: MIPS ccNUMA (на самом деле SGI изобрела термин ccNUMA - или, по крайней мере, я впервые услышал его примерно в 2000 году ... Ссылки NUMA на самом деле были межсоединениями ex-Cray) - person Sigi; 13.03.2014
comment
Спасибо, это полезно знать, чтобы писать правильные многопоточные программы. Мне понадобится время, чтобы изучить этот материал. Я понял, что в системах ccNUMA и кэш-когерентных системах в целом (?) барьеры памяти ЦП нужны только для того, чтобы гарантировать, что операции записи и чтения памяти правильно упорядочены и не затронуты Переупорядочивание доступа к памяти ЦП (параллелизм внутри инструкций ЦП и конвейера доступа к памяти для максимизации пропускной способности инструкций)? - person FooF; 14.03.2014
comment
барьеры памяти не нужны для стандартных многопоточных программ, но я согласен, что очень полезно знать все внутренности, особенно при решении проблем. А вот для разработки ПО есть полезные модели — pthreads. Что касается барьеров памяти, то да. это довольно хорошее резюме: en.wikipedia.org/wiki/Memory_barrier - person Sigi; 15.03.2014

Вы превратили гарантию, которую предоставляет pthread_create, в бессвязную. Единственное, что может сделать функция pthread_create, — это установить отношение «происходит до» между потоком, который ее вызывает, и вновь созданным потоком.

Невозможно установить такую ​​связь с существующими потоками. Рассмотрим два потока: один вызывает pthread_create, другой обращается к общей переменной. Какая у вас может быть гарантия? «Если поток вызвал pthread_create первым, то другой поток гарантированно увидит последнее значение переменной». Но это «если» делает гарантию бессмысленной и бесполезной.

Создание потока:

i = 1;
pthread_create (...)

Созданный поток:

if (i == 1)
   ...

Теперь это последовательная гарантия — созданный поток должен видеть i как 1, поскольку это «произошло до» создания потока. Наш код позволил стандарту обеспечить логическую связь «произошло раньше», и стандарт сделал это, чтобы гарантировать нам, что наш код работает так, как мы ожидаем.

Теперь давайте попробуем сделать это с несвязанным потоком:

Создание потока:

i = 1;
pthread_create (...)

Несвязанная тема:

if ( i == 1)
    ...

Какую гарантию мы могли бы иметь, даже если бы стандарт хотел ее предоставить? Без синхронизации между потоками мы не пытались сделать логику перед отношениями. Так что стандарт не может соблюдать его — нечего соблюдать. Не существует определенного «правильного» поведения, поэтому стандарт никоим образом не может обещать нам правильное поведение.

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

person David Schwartz    schedule 12.03.2014
comment
Мой первоначально опубликованный вопрос был недостаточно ясен. Я уточнил свой вопрос. Уместно думать только о двух потоках (тот, который вызывает pthread_create(), и вновь созданный поток), хотя для того, чтобы старое значение памяти кэшировалось в ядре ЦП вновь запущенного потока, должен быть несвязанный поток, имеющий доступ к памяти. место расположения. - person FooF; 12.03.2014
comment
На мой взгляд, стандарт не очень последовательно определяет семантику синхронизации памяти вновь созданного потока, отсюда и вопрос, хотя если подумать об этом, было бы безумием иметь реализацию, которая не гарантирует, что вновь запущенный поток запускается без синхронизации с вызывающим потоком. - person FooF; 12.03.2014
comment
Извините за неясную первую версию вопроса и спасибо за вклад. - person FooF; 12.03.2014