Исходная ситуация: значение переменной Int не определено, значение переменной Bool = false
Поток1 пишет новое значение в переменную Int, а затем в переменную Bool значение true.
Поток2 читает переменную Bool, и если она True, то читает переменную Int.
Вопрос: может ли Поток2 быть уверен в том, что если переменная Bool == true, то переменная Int гарантированно содержит то значение, которое записал Поток1. Или на многопроцессорных системах это не гарантированно?
Здравствуйте, Aniskin, Вы писали:
A>Исходная ситуация: значение переменной Int не определено, значение переменной Bool = false A>Поток1 пишет новое значение в переменную Int, а затем в переменную Bool значение true. A>Поток2 читает переменную Bool, и если она True, то читает переменную Int.
A>Вопрос: может ли Поток2 быть уверен в том, что если переменная Bool == true, то переменная Int гарантированно содержит то значение, которое записал Поток1. Или на многопроцессорных системах это не гарантированно?
Если это был весь сценарий то, да. А для общих случаев — нет.
Здравствуйте, Aniskin, Вы писали:
A>Вопрос: может ли Поток2 быть уверен в том, что если переменная Bool == true, то переменная Int гарантированно содержит то значение, которое записал Поток1. Или на многопроцессорных системах это не гарантированно?
Здравствуйте, Aniskin, Вы писали:
A>Здравствуйте, Qulac, Вы писали:
Q>>Если это был весь сценарий то, да. А для общих случаев — нет.
A>Спасибо. А можно пару слов об общих случаях?
Если есть третий пишущий поток, пока поток2 шел от bool к int он мог int уже переписать и там будет уже не то значение, что записал поток1
Здравствуйте, Aniskin, Вы писали:
A>Вопрос: может ли Поток2 быть уверен в том, что если переменная Bool == true, то переменная Int гарантированно содержит то значение, которое записал Поток1. Или на многопроцессорных системах это не гарантированно?
В общем случае зависит от предоставленных гарантий — модели памяти (happens before)
Т.е. конкретный ответ зависит от того, что ты называешь потоком.
Например, компилятор запросто может поменять два действия, выполненные в рамках одного потока (установка int и bool), т.к. они не зависят друг от друга.
Здравствуйте, Буравчик, Вы писали:
Б>В общем случае зависит от предоставленных гарантий — модели памяти (happens before) Б>Т.е. конкретный ответ зависит от того, что ты называешь потоком.
Потоком я называю поток исполнения, создаваемый CreateThread. Или я не правильно понял комментарий?
Б>Например, компилятор запросто может поменять два действия, выполненные в рамках одного потока (установка int и bool), т.к. они не зависят друг от друга.
Будем считать, что компилятор в моем случае не меняет местами установку int и bool. Мой вопрос можно свести к вопросу, а может ли bool записаться в память раньше чем int с точки зрения потока2.
Здравствуйте, Aniskin, Вы писали:
A>Будем считать, что компилятор в моем случае не меняет местами установку int и bool. Мой вопрос можно свести к вопросу, а может ли bool записаться в память раньше чем int с точки зрения потока2.
Здравствуйте, Pzz, Вы писали:
A>>Будем считать, что компилятор в моем случае не меняет местами установку int и bool. Мой вопрос можно свести к вопросу, а может ли bool записаться в память раньше чем int с точки зрения потока2.
Pzz>Может.
Понятно. Правильно ли я понимаю, что использование любого примитива синхронизации заставит CPU сбросить все кеши, и устранит неоднозначность моей ситуации?
Здравствуйте, Aniskin, Вы писали:
Pzz>>Может.
A>Понятно. Правильно ли я понимаю, что использование любого примитива синхронизации заставит CPU сбросить все кеши, и устранит неоднозначность моей ситуации?
У тебя есть две переменные, и ты хочешь, чтобы работа с ними из двух потоков была консистентной. Проще всего, действительно, использовать примитивы синхронизации:
mutex m;
int i;
bool b;
void set_i (int val) {
lock(m);
i = val;
b = true;
unlock(m);
}
int get_i (void) {
int val;
lock(m);
if (b) {
val = i;
} else {
val = 5; // Why not?
}
unlock(m);
return val;
}
Но это дорого (хотя далеко не всегда эта дороговизна имеет значение) и, возможно, тебе столько гарантий не надо, сколько ты получишь, от мьютекса (хорошо оплатив эти гаранзии ценой самого мьютекса).
Возможно, тебе хватили бы atimic-ов. Но atomic-и сложны для понимания, а мьютексы просты. Кроме того, атомики тоже отнюдь не бесплатны.
В общем, лучше отталкиваться от проблемы, которую ты пытаешься решить, а не от имеющегося низкоуровнего API. Если, конечно, твоя проблема не заключается в знакомстве с этим API, как таковым.
P.S. Кеши CPU тут вообще не при чем. На x86 синхронизация кешей происходит автоматически. Даже и между разными ядрами одноко процессора. И даже если в материнскую плату вставлено несколько процессоров (такое бывает в природе, но редко встречается на дектопе), между ними сихнронизация кешей тоже происходит автоматически.
Но процессор вправе исполнять инструкции не в том порядке, в каком они написаны в программе, а как ему удобнее — при условии, что суть не меняется. И это реально случается, и дело не в кешах, а в том, что обращение к памяти — это долгая асинхронная операция. И пока она идет, процессор вправе заняться чем-нибудь еще.
При этом в понятие "суть не меняется" не входят гарантии, что глядя с другого ядра на порядок операций с памятью, мы увидим такой же порядок, в каком они были запущены на текущем ядре. Чтобы на это повлиять, существуют барьеры памяти, они есть сами по себе и в том или ином виде встроены в атомики и в примитивы синхронизации.
Здравствуйте, Pzz, Вы писали:
Pzz>В общем, лучше отталкиваться от проблемы, которую ты пытаешься решить
Есть список элементов, с каждым элементом ассоциированы данные. Данных изначально нет, получение данных является тяжелой операций, но выполняется она один раз отдельно для каждого элемента. Поэтому данные я получаю только при обращении к ним. Обращение к элементам и данным происходит из разных потоков. У каждого элемента есть bool-поле HasData, определяющие то, получены данные для элемента, или нет. Изначально я хотел сделать так:
1) читатель проверяет HasData,
2) и если равно true, то читатель просто забирает данные, (что бы не входить в критическую секцию)
3) а если равно false, то читатель заходит в критическую секцию, снова проверяет HasData, при необходимости получает данные и устанавливает HasData в true, после чего забирает данные и выходит из секции.
Но у меня возникли сомнения относительно корректности пунктов 1-2. Поскольку вы пояснили мне, что так делать нельзя, то я убираю проверку HasData вне секции, и потоки всегда будут начинать с критической секции.
Здравствуйте, Aniskin, Вы писали:
A>1) читатель проверяет HasData, A>2) и если равно true, то читатель просто забирает данные, (что бы не входить в критическую секцию) A>3) а если равно false, то читатель заходит в критическую секцию, снова проверяет HasData, при необходимости получает данные и устанавливает HasData в true, после чего забирает данные и выходит из секции.
Я бы сделал HasData atomic-ом, и спал спокойно.
A>Но у меня возникли сомнения относительно корректности пунктов 1-2. Поскольку вы пояснили мне, что так делать нельзя, то я убираю проверку HasData вне секции, и потоки всегда будут начинать с критической секции.
Возможно, на общем фоне выигрыш от избежания вхождения в критическую секцию столь невелик, что оно не стоит возросшей сложности. Есть смысл сначала убедиться в том, что захват/освобождение критической секции создают сколь заметную нагрузку по сравнению со всем прочим.
Еще один момент, если обращений на чтение много а на запись — не очень, выглядит так, что rwlock может быть уместнее простого мьютекса.
Здравствуйте, Pzz, Вы писали:
Pzz>Я бы сделал HasData atomic-ом, и спал спокойно.
Т.е. если поток1 устанавливает Int, а затем делает InterlockedIncrement(HasData), то в потоке2 я могу проверять HasData != 0 и считать, что Int гарантированно корректен?
Здравствуйте, Aniskin, Вы писали:
A>Исходная ситуация: значение переменной Int не определено, значение переменной Bool = false A>Поток1 пишет новое значение в переменную Int, а затем в переменную Bool значение true. A>Поток2 читает переменную Bool, и если она True, то читает переменную Int.
A>Вопрос: может ли Поток2 быть уверен в том, что если переменная Bool == true, то переменная Int гарантированно содержит то значение, которое записал Поток1. Или на многопроцессорных системах это не гарантированно?
Нет не может. Т.к. если ни на разных ядрах то пока буфер записи не попал в кэш второй поток с другого ядра ничего не увидит. Для решения есть специальные команды барьеры.
Здравствуйте, Aniskin, Вы писали:
A>Здравствуйте, Pzz, Вы писали:
Pzz>>Я бы сделал HasData atomic-ом, и спал спокойно.
A>Т.е. если поток1 устанавливает Int, а затем делает InterlockedIncrement(HasData), то в потоке2 я могу проверять HasData != 0 и считать, что Int гарантированно корректен?
Здравствуйте, kov_serg, Вы писали:
_>Нет не может. Т.к. если ни на разных ядрах то пока буфер записи не попал в кэш второй поток с другого ядра ничего не увидит. Для решения есть специальные команды барьеры.
Тут вот такой нюанс, невредный для понимания: на x86 кеши синхронизируются автоматически. Даже когда не то, что ядер, физических процессоров больше одной штуки.
Но команды не обязаны исполняться в том порядке, в котором они написаны в программе. В т.ч., команды, модифицирующие память.
При этом любое ядро само с собой self-consistent. Но не обязательно консистентно с другими ядрами. Впрочем, в user space поток может в любое время сам собой переехать на другое ядро, так что для user-space кода это не очень важное замечание.
Здравствуйте, Pzz, Вы писали:
Pzz>Тут вот такой нюанс, невредный для понимания: на x86 кеши синхронизируются автоматически. Даже когда не то, что ядер, физических процессоров больше одной штуки.
x86 выполняют команды не по порядку и данные при записи не сразу попадают в кэш, там есть промежуточный буфер.
Здравствуйте, Aniskin, Вы писали:
A>Вопрос тот же. Этот код обеспечивает когерентность HasData, но обеспечивает ли он когерентность data?
Да, за счет InterlockedXXX, которые подразумевают внутри себя барьеры памяти и мьютекса, который гарантирует, что данные и HasData изменяются консистентно.