Этот вопрос является примером учебника о том, что затрудняет параллельное программирование. Действительно полное объяснение could fill an entire book, а также lots of articles различного качества.
Но мы можем подвести итоги. Глобальная переменная находится в пространстве памяти, видимом для всех потоков. (Альтернатива thread-local storage, что только один поток может видеть.) Таким образом, можно было бы ожидать, что если у вас есть глобальная переменная G, и нить пишет значение х к нему, то поток B будет видеть x, когда он читает эту переменную позже. И вообще, это правда - в конце концов. Интересные части - это то, что происходит до «в конце концов».
Самый большой источник trickiness являются консистенция памяти и memory coherence.
Согласованность описывает то, что происходит, когда поток пишет G и нити B пытается прочитать его почти в тот же момент. Представьте себе, что нить A и B находятся на разных процессорах (давайте также назовем их A и B для простоты).Когда A записывает в переменную, между ним и памятью существует много схем, которые видна нить B. Во-первых, A, вероятно, будет писать до its own data cache. Он сохранит это значение некоторое время до writing it back to main memory. Для очистки кэша основной памяти также требуется время: есть number of signals that have to go back and forth on wires and capacitors and transistors и сложный разговор между кешем и основным блоком памяти. Между тем, B имеет свой собственный кеш. Когда происходят изменения в основной памяти, B может не видеть их сразу —, по крайней мере, пока он не пополнит свой кеш из этой строки. И так далее. В общем, может быть много микросекунд перед потоком A меняется с B.
Согласованность описывает то, что происходит, когда пишет в переменную G, а затем переменная H. Если он вернет эти переменные, он увидит, что записи происходят в этом порядке. Но нить B может видеть их в другом порядке, в зависимости от того, будет ли H сбрасываться с кеша обратно в основную RAM. И что произойдет, если оба A и B написать G в то же время (на настенные часы), а затем попытаться прочитать от него? Какую ценность они увидят?
Согласованность и последовательность выполняются на многих процессорах с помощью операций memory barrier. Например, PowerPC имеет код операции sync, который гласит: «гарантируйте, что любые записи, сделанные любым потоком в основную память, будут видны после любого считывания после этой операции sync». (в основном это делается путем перепроверки каждой строки кэша в отношении основной оперативной памяти.) Архитектура Intel does this automatically to some extent, если вы заранее предостерегаете, что «эта операция касается синхронизированной памяти».
После этого у вас возникла проблема с переупорядочением компилятора . Здесь код
int foo(int *e, int *f, int *g, int *h)
{
*e = *g;
*f = *h;
// <-- another thread could theoretically write to g and h here
return *g + *h ;
}
может быть внутренне преобразован компилятором в нечто вроде
int bar(int *e, int *f, int *g, int *h)
{
int b = *h;
int a = *g;
*f = b ;
int result = a + b;
*e = a ;
return result;
}
, которые могли бы дать вам совершенно другой результат, если другой поток выполнил запись на место, указанное выше! также обратите внимание на то, как записи происходят в другом порядке в bar
. Это проблема, которую должен решить volatile - он не позволяет компилятору хранить значение *g
в локальной сети, но вместо этого заставляет его перезагружать это значение из памяти каждый раз, когда видит *g
.
Как вы можете видеть, это неадекватно для обеспечения согласованности и согласованности памяти на многих процессорах. Это было действительно изобретено для случаев, когда у вас был один процессор, который пытался считывать данные из памяти, - например, последовательный порт, где вы хотите посмотреть местоположение в памяти каждые n микросекунд, чтобы узнать, какое значение в данный момент включено провод. (Это действительно то, как I/O работал обратно, когда они изобрели C.)
Что делать? Ну, как я уже сказал, есть целые книги по этому вопросу.Но короткий ответ заключается в том, что вы, вероятно, захотите использовать средства, которые поддерживает ваша операционная система/среда выполнения для синхронизации памяти.
Например, Windows предоставляет interlocked memory access API, чтобы дать вам наглядный способ общения памяти между потоками и В. GCC tries to expose some similar functions. Intel's threading building blocks дает вам приятный интерфейс для платформ x86/x64, а также the C++11 thread support library.
вам нужно будет объявить указатель как 'volatile' увидеть немедленные обновления в различных потоках на указателе – thumbmunkeys
два слова:«синхронизация»и«'volatile'». – cHao
@Joe: Однако он предотвращает оптимизацию, которая будет кэшировать значение и повторно использовать его, не проверяя его снова. Это важная часть видимости нового значения. – cHao