синхронизация потоков - деликатный вопрос

пусть у меня есть этот цикл:

static a;
for (static int i=0; i<10; i++)
{
   a++;
   ///// point A
}

в эту петлю входит 2 нити...

я не уверен в чем-то.... что произойдет, если thread1 попадет в POINT A, оставайтесь там, пока THREAD2 входит в цикл 10 раз, но после 10-го цикла после увеличения значения i до 10, перед проверкой значение i, если оно меньше 10, Thread1 выходит из цикла и предполагает увеличить i и снова войти в цикл. какое значение будет увеличивать Thread1 (которое я увижу)? будет 10 или 0?

возможно ли, что Thread1 увеличит i до 1, а затем поток 2 снова попадет в цикл 9 раз (а их может быть 8, 7 и т. д.)

Благодарность


person Idan    schedule 17.12.2009    source источник
comment
Я бы не стал касаться таких вопросов с 6-футовым шестом.   -  person shoosh    schedule 17.12.2009
comment
Это просто неправильно по всем мыслимым причинам. Не делай этого.   -  person DrPizza    schedule 17.12.2009
comment
Нет. Это не вопрос? Какой ответ. См. выше. С ног на голову.   -  person reechard    schedule 11.01.2010


Ответы (11)


Вы должны понимать, что операция приращения на самом деле:

read the value
add 1
write the value back

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

static int a = 0;

thread 1 reads a (0)
adds 1 (value is 1)
thread 2 reads a (0)
adds 1 (value is 1)
thread 1 writes (1)
thread 2 writes (1)

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

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

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

person Dov    schedule 17.12.2009

Если i совместно используется несколькими потоками, все ставки сняты. Любой поток может увеличить i практически в любой момент во время выполнения другого потока (в том числе в середине операции увеличения этого потока). Нет смысла рассуждать о содержимом i в приведенном выше коде. Не делай этого. Либо дайте каждому потоку собственную копию i, либо сделайте приращение и сравнение с 10 одной атомарной операцией.

person moonshadow    schedule 17.12.2009
comment
Также возможно, что приращения будут потеряны из-за того, что они будут перезаписаны другим потоком. - person Ben S; 17.12.2009
comment
@Pierre: да, вы сделали приращение и сравнение атомарной операцией, как я и предлагал :) - person moonshadow; 17.12.2009
comment
Почему все ставки отключены? Если есть гонка данных, вы можете делать ставки. Если нет расы, вы не можете. - person R. Martinho Fernandes; 17.12.2009
comment
А кто сказал, что int можно читать или писать атомарно? Что, если машина 64-разрядная (данные) и на ней работает 32-разрядная виртуальная машина? - person reechard; 11.01.2010

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

person Dynite    schedule 17.12.2009

Я просто буду использовать i++ в вашем цикле:

for (static int i=0; i<10; i++)
{
}

Потому что он имитирует a. (Обратите внимание, static здесь очень странно)

Подумайте, приостанавливается ли поток A, как только он достигает i++. Поток B получает i вплоть до 9, переходит в i++ и делает его 10. Если бы ему нужно было двигаться дальше, петля существовала бы. Ах, но теперь Тема А возобновлена! Таким образом, он продолжает с того места, на котором остановился: приращение i! Итак, i становится 11, и ваш цикл прерывается.

Каждый раз, когда потоки обмениваются данными, они должны быть защищены. Вы также можете сделать так, чтобы i++ и i < 10 происходили атомарно (никогда не прерывались), если ваша платформа поддерживает это.

person GManNickG    schedule 17.12.2009
comment
Боже, что это за беспорядок. Можем ли мы попросить ученика обратиться к учителю за особой помощью? Могу ли я получить звук зуммера? ^G^G^G^G^G^G^G^G^G :) - person reechard; 11.01.2010

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

person Ben S    schedule 17.12.2009

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

В вашем случае вы должны написать:

bool test_increment(int& i)
{
  lock()
  ++i;
  bool result = i < 10;
  unlock();
  return result;
}

static a;
for(static int i = -1 ; test_increment(i) ; )
{
   ++a;
   // Point A
}

Теперь проблема исчезает. Обратите внимание, что lock() и unlock() должны блокировать и разблокировать мьютекс, общий для всех потоков, пытающихся получить доступ к i!

person PierreBdR    schedule 17.12.2009
comment
Окончательно! Спасибо, Майк. Есть volatile и есть sig_atomic_t, но я бы все равно скептически отнесся к гарантии атомарности. - person reechard; 11.01.2010

Да, возможно, что любой поток может выполнять большую часть работы в этом цикле. Но, как объяснил Dynite, это никогда не будет (и не должно) отображаться в реальном коде. Если синхронизация является проблемой, вы должны обеспечить мьютекс взаимного исключения (Boost, pthread или Windows Thread), чтобы предотвратить такие условия гонки.

person psublue    schedule 17.12.2009

Зачем вам использовать статический счетчик циклов?

Это пахнет домашней работой, и плохой.

person rmn    schedule 17.12.2009

Оба потока имеют свою собственную копию i, поэтому поведение может быть любым. Это одна из причин, почему это такая проблема.

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

И кто-то, несомненно, укажет, что «volatile не имеет смысла в многопоточности!» но люди говорят много глупостей. Вам не обязательно иметь volatile, но это полезно для некоторых вещей.

person Charles Eli Cheese    schedule 18.12.2009

Если ваш «int» не является размером слова атомной машины (например, 64-битный адрес + данные, эмулирующие 32-битную виртуальную машину), вы будете "word-tear". В этом случае ваш «int» равен 32 битам, но машина адресует 64 атомарно. Теперь вам нужно прочитать все 64, увеличить половину и записать все обратно.

Это гораздо более серьезная проблема; узнайте больше о наборах инструкций процессора и grep gcc о том, как он реализует «volatile» везде, если вам действительно нужны кровавые подробности.

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

person Community    schedule 10.01.2010

Если вам нужно одновременно увеличить значение с несколькими потоками, найдите «атомарные операции». Для Linux найдите «атомарные операции gcc». На большинстве платформ имеется аппаратная поддержка для атомарного увеличения, добавления, сравнения и замены и т. д. БЛОКИРОВКА БЫЛА БЫ ИЗБЫТОЧНОЙ для этого .... atomic inc на величину быстрее, чем lock inc unlock. Если вам нужно изменить много полей одновременно, вам может понадобиться блокировка, хотя вы можете изменить 128-битные поля за раз с помощью большинства атомарных операций.

volatile — это не то же самое, что атомарная операция. Volatile помогает компилятору понять, когда использовать копию переменной — плохая идея. Среди его применений volatile важно, когда у вас есть несколько потоков, изменяющих данные, которые вы хотели бы прочитать «самую последнюю версию» без блокировки. Volatile по-прежнему не решит вашу проблему с a ++, поскольку два потока могут одновременно считывать значение «a», а затем оба увеличивают одно и то же «a», а затем выигрывает последний, написавший «a», и вы потеряли inc. Volatile замедлит оптимизированный код, не позволяя компилятору хранить значения в регистрах, а что нет.

person johnnycrash    schedule 07.01.2011