Имеется несколько процессов, общие настройки между ними лежат POD-структуре в MMF. Один из процессов может читать и менять настройки, остальные — только читать.
Чтобы обойтись без лишних блокировок, и исходя из того, что настройки меняются крайне редко, в MMF также есть номер текущей версии настроек чтение и запись настроек проиходит следующим образом:
// Запись
Settings new_settings = {/*...*/};
InterlockedIncrement(&shared_data->version);
memcpy( &shared_data->settings, &new_settings , sizeof(Settings));
InterlockedIncrement(&shared_data->version);
// Чтение
LONG before, after;
Settings local_settings;
ReadSerrings:
before = InterlockedAdd(&shared_data->version, 0);
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));
after = InterlockedAdd(&shared_data->version, 0);
if (before != after) {
SwitchToThread();
goto ReadSettings;
}
Вопросы:
1. Это вообще работоспособно?
2. Как-то можно сделать, чтобы SwitchToThread с большей вероятностью делал что нужно? (т.е. активировал сохраняющий настройки процесс)
3. Что ещё можете посоветовать?
Русский военный корабль идёт ко дну!
Re: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
AG>Вопросы: AG>1. Это вообще работоспособно?
В таком виде нет.
Reader может прочитать оба значения "before" и "after" между инкрементами, которые делает writer.
В этом случае прочитанные значения будут равны, и reader может получить "битые" данные.
AG>2. Как-то можно сделать, чтобы SwitchToThread с большей вероятностью делал что нужно? (т.е. активировал сохраняющий настройки процесс)
Нет.
AG>3. Что ещё можете посоветовать?
Можно в MMF хранить две структуры, и флаг, который показывает, какая из структур активная.
Reader-ы читают из текущей активной структуры.
Writer пишет в неактивную структуру, и меняет флаг.
Надо обеспечить, чтобы все reader-ы закончили читать из структуры, которую собирается обновлять writer.
Если это происходит достаточно редко, можно на это забить.
В противном случае, жестко можно решить interlocked-счетчиком reader-ов.
Имею скафандр — готов путешествовать!
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, rus blood, Вы писали:
RB>Здравствуйте, Alexander G, Вы писали:
AG>>Вопросы: AG>>1. Это вообще работоспособно?
RB>В таком виде нет. RB>Reader может прочитать оба значения "before" и "after" между инкрементами, которые делает writer. RB>В этом случае прочитанные значения будут равны, и reader может получить "битые" данные.
Да, что-то я совсем туплю
AG>>2. Как-то можно сделать, чтобы SwitchToThread с большей вероятностью делал что нужно? (т.е. активировал сохраняющий настройки процесс)
RB>Нет.
AG>>3. Что ещё можете посоветовать?
RB>Можно в MMF хранить две структуры, и флаг, который показывает, какая из структур активная. RB>Reader-ы читают из текущей активной структуры. RB>Writer пишет в неактивную структуру, и меняет флаг.
RB>Надо обеспечить, чтобы все reader-ы закончили читать из структуры, которую собирается обновлять writer. RB>Если это происходит достаточно редко, можно на это забить. RB>В противном случае, жестко можно решить interlocked-счетчиком reader-ов.
Да, с intelocked счётчиком читателей мне нравится тем, что ждать будет писатель.
Русский военный корабль идёт ко дну!
Re: Межпроцессное разделение данных без блокировки
// Запись
Settings new_settings = {/*...*/};
for (;;)
{//ждемс другого писателя если надо
LONG new_before = InterlockedIncrement(&shared_data->version_before);
if (new_before == (shared_data->version_after+1)) break;
InterlockedDecrement(&shared_data->version_before);
SwitchToThread();
}
memcpy( &shared_data->settings, &new_settings , sizeof(Settings));
InterlockedIncrement(&shared_data->version_after);
// Чтение
Settings local_settings;
ReadSerrings:
for(;;)
{
LONG after = InterlockedCompareExchange(&shared_data->version_after, 0, 0);
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));
if (InterlockedCompareExchange(&shared_data->version_before, after, after)==after) break;
SwitchToThread();
}
Как много веселых ребят, и все делают велосипед...
Re[3]: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
AG>Да, с intelocked счётчиком читателей мне нравится тем, что ждать будет писатель.
А пока писатель будет ждать, придёт новый читатель, и ещё один, и ещё. По любому читателей придётся на входе тормазить. А учитывая, что обычно всё-таки важно побыстрее применить новые настройки, то логичнее всё-таки, на мой взгляд, тормозить читателей. Тем более, если изменение настроек происходят редко.
Как вариант на растерзание
// Запись
Settings new_settings = {/*...*/};
InterlockedIncrement(&shared_data->version);
memcpy( &shared_data->settings, &new_settings , sizeof(Settings));
InterlockedIncrement(&shared_data->version2);
// Чтение
LONG before, after;
Settings local_settings;
for(;;)
{
before = shared_data->version;
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));
after = shared_data->version;
if (before == after) break;
for(;;)
{
if (after == &shared_data->version2) break;
SwitchToThread();
}
}
"Нормальные герои всегда идут в обход!"
Re[4]: Межпроцессное разделение данных без блокировки
Здравствуйте, GlebZ, Вы писали:
GZ>А memcpy — атомарно?
Нет конечно.
GZ>Что будет если переключение потока произойдет когда происходит memcpy от писателя?
Дело даже не переключении как таковом, потоки могут просто работать одновременно.
Version до начала чтения и по его окончании будет разная. Нарвавшись на это мы ждём, когда писатель закончит запись и уравняет Version и Version2, после чего начинаем заново.
"Нормальные герои всегда идут в обход!"
Re[6]: Межпроцессное разделение данных без блокировки
Здравствуйте, Jolly Roger, Вы писали:
JR>Version до начала чтения и по его окончании будет разная. Нарвавшись на это мы ждём, когда писатель закончит запись и уравняет Version и Version2, после чего начинаем заново.
Или я что-то путаю, или это из кода это не явствует.
// Запись
Settings new_settings = {/*...*/};
InterlockedIncrement(&shared_data->version);
memcpy( &shared_data->settings, &new_settings , sizeof(Settings)); //в процессе произошла переключение к читателю
// Чтение
//сюда переключили
before = shared_data->version;//читаем уже новую версию
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));//получаем полубитые данные
after = shared_data->version; //читаем ту же версию (новую)if (before == after) break; //а причем тут version 2
Re: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
AG>Имеется несколько процессов, общие настройки между ними лежат POD-структуре в MMF. Один из процессов может читать и менять настройки, остальные — только читать.
AG>Чтобы обойтись без лишних блокировок, и исходя из того, что настройки меняются крайне редко, в MMF также есть номер текущей версии настроек чтение и запись настроек проиходит следующим образом:
Если настройки меняются крайне редко, то использование read/write lock'ов даст вам ничуть не худшее решение этой проблемы, и при том простое и понятное.
Их, правда, нет в готовом виде в Win32 API до Висты, но их можно сделать из подручного хлама из других примитивов синхронизации, и есть некоторое количество готовых реализаций в разных библиотеках.
Re[7]: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
AG>Имеется несколько процессов, общие настройки между ними лежат POD-структуре в MMF. Один из процессов может читать и менять настройки, остальные — только читать.
Тут есть еще одна подводная скала(или грабли). Как то нас шибануло. Когда процесс меняет настройки, то он должен это делать на основе прочитанных данных. Следовательно, между чтением и записью одного такого процесса не должно быть никаких записей другого процесса.
Re: Межпроцессное разделение данных без блокировки
Господа начинающие системщики!
Посмотрите на этот пример, он весьма наглядно демонстрирует не-системный подход к задаче, равно как и недостаток знаний. Давайте посмотрим, что хочет автор этого топика:
Один писатель, много читателей.
Целостность данных, полученных каждым читателем.
Во-первых, писатель у нас один, это уже большой плюс, т.к. не придётся их синхронизировать. Но есть ещё проблема с читателями. Хорошо, смотрим далее. На слове "целостность" мы уже должны насторожиться. Лучше всего в этот момент будет вспомнить понятие "транзакция", которая есть суть следующее:
Операция, которая выполняется целиком и успешно, или не выполняется совсем.
Заметьте — "выполняется целиком". Ага, т.е. целостность нам гарантируется. Подумаем, как мы могли бы применить транзакции в данном случае. Нам нужно, чтобы какое-то время, пока писатель делает свою работу, данные не были прочитаны. И прерывать работу читателей тоже нельзя. Отлично, в таком случае во время записи данных пусть читатели работают с той же копией данных, а писать в это время пишет куда-то ещё. Второй буфер, грубо говоря. По окончании записи писатель меняет данные местами. Это всё на интерлокерах можно сделать, никто никого не ждёт. Читателям только смотреть нужно будет, в какой из буфер писали последний раз, флажок там какой-нибудь. Вот и всё. Похожий принцип и в графике, например, применяется, называется swap buffers.
Да, если писателей будет больше одного, то достаточно сделать по два буфера и по одному флажку на каждого из них, а принцип тот же.
P.S.
Сорри за многа букаф, просто показалось, что тут нужно расписать чуть подробнее.
JID: x64j@jabber.ru
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, x64, Вы писали:
x64>Заметьте — "выполняется целиком". Ага, т.е. целостность нам гарантируется. Подумаем, как мы могли бы применить транзакции в данном случае. Нам нужно, чтобы какое-то время, пока писатель делает свою работу, данные не были прочитаны. И прерывать работу читателей тоже нельзя. Отлично, в таком случае во время записи данных пусть читатели работают с той же копией данных, а писать в это время пишет куда-то ещё. Второй буфер, грубо говоря. По окончании записи писатель меняет данные местами.
Меняет, я так понимаю, индексы буферов, т.к. сами буфера одной операцией не поменяешь?
Допустим, писатеь только что записал буфер, в момент, когда читеталь читал другой буфер. Теперь, читатель так и не получил управления, а писатель снова что-то записал, в другой буфер, т.е. в читаемый, и данные не целостны?
Русский военный корабль идёт ко дну!
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, Pzz, Вы писали:
Pzz>Если настройки меняются крайне редко, то использование read/write lock'ов даст вам ничуть не худшее решение этой проблемы, и при том простое и понятное.
Pzz>Их, правда, нет в готовом виде в Win32 API до Висты, но их можно сделать из подручного хлама из других примитивов синхронизации, и есть некоторое количество готовых реализаций в разных библиотеках.
Вистовский подошел бы, если бы там были потоки, а не процессы.
Русский военный корабль идёт ко дну!
Re[3]: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
Pzz>>Их, правда, нет в готовом виде в Win32 API до Висты, но их можно сделать из подручного хлама из других примитивов синхронизации, и есть некоторое количество готовых реализаций в разных библиотеках.
AG>Вистовский подошел бы, если бы там были потоки, а не процессы.
Да, правда.
Но как бы то ни было, сделать rwlock из подручного хлама несложно. В т.ч. из того, что есть в любой разумной версии венды.
Re[3]: Межпроцессное разделение данных без блокировки
x64>>Заметьте — "выполняется целиком". Ага, т.е. целостность нам гарантируется. Подумаем, как мы могли бы применить транзакции в данном случае. Нам нужно, чтобы какое-то время, пока писатель делает свою работу, данные не были прочитаны. И прерывать работу читателей тоже нельзя. Отлично, в таком случае во время записи данных пусть читатели работают с той же копией данных, а писать в это время пишет куда-то ещё. Второй буфер, грубо говоря. По окончании записи писатель меняет данные местами.
AG>Меняет, я так понимаю, индексы буферов, т.к. сами буфера одной операцией не поменяешь? AG>Допустим, писатеь только что записал буфер, в момент, когда читеталь читал другой буфер. Теперь, читатель так и не получил управления, а писатель снова что-то записал, в другой буфер, т.е. в читаемый, и данные не целостны?
Да, в таком виде это не работает. Налицо не системный подход
По существу вопроса.
Рекомендую поискать по словам lock-free. Практически все из того, что будет найдено, при некотором дотачивании напильником (а иногда и без этой процедуры) вам подойдет.
AG>Меняет, я так понимаю, индексы буферов, т.к. сами буфера одной операцией не поменяешь?
Да.
AG>Допустим, писатеь только что записал буфер, в момент, когда читеталь читал другой буфер. Теперь, читатель так и не получил управления, а писатель снова что-то записал, в другой буфер, т.е. в читаемый, и данные не целостны?
Да, ты прав.
Вот здесь, например, есть обсуждение проблемы, раздел "Retiring Old Data" и далее.
JID: x64j@jabber.ru
Re: Межпроцессное разделение данных без блокировки
Здравствуйте, Andrew S, Вы писали:
AS>Рекомендую поискать по словам lock-free. Практически все из того, что будет найдено, при некотором дотачивании напильником (а иногда и без этой процедуры) вам подойдет.
AS>Например, вот: AS>http://rsdn.ru/forum/cpp/1800649.aspx
Там замечательный пример, но он базируется на возможности создать неограниченное число копий, а MMF не резиновый. Любое же заранее заданное число всегда оставляет риск нарваться на ситуацию, когда этого числа не хватит. Можно конечно "забить", и сказать : "у меня писать будут редко, поэтому пула из 10(100, 1000,...) буферов мне хватит всегда."
Но гораздо интереснее попытаться изобрести собственный велосипед, пусть даже ошибочный. Ведь опыт — он, как сказал Классик, любимый сынок ошибок трудных и не очень, а опыт, он лишним никому не будет, ни начинающим системщикам, ни даже заканчивающим
В голову приходит проверка хэш-подписи. То есть смотрим версию в MMF, если старая — работаем с локальной копией, если нет — читаем новые и считаем хэш. Не совпал — перечитываем. Но как-то это "топорно", по-моему, да и полной гарантии не даёт из-за неуникальности хэша.
"Нормальные герои всегда идут в обход!"
Re[4]: Межпроцессное разделение данных без блокировки
Здравствуйте, Jolly Roger, Вы писали:
JR>А пока писатель будет ждать, придёт новый читатель, и ещё один, и ещё. По любому читателей придётся на входе тормазить.
Когда придет новый читатель, он будет использовать другую структуру, не в ту, в которую собирается использовать писатель (потому что писатель пишет в неактивную структуру, а читатель использует активную структуру). И писателю не придется ждать нового читателя, а только тех, которые уже начали использовать его структуру (в которую он писал на пред-предыдущем шаге). И происходить такое будет только если писателю надо часто менять данные.
С учетом это фразы
настройки меняются крайне редко
можно вообще забить на синхронизацию.
Например, писатель пишет timestamp в структуру.
Перед записью новых данных писатель ждет определенного timeout-а на основе timestamp его новой структуры.
Имею скафандр — готов путешествовать!
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, GlebZ, Вы писали:
GZ>Здравствуйте, Alexander G, Вы писали:
AG>>Имеется несколько процессов, общие настройки между ними лежат POD-структуре в MMF. Один из процессов может читать и менять настройки, остальные — только читать. GZ>Тут есть еще одна подводная скала(или грабли). Как то нас шибануло. Когда процесс меняет настройки, то он должен это делать на основе прочитанных данных. Следовательно, между чтением и записью одного такого процесса не должно быть никаких записей другого процесса.
Здесь только один писатель, такой проблемы не будет.
Имею скафандр — готов путешествовать!
Re[3]: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
AG>Меняет, я так понимаю, индексы буферов, т.к. сами буфера одной операцией не поменяешь? AG>Допустим, писатеь только что записал буфер, в момент, когда читеталь читал другой буфер. Теперь, читатель так и не получил управления, а писатель снова что-то записал, в другой буфер, т.е. в читаемый, и данные не целостны?
Ты сам написал, что данные меняются "крайне редко".
Вероятность, что какой-то читатель не закончил с позапрошлым буфером, будет мала.
Чтобы совсем исключить столкновение, можно добавить счетчики читателей.
Причем, значение счетчика той структуры, которую ждет писатель, будет только уменьшаться, если будет ненулевое.
Но счетчики мне не нравятся, т.к. это лишние interlocked-операции. Если данные меняются крайне редко, можно сделать ожидание писателем определенного timeout-а с момента последней записи в ту структуру, в которую он собирается записывать новые данные. Т.е. с момента позапрошлого обновления данных.
А зачем усложнять ? Такой код малопонятен и не дает никакого выигрыша, поскольку настройки все равно
меняются редко. К тому же его труднее сопровождать. На мой взгляд, решение с обычными блокировками
лучше соответствует сути задачи.
Re: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали: AG>3. Что ещё можете посоветовать?
Засинхронизировать доступ к шаренным данным с помощью примитивов синхронизации и не морочить себе голову (вы ж не embedded утройства программируете, правда? sync-примитивы на целевой платформе имеются, не так ли?)
Например при одном писателе, редких записях, прорве читателей и частых чтениях прекрасно подойдут boost::shared_mutex и его типы блокировок:
boost::shared_mutex syncObj;
// Читаем данные:
// читатели друг дружку не ждут,
// впадают в ожидание, если данные собираются быть захвачены
// или уже захвачены писателем
{
boost::shared_lock<boost::shared_mutex> lockObj(syncObj);
// TODO
}
// Пишем данные:
// писатель ждёт всех читателей, кто ещё не успел закончить работу
// (в это время блокируется захват примитива "новыми читателями"),
// после чего захватывает окончательно и начинает писать.
{
boost::upgrade_lock<boost::shared_mutex> lockObj(syncObj);
boost::upgrade_to_unique_lock<boost::shared_mutex> lockObj2(lockObj);
// TODO
}
"Дайте мне возможность выпускать и контролировать деньги в государстве и – мне нет дела до того, кто пишет его законы." (c) Мейер Ансельм Ротшильд , банкир.
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, okman, Вы писали:
O>А зачем усложнять ? Такой код малопонятен и не дает никакого выигрыша, поскольку настройки все равно O>меняются редко. К тому же его труднее сопровождать. На мой взгляд, решение с обычными блокировками O>лучше соответствует сути задачи.
Выигрыш в том, что меняются настройки очень редко, но читаются они очень часто. Но наверное так и сделаю: счётчик версии настроек, но дальше — блокировка.
Русский военный корабль идёт ко дну!
Re[3]: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали:
AG>Выигрыш в том, что меняются настройки очень редко, но читаются они очень часто. Но наверное так и сделаю: счётчик версии настроек, но дальше — блокировка.
Причем шаги W3 и W6 прошли между R1 и R3.
Это значит, что шаги W4 и W5 попали внутрь R1 и R3.
Единственная возможность читателю прочитать одинаковое значение counter на шагах R2 и R4 — это выполнение шагов W4 и W5 до шага R2.
Но это означает, что писатель закончил обновление данных до того, как читатель их прочитал.
В любом другом случае значение counter на шаге R4 будет отличаться от прочитанного на шаге R2 и читатель пойдет на второй круг.
Имею скафандр — готов путешествовать!
Re[3]: Межпроцессное разделение данных без блокировки
Здравствуйте, Alexander G, Вы писали: AG>boost::shared_mutex не работает для межпроцессной синхронизации
Попробуйте named_upgradable_mutex
"Дайте мне возможность выпускать и контролировать деньги в государстве и – мне нет дела до того, кто пишет его законы." (c) Мейер Ансельм Ротшильд , банкир.
Re[5]: Межпроцессное разделение данных без блокировки
AS>>Рекомендую поискать по словам lock-free. Практически все из того, что будет найдено, при некотором дотачивании напильником (а иногда и без этой процедуры) вам подойдет.
AS>>Например, вот: AS>>http://rsdn.ru/forum/cpp/1800649.aspx
JR>Там замечательный пример, но он базируется на возможности создать неограниченное число копий, а MMF не резиновый. Любое же заранее заданное число всегда оставляет риск нарваться на ситуацию, когда этого числа не хватит. Можно конечно "забить", и сказать : "у меня писать будут редко, поэтому пула из 10(100, 1000,...) буферов мне хватит всегда."
Есть примеры очередей и без копирования.
Кроме того, в boost::interprocess есть как именованые примитивы синхронизации, так и возможность создать оные на основе MMF.
JR>Но гораздо интереснее попытаться изобрести собственный велосипед, пусть даже ошибочный. Ведь опыт — он, как сказал Классик, любимый сынок ошибок трудных и не очень, а опыт, он лишним никому не будет, ни начинающим системщикам, ни даже заканчивающим
Опыт, он, конечно, не лишний. Но прежде чем изобретать такие сложные вещи, как примитивы синхронизации, необходимо четко изучить то, что уже сделано в этой области.
Прошу прощения если поздно но обычно решал эту проблему одним указателем, объектом с автореференсом и локом
лок используется для блокировки кода писателя, если вдруг писателей несколько.
template<class _T>
class TMultiReaderAndOneWriterLock {
protected:
CCriticalSection m_Sync ; // protect for multi writersvolatile _T* m_ActiveObject ; // pointer to active data, must be alligned to CPU wordpublic:
void Write( _T* obj ) { // writer put new data
m_Sync.Enter() ;
_T* old = m_ActiveObject ;
obj->AddRef() ;
m_ActiveObject = obj ;
m_Sync.Leave() ;
old->Release() ;
}
// read current data
// NOTES: Reader MUST release data after use it
// Reader can use data as long as needed,
// but better practice is to release it asapinline _T* Read() {
_T* result = m_ActiveObject ;
result->AddRef() ;
return( result ) ;
}
} ;
AddRef() и Release() — обязательно должны использовать атомарные операции инкременда и декремента.
ЗЫ. Писал по памяти, кода под рукой нету, прошу прощения за неточности
ЗЫЫ. Я знаю что данная схема не всегда приминима, но для ПС где мы можем позволить себе копии данных это работает причем если реально один писатель то обходимся без вызова системых функций и переключения контекстов.
ЗЫЫЫ. А адаптировать ее для шаред памяти можно без проблем. Перетащи темплейт в структуру и выдели тройку блоков под инстанцы объекта. Три блока: один активный, один ждет когда ридеры его отрелизят, а один в который писатель пишет.
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, Pzz, Вы писали:
Pzz>Если настройки меняются крайне редко, то использование read/write lock'ов даст вам ничуть не худшее решение этой проблемы, и при том простое и понятное.
Принципиально худшее и полностью не масштабируемое. read/write lock НЕ обеспечивает параллельную работу нескольких читателей в общем случае, он обеспечивает более-менее параллельную работу только если размер критической секции для читателей составляет десятки микросекунд. Если же секция маленькая (что обычно имеет место быть), то read/write lock не только не обеспечивает физически параллельной работы, то и получается медленнее обычного лока. Основная его проблема, что читатели получаются вовсе не читатели, а писатели — пишут во внутренние данные лока. А записи а разделяемые данные очень медленные и убивают масштабируемость на корню. На физическом уровне нельзя писать в одну ячейку памяти параллельно, все записи сериализуются аппаратным обеспечением, отсюда и проблемы.
Когда я тестировал истинный (физический) доступ на чтение против доступа, который пишет в разделяемые данные, на 4-ёх ядерном процессоре, я получал разницу в 300 раз. При чём чем больше ядер/процессоров, тем эта разница увеличивается, причём драматически. Если бы речь шла о многопроцессорной системе, то разница была бы более чем в 1000 раз.
Доступ на чтение — это очень хорошая вещь в многопоточном окружении, и очень желательно реализовывать его именно как доступ на чтение на физическом уровне, без каких-либо записей в разделяемые данные. Это ключ к линейной масштабируемости.
Здравствуйте, x64, Вы писали:
x64>Заметьте — "выполняется целиком". Ага, т.е. целостность нам гарантируется. Подумаем, как мы могли бы применить транзакции в данном случае. Нам нужно, чтобы какое-то время, пока писатель делает свою работу, данные не были прочитаны. И прерывать работу читателей тоже нельзя. Отлично, в таком случае во время записи данных пусть читатели работают с той же копией данных, а писать в это время пишет куда-то ещё. Второй буфер, грубо говоря. По окончании записи писатель меняет данные местами. Это всё на интерлокерах можно сделать, никто никого не ждёт. Читателям только смотреть нужно будет, в какой из буфер писали последний раз, флажок там какой-нибудь. Вот и всё. Похожий принцип и в графике, например, применяется, называется swap buffers.
Здравствуйте, x64, Вы писали:
AG>>Допустим, писатеь только что записал буфер, в момент, когда читеталь читал другой буфер. Теперь, читатель так и не получил управления, а писатель снова что-то записал, в другой буфер, т.е. в читаемый, и данные не целостны?
x64>Да, ты прав. x64>Вот здесь, например, есть обсуждение проблемы, раздел "Retiring Old Data" и далее.
На всякий случай, SMR (Hazard pointers) запатентована Sun Microsystems (ныне Oracle) на территории США и возможно других стран, и не может там свободно использоваться.
Здравствуйте, Jolly Roger, Вы писали:
AS>>Рекомендую поискать по словам lock-free. Практически все из того, что будет найдено, при некотором дотачивании напильником (а иногда и без этой процедуры) вам подойдет.
AS>>Например, вот: AS>>http://rsdn.ru/forum/cpp/1800649.aspx
JR>Там замечательный пример, но он базируется на возможности создать неограниченное число копий, а MMF не резиновый. Любое же заранее заданное число всегда оставляет риск нарваться на ситуацию, когда этого числа не хватит. Можно конечно "забить", и сказать : "у меня писать будут редко, поэтому пула из 10(100, 1000,...) буферов мне хватит всегда."
В целом, я его делал именно для такой ситуации — хранение редко изменяемых объектов.
Да, этот алгоритм потенциально использует неограниченное число копий. Теоретически, его можно изменить под использование не более N копий (тогда, кстати, не нужен будет DWCAS). Но в целом я бы не рекомендовал его использовать в данной ситуации, тут действительно лучше подойдёт оригинальный вариант из первого поста ветки (немного подправленный), более подробно в отдельном сообщении.
Здравствуйте, rus blood, Вы писали:
RB>Здравствуйте, rus blood, Вы писали:
RB>Наоборот, конечно...
RB>>
RB> #LoadLaod
RB>> }
RB>> while (!(flag == mmf->flag && counter == data->counter)); // сначала - flag, потом - counter
RB>>}
RB>>
И там и там:
do
{
flag = mmf->flag;
#LoadLaod
data = &mmf->data[flag];
counter = data->counter;
#LoadLaod
.... // читаем#LoadLaod
}
while (counter == data->counter);
Два считывания data->counter обрамляют критическую секцию, чтения данные не должны уходить из неё ни вверх, ни вниз.
Плюс я бы ограничился проверкой только "counter == data->counter".
Здравствуйте, Rakafon, Вы писали:
R>Засинхронизировать доступ к шаренным данным с помощью примитивов синхронизации и не морочить себе голову (вы ж не embedded утройства программируете, правда? sync-примитивы на целевой платформе имеются, не так ли?)
Проблема только что деградирует в сотни и тысячи раз. И чем больше ядер/процессоров — тем хуже. Масштабируемость — отрицательная.
G6> // read current data
G6> // NOTES: Reader MUST release data after use it
G6> // Reader can use data as long as needed,
G6> // but better practice is to release it asap
G6> inline _T* Read() {
G6> _T* result = m_ActiveObject ;
А если вот тут result заменят и удалят?
G6> result->AddRef() ;
G6> return( result ) ;
G6> }
G6>} ;
G6>
Здравствуйте, Alexander G, Вы писали:
AG>Имеется несколько процессов, общие настройки между ними лежат POD-структуре в MMF. Один из процессов может читать и менять настройки, остальные — только читать. AG>Вопросы: AG>1. Это вообще работоспособно? AG>2. Как-то можно сделать, чтобы SwitchToThread с большей вероятностью делал что нужно? (т.е. активировал сохраняющий настройки процесс) AG>3. Что ещё можете посоветовать?
Вполне работоспособно (после небольших правок) и называется SeqLock. Используется в ядре Linux как раз для таких ситуаций, в частности при чтении 64-битных таймстемпов на архитектурах, где не поддерживаются атомарные 64-битные чтения — использовать тут спинлок было бы убийством. За гарантированно работающей реализацией смотри ./kernel/linux/seqlock.h
В целом SeqLock — очень хороший алгоритм (если применим), т.к. сочетает в себе относительную просто реализацию + идеальную масштабируемость (доступ на чтение остаётся доступом на чтение на физическом уровне). А применим он, если структура полностью самодостаточная (не содержит указателей на внешние данные), и читатели могут пережить чтение неконсистентных данных (после которого, естественно, будет повторное чтение, но это чтение должно по-крайней мере закончиться без зависаний и падений).
Единственное, что структуры может быть не 2, а произвольное число — в зависимости от частоты записи, и размера структуры. В данном случае, я думаю, вполне хватит двух объектов.
Кроме этого ничего советовать не буду. Если данные можно копировать memcpy'ем, и это не обходится очень дорого, то это — идеальный вариант.
Единственное, в контексте меж-процессного взаимодействия есть проблема поддержания консистентности данных при смерти пишущего процесса. Можно решить следующим образом.
Заводишь именованный мьютекс для писателя, при старте процесса он захватывает этот мьютекс, при выходе — освобождает. Мьютекс не используется для обеспечения консистентности данных, он используется только для определения смерти процесса. Если процесс при захвате мьютекса получает код, что предыдущий процесс умер насильственной смертью, то он проходит по всем объектам и инкрементирует нечётные счётчики. На первый взгляд, этого должно хватить.
Про reader-writer мьютексы не слушай. Они не обеспечивают параллельной работы читателей и не масштабируются. Да и robust реализацию его сделать не так-то и просто.
Вот код SeqLock из ядра 2.6.31:
typedef struct seqcount {
unsigned sequence;
} seqcount_t;
#define seqcount_init(x) do { *(x) = (seqcount_t) SEQCNT_ZERO; } while (0)
/* Start of read using pointer to a sequence counter only. */static inline unsigned read_seqcount_begin(const seqcount_t *s)
{
unsigned ret;
repeat:
ret = s->sequence;
smp_rmb();
if (unlikely(ret & 1)) {
cpu_relax();
goto repeat;
}
return ret;
}
/*
* Test if reader processed invalid data because sequence number has changed.
*/static inline int read_seqcount_retry(const seqcount_t *s, unsigned start)
{
smp_rmb();
return s->sequence != start;
}
/*
* Sequence counter only version assumes that callers are using their
* own mutexing.
*/static inline void write_seqcount_begin(seqcount_t *s)
{
s->sequence++;
smp_wmb();
}
static inline void write_seqcount_end(seqcount_t *s)
{
smp_wmb();
s->sequence++;
}
Здравствуйте, remark, Вы писали:
R>Вполне работоспособно (после небольших правок) и называется SeqLock. Используется в ядре Linux как раз для таких ситуаций, в частности при чтении 64-битных таймстемпов на архитектурах, где не поддерживаются атомарные 64-битные чтения — использовать тут спинлок было бы убийством. За гарантированно работающей реализацией смотри ./kernel/linux/seqlock.h
Суть исправления только в этом? :
// Чтение
LONG before, after;
Settings local_settings;
ReadSettings:
before = InterlockedAdd(&shared_data->version, 0);
if (before % 2) {
SwitchToThread();
goto ReadSettings;
}
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));
after = InterlockedAdd(&shared_data->version, 0);
if (before != after) {
SwitchToThread();
goto ReadSettings;
}
R>Единственное, что структуры может быть не 2, а произвольное число — в зависимости от частоты записи, и размера структуры. В данном случае, я думаю, вполне хватит двух объектов.
Я ещё подумаю, я что-то плохо понимаю его.
R>Единственное, в контексте меж-процессного взаимодействия есть проблема поддержания консистентности данных при смерти пишущего процесса. Можно решить следующим образом. R>Заводишь именованный мьютекс для писателя, при старте процесса он захватывает этот мьютекс, при выходе — освобождает. Мьютекс не используется для обеспечения консистентности данных, он используется только для определения смерти процесса. Если процесс при захвате мьютекса получает код, что предыдущий процесс умер насильственной смертью, то он проходит по всем объектам и инкрементирует нечётные счётчики. На первый взгляд, этого должно хватить.
так можно?
// Чтение
...
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));
after = InterlockedAdd(&shared_data->version, 0);
if (before != after) {
if (after % 2)
{
// главный начал и не окончил запись
DWORD code = 0;
VERIFY(GetExitCodeProcess(main_process_handle, &code));
if (code != STILL_ACTIVE) {
// главный умер, больше делать нечего
memset(&local_settings, 0, sizeof(Settings));
goto NothingMoreToDo;
}
// попробуем дать главному дописать
SwitchToThread();
}
goto ReadSettings;
}
R>
Русский военный корабль идёт ко дну!
Re[2]: Межпроцессное разделение данных без блокировки
Здравствуйте, remark, Вы писали:
R>Единственное, что структуры может быть не 2, а произвольное число — в зависимости от частоты записи, и размера структуры. В данном случае, я думаю, вполне хватит двух объектов.
От лишних обозримого вреда нет?
У меня данные всё равно на MMF, поэтому меньше чем размер страницы там не выделишь, а настроек там байт 20.
Русский военный корабль идёт ко дну!
Re[3]: Межпроцессное разделение данных без блокировки
Забыл упомянуть — никогда не надо читать данные с помощью InterlockedXXX, это превращает их в записи, а записи не масштабируются.
Плюс я бы не делал лишние SwitchToThread().
Это для MSVC/x86, предполагается, что shared_data->version объявлена volatile:
// Чтение
LONG before, after;
Settings local_settings;
ReadSettings:
before = shared_data->version;
if (before % 2) {
SwitchToThread();
goto ReadSettings;
}
memcpy(&local_settings, &shared_data->settings, sizeof(Settings));
_ReadWriteBarrier();
after = shared_data->version;
if (before != after) {
if (after % 2)
SwitchToThread();
goto ReadSettings;
}
R>>Единственное, что структуры может быть не 2, а произвольное число — в зависимости от частоты записи, и размера структуры. В данном случае, я думаю, вполне хватит двух объектов.
AG>Я ещё подумаю, я что-то плохо понимаю его.
Если коротко, то там примерно так же как и у тебя, за исключением следующего.
Объекта 2. Один из них является "текущим" для чтения. Писатель всегда пишет не в "текущий", и после записи делает его "текущим". Т.о. у нас всегда есть хотя бы 1 консистентный объект для чтения, и если читатель увидел неконсистентный объект (version % 2 != 0), то значит, что он читает не тот объект, который надо. Соотв. он просто перечитывает какой объект текущий и повторяет.
В твоём же варианте, если писатель прерван по время записи, все читатели будут заблокированы на это время (объект только один, и он неконсистентный, делать нечего — приходится ждать).