2013-09-03 4 views
5

У глобальных указателей есть область видимости между потоками?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 и выполняет change_ptr (& g_ptr), а также read_from_addr(). И тогда threadB запускается, и он запускается через file2.c, выполняя функцию2A().

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

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

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

+0

вам нужно будет объявить указатель как 'volatile' увидеть немедленные обновления в различных потоках на указателе – thumbmunkeys

+0

два слова:«синхронизация»и«'volatile'». – cHao

+0

@Joe: Однако он предотвращает оптимизацию, которая будет кэшировать значение и повторно использовать его, не проверяя его снова. Это важная часть видимости нового значения. – cHao

ответ

7

Мой вопрос: Does 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 здесь только замедлит вашу программу малыми путями и замаскирует реальную проблему - отсутствие правильной синхронизации.

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

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) Вы можете также использовать специализированные безблокировочные структуры данных, но это продвинутая техника, и очень легко ошибиться

+3

+1 Хороший ответ, и спасибо вам за то, (во всяком случае, на данный момент, по крайней мере, сейчас). – WhozCraig

+1

@WhozCraig: Извините, я был далеко. : P Я не сказал использовать 'volatile' для синхронизации. Когда-либо. Ни разу. То, что я сказал, должно было использовать его, чтобы компилятор не становился слишком умным для своего же блага. Даже этот ответ допускает, что он делает столько же, и моя единственная говядина с ним (и все, кто попугаирует линию волатильности-дьявола) - это то, что * ни один человек во всем культе * еще не дал убедительных доказательств того, что мьютекс фактически выполняет (или даже обещает сделать) работу 'volatile' последовательно и надежно во всех случаях. – cHao

+1

Я полностью согласен. У них две разные цели, простые и не простые. Есть вещи 'volatile' *, предназначенные для размещения. Точно так же, как объекты синхронизации предназначены для размещения. Они делают разные вещи, и вы будете так же склонны видеть, что я говорю: «Синхро-объекты - это * не *, предназначенные для этого, - это volatile», когда ситуация требует его, поскольку я не хочу использовать volatile для каких объектов синхронизации предназначены для. неустойчивый, конечно, не дьявол, но он, безусловно, может быть, если не использоваться для того, что он предназначался. = Р. Точно так же наоборот. – WhozCraig

0

Моим вопрос: Есть ли 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_*. Я знаю, что на других платформах есть эквиваленты, но я понятия не имею, что это такое.

4

Этот вопрос является примером учебника о том, что затрудняет параллельное программирование. Действительно полное объяснение 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.

+1

+1 Хорошая запись. – WhozCraig

-1

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

Для Ex:

структура yalagur
{
имя символ [200];
int rollno;
struct yalagur * next;
} head;

INT Основной()
{
thread1();
thread2();
thread3();
}

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

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

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

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

Благодаря Sada

Смежные вопросы