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

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

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);
}

person Clark Gaebel    schedule 13.01.2011    source источник
comment
+1 за то, что подумал о такого рода проблемах, прежде чем писать многопоточный код методом проб и ошибок!   -  person R.. GitHub STOP HELPING ICE    schedule 14.01.2011


Ответы (4)


Обычно вы должны попытаться заблокировать мьютекс 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, это answer объясняет это более подробно, чем я могу.

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

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

person Sylvain Defresne    schedule 14.01.2011
comment
Означает ли это, что компилятор не может кэшировать значение p->some_flag в регистре? Я не уверен в значении барьера памяти. Не могли бы их немного объяснить? - person Clark Gaebel; 14.01.2011
comment
Неспособность компилятора кэшировать значение не имеет ничего общего с барьерами памяти или потоками; это просто следствие того факта, что вызов может изменить флаг. С точки зрения компилятора, это фактический вызов pthread_cond_wait, а не какой-либо другой поток, который мог изменить флаг. - person R.. GitHub STOP HELPING ICE; 14.01.2011
comment
В основном, компилятор знает семантику pthread_cond_wait, pthread_mutex_lock и pthread_mutex_unlock. Не могли бы вы упомянуть один компилятор, который знает? Готов поспорить, большинство компиляторов понятия не имеют о семантике pthread_cond_wait, pthread_mutex_lock и pthread_mutex_unlock. - person curiousguy; 02.10.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.

person Michael Burr    schedule 14.01.2011
comment
Хотел бы я принять два ответа, но не могу. Я просто выбрал ту, у которой меньше всего повторений. Хотя они были одинаково полезны! - person Clark Gaebel; 14.01.2011
comment
Это лучший ответ, но мне нравится, почему OP согласился с другим. :-) Как бы то ни было, функции синхронизации потоков указаны как полные барьеры памяти. Как это реализовано, приложение не касается; это просто гарантированно работает. - person R.. GitHub STOP HELPING ICE; 14.01.2011
comment
pthread_cond_wait () должен быть реализован таким образом, чтобы предотвратить эту оптимизацию. Мне любопытно, как можно разумно реализовать pthread_cond_wait таким образом, чтобы разрешить (неправильную) оптимизацию! - person curiousguy; 02.10.2011

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

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

person EmeryBerger    schedule 14.01.2011
comment
Я не могу читать эту газету, не заплатив денег. Я слишком беден, чтобы учиться = / - person Clark Gaebel; 14.01.2011
comment
Альтернативная бесплатная ссылка на технический отчет: hpl.hp.com/ techreports / 2004 / HPL-2004-209.html - person EmeryBerger; 14.01.2011
comment
Спасибо за ссылку. Я люблю читать статьи Ганса. Хотя они слишком веселые. Я потеряю час своей жизни. - person Clark Gaebel; 14.01.2011

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

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

person johnnycrash    schedule 19.01.2011
comment
компиляторы в наши дни довольно умны. Когда компилятор предполагал, что вызов функции ни на что не влияет? - person curiousguy; 02.10.2011