C - Могут ли глобальные указатели быть изменены разными потоками?

Есть ли у глобальных указателей область видимости между потоками?

Например, предположим, что у меня есть два файла, file1.c и file2.c:

file1.c:

uint64_t *g_ptr = NULL;

modify_ptr(&g_ptr) { 
    //code to modify g_ptr to point to a valid address 
}

read_from_addr() {
    //code which uses g_ptr to read values from the memory it's pointing to
}

file2.c:

function2A() {
    read_from_addr();
}

Итак, у меня есть threadA, который проходит через file1.c и выполняет modify_ptr (& g_ptr), а также read_from_addr (). Затем запускается threadB, и он проходит через file2.c, выполняя function2A ().

Мой вопрос: видит ли threadB, что g_ptr изменен? Или он все еще видит, что указывает на NULL?

Если это не так, что означает, что указатель является глобальным? И как мне убедиться, что этот указатель доступен между разными потоками?

Пожалуйста, дайте мне знать, если мне нужно что-то уточнить. Спасибо


person OfLettersAndNumbers    schedule 03.09.2013    source источник
comment
вам нужно будет объявить указатель как volatile, чтобы увидеть немедленные обновления в разных потоках на указателе   -  person thumbmunkeys    schedule 03.09.2013
comment
Два слова: синхронизация и volatile.   -  person cHao    schedule 03.09.2013
comment
@Joe: Однако он предотвращает оптимизацию, которая кэширует значение и повторно использует его без повторной проверки. Это важная часть того, чтобы сделать новое значение видимым.   -  person cHao    schedule 03.09.2013
comment
Согласовано: stackoverflow .com / questions / 4557979 /.   -  person Joe    schedule 03.09.2013
comment
volatile не дает гарантий непротиворечивости памяти.   -  person user7116    schedule 03.09.2013
comment
Volatile здесь бесполезен, должна быть какая-то синхронизация, а сама синхронизация гарантирует, что глобальные данные не кэшируются в одном потоке, пока они модифицируются другим потоком. см. stackoverflow.com/questions/2484980/   -  person Étienne    schedule 03.09.2013
comment
Кстати, modify_ptr (& g_ptr) {не является правильным C, вы имели в виду modify_ptr (uint64_t * указатель) {?   -  person Étienne    schedule 03.09.2013
comment
@ Étienne: Синхронизация не помешает компилятору оптимизировать повторную проверку значения. Именно для этого volatile.   -  person cHao    schedule 03.09.2013
comment
@cHao: см. software.intel.com/en-us/blogs/2007/11/30/ и stackoverflow.com/questions/2478397/atomic-swap-in-gnu-c/   -  person Étienne    schedule 03.09.2013
comment
@ Этьен: Посмотрите эту первую ссылку сами - особенно второй полезный случай. Это именно то, что мы имеем здесь; другой поток - внешний агент. То, что volatile ничего не гарантирует о видимости нового значения, не имеет значения. Дело в том, что он заставляет компилятор предполагать, что значение может измениться, поэтому что-то вроде while (g_ptr); не повторяется навсегда, когда вы включаете оптимизацию.   -  person cHao    schedule 03.09.2013
comment
@cHao: он не может зацикливаться вечно, если вы приобрели на нем мьютекс, другой поток должен ждать, пока ваш поток, содержащий while(g_ptr), освободит мьютекс, чтобы иметь возможность его изменить. Барьер памяти, обеспечиваемый мьютексом, не позволяет компилятору переупорядочивать, делая volatile совершенно бесполезным.   -  person Étienne    schedule 03.09.2013
comment
@ Этьен: изменение порядка не имеет отношения к значению, которое кэшировано в коде. И мьютекс не препятствует кэшированию значения в коде. Забор памяти только защищает оборудование от кэширования; это не мешает компилятору генерировать mov esi, [g_ptr] и использовать esi везде, где он иначе использовал бы [g_ptr].   -  person cHao    schedule 03.09.2013
comment
@ Этьен: Даже с мьютексом нет гарантии, что g_ptr имеет правильные значения, поскольку этот компилятор может иметь значение, кэшированное в некоторых регистрах.   -  person bkausbk    schedule 03.09.2013
comment
@cHao, @bkausbk: Хорошо, действительно, если вы не приобретете мьютекс для чтения глобальной переменной и выполните while(g_ptr) без использования какой-либо функции в цикле, значение g_ptr может быть кэшировано, если вы не объявите его изменчивым. Но если вы заблокируете мьютекс для чтения значения g_ptr, его значение не может быть кэшировано компилятором.   -  person Étienne    schedule 03.09.2013
comment
@ Этьен: Учтите, что до C11 определение абстрактной машины даже не предполагало многопоточность. В соответствии со всеми предыдущими стандартами компилятор мог свободно предполагать, что любой объект, о котором он знает, что текущий поток не изменил, не изменился, если только к этому объекту не обращались напрямую через переменную volatile. Будь прокляты мьютексы и заборы памяти; они даже не рассматривались до C11. Что касается C11, я не могу предположить, что это изменилось, пока я не увидел, где это указано в спецификации.   -  person cHao    schedule 04.09.2013
comment
@cHao Получение мьютекса для чтения переменной запрещает компилятору предполагать, что она не изменилась, поскольку мьютекс обеспечивает барьер памяти. Я не понимаю вашей точки зрения, поскольку даже если вы объявите переменную volatile, вам все равно понадобится какой-то мьютекс, потому что чтение и запись не являются атомарными. После того, как вы используете мьютекс, volatile бесполезен.   -  person Étienne    schedule 04.09.2013
comment
@ Этьен: До C11 ничего подобного не было. Сама концепция барьера памяти тогда еще не существовала в стандарте Си.   -  person cHao    schedule 04.09.2013
comment
@cHao: Мьютексы, реализующие барьеры памяти, существовали задолго до C11, например мьютексы posix: stackoverflow.com/questions/3208060/   -  person Étienne    schedule 04.09.2013
comment
@ Étienne: POSIX - это не C. Все, что он определяет, выходит за рамки того, что определяет C, и не связывает компилятор, который не заявляет о соответствии POSIX. В частности, он не связан ни с одним компилятором для ОС, отличной от POSIX, и не вызывает принудительную перезагрузку значения, если компилятор не обещает этого.   -  person cHao    schedule 04.09.2013
comment
позвольте нам продолжить обсуждение в чате   -  person Étienne    schedule 04.09.2013


Ответы (4)


Мой вопрос: видит ли threadB, что g_ptr изменен? Или он все еще видит, что указывает на NULL?

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

// Global variable
int global = 0;

// Thread 1 runs this code:
while (global == 0)
{
    // Do nothing
}

// Thread 2 at some point does this:
global = 1;

В этом случае компилятор может видеть, что global не изменяется внутри цикла while и не вызывает никаких внешних функций, поэтому он может «оптимизировать» его до чего-то вроде этого:

if (global == 0)
{
    while (1)
    {
        // Do nothing
    }
}

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

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

uint64_t *g_ptr = NULL;
pthread_mutex_t g_ptr_mutex = PTHREAD_MUTEX_INITIALIZER;

void modify_ptr(uint64_t **ptr, pthread_mutex_t *mutex)
{
    // Lock the mutex, assign the pointer to a new value, then unlock the mutex
    pthread_mutex_lock(mutex);
    *ptr = ...;
    pthread_mutex_unlock(mutex);
}

void read_from_addr()
{
    modify_ptr(&g_ptr, &g_ptr_mutex);
}

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

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

person Adam Rosenfield    schedule 03.09.2013
comment
+1 Хороший ответ, и спасибо, что, надеюсь, успокоили идеологов use-volatile-for-thread-synchro (по крайней мере, на данный момент, во всяком случае). - person WhozCraig; 04.09.2013
comment
@WhozCraig: Извини, я ненадолго уехал. : P Я не говорил использовать volatile для синхронизации. Всегда. Ни разу. Что я действительно сказал, так это использовать его, чтобы компилятор не стал слишком умным для его же блага. Даже в этом ответе признается, что он делает то же самое, и моя единственная претензия к нему (и все, кто повторяет линию партии volatile-is-the-devil) заключается в том, что ни один человек во всем культе еще не чтобы предоставить неопровержимые доказательства того, что мьютекс действительно выполняет (или даже обещает делать) работу volatile последовательно и надежно во всех случаях. - person cHao; 04.09.2013
comment
Я полностью согласен. У них две разные цели, простая и не очень простая. Есть вещи, которые volatile предназначены для приспособления. Так же, как есть вещи, для которых предназначены объекты синхронизации. Они делают разные вещи, и вы с такой же вероятностью увидите, как я говорю, что синхрообъекты не предназначены для этого; volatile - это когда ситуация требует этого, поскольку я не собираюсь использовать volatile для того, для чего предназначены объекты синхронизации. volatile, конечно, не дьявол, но он определенно может быть, если не используется для того, для чего был предназначен. = P. Точно так же наоборот. - person WhozCraig; 04.09.2013
comment
@WhozCraig: Также этот код можно скомпилировать так, чтобы ptr хранился в регистре. Я не вижу гарантии, что этого никогда не произойдет без volatile. Кстати. никто не сказал, что volatile используется для синхронизации, однако это предварительное условие для использования концепций синхронизации. - person bkausbk; 04.09.2013
comment
@bkausbk, и с этим мы официально достигли стратосферы 100 000 футов. Я написал слишком много многопоточного кода, даже не вставив volatile в исходный код, чтобы даже обсудить то, что в конечном итоге ни к чему не приведет. (это не значит, что я никогда не использовал volatile; я использовал; но не потому, что я боялся, что глобальная переменная будет укрыта в регистре). Я спишу это на удачу. Каждый раз (сейчас я иду покупать лотерейный билет). Я желаю вам всего наилучшего. - person WhozCraig; 04.09.2013
comment
@bkausbk: volatile не является предпосылкой для использования концепций синхронизации. Компилятору не разрешается кэшировать глобальную переменную в регистре при вызове не встроенной функции, например pthread_mutex_lock(), потому что он не имеет возможности узнать, может ли эта функция изменить глобальную переменную, поэтому он должен перезагрузить переменную из памяти. - person Adam Rosenfield; 04.09.2013
comment
@AdamRosenfield: Хорошо, если это верно, volatile в этом случае не потребуется. У вас есть официальная информация, где я могу это прочитать? - person bkausbk; 05.09.2013
comment
@bkausbk: См. стандарт языка C99: §5.1.2.3 для выполнения программы, §6.7.3 / 6 для volatile и приложение C для точек последовательности. Ключевой текст - §5.1.2.3 / 2, в котором говорится, что [...] изменение объекта [...] - все это побочные эффекты [...]. В [...] точках последовательности все побочные эффекты предыдущих оценок должны быть завершены, и никаких побочных эффектов последующих оценок не должно происходить. - person Adam Rosenfield; 05.09.2013
comment
@AdamRosenfield: Хорошо, но не похоже, что это как-то связано с моим вопросом. Вы сказали, что компилятору не разрешено кэшировать глобальную переменную в регистре при вызове невстраиваемой функции, или мне что-то не хватает. - person bkausbk; 06.09.2013
comment
@bkausbk: глобальная переменная - это объект. Если вы читаете эту переменную, вызываете функцию, которая изменяет эту переменную, а затем снова читаете эту переменную, абстрактная машина, указанная в стандарте языка C, сообщает, что когда вы читаете эту переменную во второй раз, она будет иметь значение, установленное в вызове функции. Соответствующая реализация не требуется для точной реализации абстрактной машины, но требуется, чтобы она соответствовала наблюдаемым побочным эффектам в точках последовательности. Следовательно, после вызова функции считанное значение должно быть самым последним записанным значением. - person Adam Rosenfield; 06.09.2013
comment
@bkausbk: (продолжение) Таким образом, компилятору не разрешено кэшировать глобальную переменную в регистре при вызове внешней функции (если функция находится в той же единице перевода, то компилятор может сделать вывод, что функция не может возможно изменить глобальную переменную), потому что это больше не будет соответствовать семантике абстрактной машины. - person Adam Rosenfield; 06.09.2013
comment
@AdamRosenfield: Что касается вашего утверждения о глобальных переменных: вы говорите, что глобальные переменные являются глобальными только в этом файле? А что будет, если у вас есть статическая глобальная переменная. Это глобально внутри процесса или только внутри существующего файла? Я не понимаю, что вы имели в виду, когда сказали, что соответствующая реализация не требуется для точной реализации абстрактной машины, но требуется, чтобы она соответствовала наблюдаемым побочным эффектам в точках последовательности. Спасибо! - person OfLettersAndNumbers; 09.10.2013

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

Но можно немного резюмировать. Глобальная переменная находится в области памяти, видимой для всех потоков. (Альтернативой является локальное хранилище потока, которое может видеть только один поток.) ​​Итак можно ожидать, что если у вас есть глобальная переменная G, и поток A записывает в нее значение x, то поток B увидит x при чтении этой переменной позже. И в общем, правда - в конце концов. Интересно то, что происходит до «в конце концов».

Самым большим источником хитростей являются согласованность памяти и согласованность памяти.

Согласованность описывает, что происходит, когда поток A записывает в G, а поток B пытается прочитать его почти в один и тот же момент. Представьте, что поток A и B находится на разных процессорах (давайте также назовем их A и B для простоты). Когда A записывает в переменную, между ним и памятью, которую видит поток B, существует множество схем. Во-первых, A, вероятно, напишет на количество сигналов, которые должны идти вперед и назад по проводам и конденсаторов и транзисторов и сложный диалог между кешем и основным блоком памяти. Между тем, у B есть собственный кеш. Когда изменения происходят в основной памяти, B может не сразу их увидеть, по крайней мере, до тех пор, пока не заполнит свой кэш из этой строки. И так далее. В общем, может пройти много микросекунд, прежде чем изменение потока A станет видимым для B.

Согласованность описывает, что происходит, когда A записывает в переменную G, а затем в переменную H. Если он считывает эти переменные, он увидит, что записи происходят в этом порядке. Но поток B может видеть их в другом порядке, в зависимости от того, очищается ли сначала H из кеша обратно в основную оперативную память. И что произойдет, если и A, и B одновременно пишут в G (по настенным часам), а затем попытаются прочитать ответ из Это? Какое значение они увидят?

Согласованность и согласованность обеспечиваются на многих процессорах с помощью операций барьер памяти. Например, PowerPC имеет синхронизацию код операции, который гласит: «Гарантия того, что любая запись, сделанная любым потоком в основную память, будет видна при любом чтении после этой операции синхронизации». (в основном это делается путем перепроверки каждой строки кэша относительно основной ОЗУ.) Архитектура Intel делает это до некоторой степени автоматически, если вы заранее предупредите его, что« эта операция затрагивает синхронизированную память ».

Тогда у вас возникнет проблема переупорядочения компилятора. Вот где код

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 предоставляет API доступа к заблокированной памяти, чтобы дать вам четкий способ обмена данными между потоками A и B. GCC пытается предоставить некоторые похожие функции. Строительные блоки потоковой передачи Intel предоставляют вам удобный интерфейс для платформ x86 / x64 и библиотека поддержки потоков C ++ 11 также предоставляет некоторые возможности.

person Crashworks    schedule 03.09.2013

Мой вопрос: видит ли threadB, что g_ptr изменен?

Наверное. g_ptr доступен threadB через read_from_addr(), поэтому все время виден один и тот же g_ptr. Это не имеет ничего общего с «внутримодульной глобальностью» g_ptr: он работал бы так же хорошо, если бы g_ptr был объявлен static и имел внутреннюю связь, поскольку, как вы написали здесь, он появляется в области видимости файла до read_from_addr().

Или он все еще видит, что указывает на NULL?

Возможно нет. Как только назначение выполнено, оно становится видимым для всех потоков.

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

Таким образом, вы действительно захотите использовать соответствующий примитив синхронизации потоков (например, блокировку чтения / записи или мьютекс), чтобы обеспечить хорошее поведение программы. В Linux с pthreads вы захотите посмотреть pthread_rwlock_* и pthread_mutex_*. Я знаю, что у других платформ есть эквиваленты, но понятия не имею, что они из себя представляют.

person Emmet    schedule 03.09.2013

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

Для Ex:

struct yalagur
{
char name [200];
int rollno;
struct yalagur * next;
} head;

int main ()
{
thread1 ();
thread2 ();
thread3 ();
}

Теперь указанная выше структура используется всеми потоками.

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

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

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

Спасибо Сада

person Sada    schedule 07.01.2014