Следующие указатели в многопоточной среде - PullRequest
6 голосов
/ 14 января 2011

Если у меня есть код, который выглядит примерно так:

typedef struct {
    bool some_flag;

    pthread_cond_t  c;
    pthread_mutex_t m;
} foo_t;

// I assume the mutex has already been locked, and will be unlocked
// some time after this function returns. For clarity. Definitely not
// out of laziness ;)
void check_flag(foo_t* f) {
    while(f->flag)
        pthread_cond_wait(&f->c, &f->m);
}

Есть ли что-то в стандарте C, мешающее оптимизатору переписать check_flag как:

void check_flag(foo_t* f) {
    bool cache = f->flag;
    while(cache)
        pthread_cond_wait(&f->c, &f->m);
}

Другими словами,сгенерированный код имеет , чтобы каждый раз следовать по указателю f через цикл, или компилятор свободен вытаскивать разыменование?

Если равно свободно вытащить его, есть ли способ предотвратить это?Нужно ли где-нибудь посыпать изменчивое ключевое слово?Это не может быть параметр check_flag, потому что я планирую иметь другие переменные в этой структуре, которые я не возражаю против оптимизации компилятора следующим образом.

Могу ли я прибегнуть к:

void check_flag(foo_t* f) {
    volatile bool* cache = &f->some_flag;
    while(*cache)
        pthread_cond_wait(&f->c, &f->m);
}

Ответы [ 4 ]

7 голосов
/ 14 января 2011

В общем случае, даже если многопоточность не была задействована и ваш цикл выглядел следующим образом:

void check_flag(foo_t* f) {
    while(f->flag)
        foo(&f->c, &f->m);
}

, компилятор не сможет кэшировать тест f->flag.Это потому, что компилятор не может знать, может ли функция (например, foo() выше) изменить любой объект, на который указывает f.

При особых обстоятельствах (foo() виден компилятору,и все указатели, переданные в check_flag(), как известно, не являются псевдонимами или иным образом модифицируемыми foo()), компилятор может оптимизировать проверку.

Однако pthread_cond_wait() должен быть реализован таким образом, чтобыэто предотвратит эту оптимизацию.

См. Гарантирует ли защита переменной с помощью мьютекса pthread, что она также не кэшируется? :

Возможно, вас также заинтересует ответ Стива Джессопа на: Может ли компилятор C / C ++ легально кэшировать переменную в регистре при вызове библиотеки pthread?

Но как далеко вы хотите заняться проблемами, поднятыми в статье Бёма, в своей работе?вам решать.Насколько я могу судить, если вы хотите принять точку зрения, что pthreads не дает / не может дать гарантию, то вы, в сущности, занимаетесь тем, что pthreads бесполезна (или, по крайней мере, не дает никаких гарантий безопасности, чтоЯ думаю, что сокращение имеет тот же результат).Хотя это может быть правдой в самом строгом смысле (как указано в документе), это также, вероятно, не является полезным ответом.Я не уверен, какой вариант у вас будет, кроме pthreads на Unix-платформах.

3 голосов
/ 14 января 2011

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

Попытка обойти библиотеку - это плохо, но становится хуже. Возможно, читая статью Ханса Бёма на общую тему из PLDI 2005 («Потоки не могут быть реализованы как библиотека») или многие из его последующих статей (которые приводят к работе на пересмотренной модели памяти C ++) заставит вас бояться Бога и направит вас обратно к прямому и узкому:).

3 голосов
/ 14 января 2011

Обычно вы должны попытаться заблокировать мьютекс pthread перед ожиданием объекта условия, поскольку вызов pthread_cond_wait освобождает мьютекс (и повторно запрашивает его перед возвратом). Таким образом, ваша check_flag функция должна быть переписана так, чтобы соответствовать семантике в условии pthread.

void check_flag(foo_t* f) {
    pthread_mutex_lock(&f->m);
    while(f->flag)
        pthread_cond_wait(&f->c, &f->m);
    pthread_mutex_unlock(&f->m);
}

Относительно вопроса о том, разрешено ли компилятору оптимизировать чтение поля flag, этот ответ объясняет это более подробно, чем я.

По сути, компилятор знает о семантике pthread_cond_wait, pthread_mutex_lock и pthread_mutex_unlock. Он знает, что не может оптимизировать чтение памяти в этих ситуациях (вызов pthread_cond_wait в этом примере). Здесь нет понятия о барьере памяти, просто есть специальное знание определенной функции и какое-то правило, которому нужно следовать в их присутствии.

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

1 голос
/ 19 января 2011

Летучий для этой цели. Полагаться на то, что компилятор знает о методах кодирования pthread, кажется мне немного чокнутым; компиляторы довольно умны в наши дни. Фактически, компилятор, вероятно, видит, что вы зацикливаетесь на проверке переменной, и по этой причине не кеширует ее в регистре, а не потому, что видит, что вы используете pthreads. Просто используйте volatile, если вам действительно все равно.

Какая-то забавная маленькая записка. У нас есть VOLATILE #define, который является либо volatile (когда мы думаем, что ошибка не может быть нашим кодом ...), либо пустым. Когда мы думаем, что у нас происходит сбой из-за того, что оптимизатор убивает нас, мы # определяем его как «изменчивый», который ставит изменчивый перед почти всем. Затем мы проверяем, уходит ли проблема. Пока что ... ошибки были разработчиком, а не компилятором! кто бы мог подумать !? Мы разработали высокопроизводительную библиотеку потоков «без блокировки» и «без блокировки». У нас есть тестовая платформа, которая забивает ее до тысячи гонок в секунду. Таким образом, мы никогда не обнаруживали проблему, требующую волатильности! До сих пор gcc никогда не кэшировал общую переменную в регистре. да ... мы тоже удивлены. Мы все еще ждем нашего шанса использовать volatile!

...