Можно ли безопасно разделить целое число между потоками? - PullRequest
7 голосов
/ 04 января 2011

Есть ли проблема с несколькими потоками, использующими одно и то же целочисленное расположение памяти между pthreads в программе на C без каких-либо утилит синхронизации?

Чтобы упростить проблему,

  • Только один поток будет писать в целое число
  • Несколько потоков будут читать целое число

Этопсевдо-C иллюстрирует то, что я думаю

void thread_main(int *a) {
  //wait for something to finish
  //dereference 'a', make decision based on its value
}

int value = 0;

for (int i=0; i<10; i++)
  pthread_create(NULL,NULL,thread_main,&value);
}
// do something
value = 1;

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

Ответы [ 7 ]

12 голосов
/ 04 января 2011

Ваш псевдокод НЕ является безопасным.

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

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

Вам нужен какой-то забор памяти для предотвращения переупорядочения записи.В противном случае не гарантируется, что другие потоки когда-либо увидят новое значение.

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

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

0 голосов
/ 14 апреля 2011

EDIT: Бен прав (и я идиот, говоря, что это не так), что есть вероятность, что процессор переупорядочит инструкции и выполнит их одновременно по нескольким конвейерам. Это означает, что значение = 1 может быть установлено до того, как конвейер выполнит «работу». В свою защиту (не полный идиот?) Я никогда не видел, чтобы это произошло в реальной жизни, и у нас есть обширная библиотека потоков, и мы проводим исчерпывающие долгосрочные тесты, и этот шаблон используется повсеместно. Я бы видел это, если бы это происходило, но ни один из наших тестов никогда не дал сбой или дал бы неправильный ответ. Но ... Бен прав, возможность существует. Это, вероятно, происходит все время в нашем коде, но переупорядочение не устанавливает флаги достаточно рано, чтобы потребители данных, защищенных флагами, могли использовать данные до его завершения. Я буду изменять наш код, чтобы включить барьеры, потому что нет никакой гарантии, что это продолжит работать в дикой природе. Я считаю, что правильное решение похоже на это:

Темы, которые читают значение:

...
if (value)
{
  __sync_synchronize();  // don't pipeline any of the work until after checking value
  DoSomething();
}
...

Поток, который устанавливает значение:

...
DoStuff()
__sync_synchronize();  // Don't pipeline "setting value" until after finishing stuff
value = 1;  // Stuff Done
...

При этом я обнаружил, что это является простым объяснением барьеров.

БАРЬЕР КОМПИЛЕРА Барьеры памяти влияют на процессор. Барьеры компилятора влияют на компилятор. Volatile не будет удерживать компилятор от переупорядочения кода. Здесь для получения дополнительной информации.

Я полагаю, что вы можете использовать этот код, чтобы gcc не переставлял код во время компиляции:

#define COMPILER_BARRIER() __asm__ __volatile__ ("" ::: "memory")

Так, может быть, именно это и следует сделать?

#define GENERAL_BARRIER() do { COMPILER_BARRIER(); __sync_synchronize(); } while(0)

Темы, которые читают значение:

...
if (value)
{
  GENERAL_BARRIER();  // don't pipeline any of the work until after checking value
  DoSomething();
}
...

Поток, который устанавливает значение:

...
DoStuff()
GENERAL_BARRIER();  // Don't pipeline "setting value" until after finishing stuff
value = 1;  // Stuff Done
...

Использование GENERAL_BARRIER () не позволяет gcc переупорядочить код, а также не позволяет процессору переупорядочивать код. Теперь мне интересно, не будет ли gcc переупорядочивать код поверх встроенного барьера памяти, __sync_synchronize (), что сделает использование COMPILER_BARRIER избыточным.

X86 Как указывает Бен, разные архитектуры имеют разные правила относительно того, как они переставляют код в конвейерах выполнения. Intel кажется довольно консервативным. Таким образом, барьеры могут не потребоваться почти столько же от Intel. Тем не менее, это не очень хорошая причина избегать барьеров, поскольку это может измениться.

ОРИГИНАЛЬНЫЙ ПОЧТА: Мы делаем это все время. это совершенно безопасно (не для всех ситуаций, но много). Наше приложение работает на 1000 серверов в огромной ферме с 16 экземплярами на сервер, и у нас нет условий гонки. Вы правильно задаетесь вопросом, почему люди используют мьютексы для защиты уже атомарных операций. Во многих ситуациях замок - пустая трата времени. Чтение и запись в 32-битные целые числа на большинстве архитектур является атомарным. Не пытайтесь делать это с 32-битными битовыми полями!

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

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

Компилятор может изменить порядок кода в функции, если он знает, что конечный результат не будет отличаться.Я не видел, чтобы компилятор делал это с глобальными переменными, потому что компилятор понятия не имеет, как изменение порядка глобальной переменной повлияет на код вне непосредственной функции.

Если функция работает, вы можете контролировать уровень оптимизации на уровне функции, используя __attrribute __.

Теперь, если вы используете этот флаг в качестве шлюза, чтобы разрешить только один потокгруппы, выполняющей какую-либо работу, , которая не будет работать .Пример: поток A и поток B оба могут прочитать флаг.Поток A запланирован.Поток B устанавливает флаг в 1 и начинает работать.Поток A просыпается, устанавливает флаг в 1 и начинает работать.По электронной почте Ой!Чтобы избежать блокировок и все-таки делать что-то подобное, вам нужно изучить атомарные операции, в частности gcc atomic builtins , например __sync_bool_compare_and_swap (значение, старое, новое).Это позволяет вам установить значение = новое, если значение в настоящее время старое.В предыдущем примере, если значение = 1, только один поток (A или B) мог выполнить __sync_bool_compare_and_swap (& value, 1, 2) и изменить значение с 1 на 2. Потерянный поток потерпит неудачу.__sync_bool_compare_and_swap возвращает успешное завершение операции.

В глубине есть «блокировка» при использовании атомарных встроенных функций, но это аппаратная инструкция и очень быстрая по сравнению с использованием мьютексов.

Тем не менее, используйте мьютексы, когда вам нужно изменить много значений одновременно.атомарные операции (на сегодняшний день) работают только тогда, когда все данные, которые должны изменяться атомарно, могут помещаться в непрерывные 8, 16, 32, 64 или 128 бит.

0 голосов
/ 04 января 2011

В любой момент вы должны хотя бы объявить общую переменную volatile.Однако вы должны во всех случаях предпочитать какую-либо другую форму IPC или синхронизации потоков;в данном случае это выглядит так: условная переменная - это то, что вам действительно нужно.

0 голосов
/ 04 января 2011

Предположим, что первое, что вы делаете в потоке func, когда спите на секунду. Таким образом, значение после этого будет определенно 1.

0 голосов
/ 04 января 2011

Хм, я думаю, что это безопасно, но почему бы вам просто не объявить функцию, которая возвращает значение другим потокам, поскольку они будут только читать его?

Потому что простая идея передачи указателейпо моему скромному мнению, разделение потоков - это уже сбой в системе безопасности.Я говорю вам: зачем давать (модифицируемый, общедоступный) целочисленный адрес, когда вам нужно только значение?

0 голосов
/ 04 января 2011

Я бы на это не рассчитывал. Компилятор может генерировать код, который предполагает, что он знает, какое значение 'value' есть в любой момент времени в регистре ЦП, без повторной загрузки его из памяти.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...