[параллель. прог] синхронизация много reader-ов, мало writer-ов.

Julie16

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

bobby

тебе надо именно на мьютексах?
а то есть pthread_rwlock.

enochka1145

// Уря, design patterns!
То, что ты описал, соответствует шаблону "Read/Write Lock" . Могу привести код на Java, а лучше поищи где-нибудь в Google.

Julie16

О, это то что нужно. Я сегодня узнал что-то новое Спасибо.

Julie16

Приведи код, если не сложно

bobby

в сырцах mysql (mysys/thr_rwlock.c) есть реализация через 1 mutex и 2 cond'а =)
thr_rwlock.c можно посмотреть в =)

kamputer

Всё подробно разжёвано, например, в книжке J. Richter "Advanced Windows"
Есть в локалке в электронном виде, с примерами кода.

Julie16

Блин, судя по коду это не то что мне нужно. В этом случае читающие треды могут блокироваться. А это совсем не то что нужно.

int my_rw_rdlock(rw_lock_t *rwp)
{
pthread_mutex_lock(&rwp->lock);

bobby

а, ну юзай pthread_rwlock =)
я особо не вчитывался просто =)

Julie16

PS: в nptl(pthreads) примерно то же самое, с точностью до названий переменных Так что это не то что нужно...
PPS: так что вопрос остается открытым.

bobby

так там же лок краткосрочный для увеличения разделяемого ресурса (количества читателей)
по-моему, все отлично =)

Julie16

Блин. Так и я придумал Мне нужно чтобы читающие треды вообще не блокировались. Понимаешь, это не проблема, когда тредов - 5. А когда их 1000 - это уже может быть проблемой.

rosali

Я в этих мутексах не очень силен, но как тебе такой план.
1. Читающие и Пишущий лочатся на общем мутексе.
2. Читающие кешируют прочитанное.
3. Валидность кеша определяется по атомарному разделяемому ресурсу. Если я правильно все понимаю, атомарный ресурс не требует синхронизации.

Julie16

Не, не пойдет. Читающие не могут кешировать прочитанное, так как оно всегда разное.

Makc500

А чо, этого чуве не котируется?

rosali

Ну если так часто обновляется, то пиши просто в разные места. То есть имеется массифчик, и пишуший пишет сначала в первый элемент потом во второй, ... где-то хранится, какой элемент сейчас последний, читающие читают оттуда, синхронизоваться не надо, потому что в _этот_ элемент читающий больше ничего не запишет. А когда массив кончится,... ну это редко, придумай что нибудь.

lordik72

Вот здесь в главе 7.1.2 есть хороший пример о rwlock

Julie16

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

VOID CSWMRG::WaitToRead {

// Ensure exclusive access to the member variables
EnterCriticalSection(&m_cs);

kamputer

>А чо, совет этого чуве не котируется?
Там тоже используется кратковременная блокировка.
У месье редкостные по извращённости запросы.

Julie16

Блин. Спасибо конечно, но ты прочел тред перед тем как отвечать? Мне это не подходит, уже выяснили. Мне нужно чтобы читающие треды ВООБЩЕ не блокировались.

lordik72

нет ломало

Julie16

И чем же мои запросы так извращены? Нормальные запросы, ИМХО. Судя по всему мне придется остановиться на первоначальной схеме.

Julie16

В принципе так конечно можно сделать, но для этого придется сильно ломать структуру программы. Так как ресурс - это не просто кусок памяти. Это сложный класс, который очень накладно копировать.

daru

Так как ресурс - это не просто кусок памяти. Это сложный класс
Может быть тогда не всем читающим нужен он весь одновременно? Равно как и всем пишущим?
Разбить на более мелкие части, которые каждая защищаются отдельно?
ЗЫ А без блокировки (хотя бы очень краткосрочной) читающий поток не реализовать, потому что по крайней мере он должен проверить, что сейчас над данными не глумится пишущий.
ЗЫЫ Как вариант, заведи смарт-указатели (a la CComPtr) на защищаемый объект. Заведи helper-объект с двумя методами: "получить указатель на защищаемый объект"и "установить новый указатель на объект". Блокировка нужна только в момент замещения указателя внутри helper'а
При желании на x86 можно скопировать указатели с помощью Interlocked-функций - это весьма быстро. Получится транзакционный механизм в духе Oracle :-)

Julie16

Разбить на более мелкие части, которые каждая защищаются отдельно?
Это невозможно да и не нужно.
ЗЫ А без блокировки (хотя бы очень краткосрочной) читающий поток не реализовать, потому что по крайней мере он должен проверить, что сейчас над данными не глумится пишущий.
Конечно не реализовать без блокировки. Но эту штуку можно реализовать без ВЗАИМОБЛОКИРОВКИ. Разницу чувствуешь?
ЗЫЫ Как вариант, заведи смарт-указатели (a la CComPtr) на защищаемый объект. Заведи helper-объект с двумя методами: "получить указатель на защищаемый объект"и "установить новый указатель на объект". Блокировка нужна только в момент замещения указателя внутри helper'а
При желании на x86 можно скопировать указатели с помощью Interlocked-функций - это весьма быстро. Получится транзакционный механизм в духе Oracle :-)
Не имеет отношения к задаче.

daru

Это невозможно да и не нужно... Не имеет отношения к задаче
жираф большой...
Но эту штуку можно реализовать без ВЗАИМОБЛОКИРОВКИ. Разницу чувствуешь?
Как же-с не чувствовать! Чувствую, конечно. Взаимоблокировка - это deadlock по-аглицки. Без него действительно желательно обходиться :-
Нет, эту штуку можно реализовать без синхронизации читающих потоков, только если каждый из них будет синхронизироваться с пишущими потоками сам. Т.е. всё сводится к пресловутым N (ну или f(N) - для всяких компромиссных вариантов) объектам синхронизации, которые ты отмел еще в первом посте.

Dasar

> А когда их 1000 - это уже может быть проблемой.
Если треды живые - то больше 10 смысла заводит их нет, т.к. производительность редко падает.
Если треды мертвые - то ожидание на lock-ах будет много меньше, чем работа самих тредов.

bastii

а может у него 10 процов

bleyman

System.Threading.ReaderWriterLock
Defines a lock that supports single writers and multiple readers.
Use .NET, fella =)
А вообще оно элементарно реализуется через один инт и мутекс.
изначально в инте ReaderCount + 1
Ридер локает мутекс и вычитает единицу
Райтер вычитает локает мутекс и вычитает ReaderCount
если в результате операции получается 0 или меньше, то всё возвращаем на место и спим некоторое время. Повторяем.
Иначе анлокаем мутекс и делаем то что нужно.
Потом локаем, прибавляем сколько вычли, анлокаем.
А ещё я точно помню, что где-то ещё такое точно было - какой-то мутекс с интом внутри... Но не помню где.

ppplva

Считаешь ReaderCount известным ?
Это не интересно.
К тому же опять все ридеры хватают один и тот же мьютекс.

bleyman

Тебе ж сказали, что без кратковременного лока не обойтись - просто потому что ридер должен локать хоть что-нибудь, чтобы уткнуться в райтера, а если это хоть что-нибудь одно, то ридеры будут очень редко и ненадолго утыкаться друг в друга. Более того, все реализации подобной фиговины всё равно работают точно так же =)
Если ридеркаунт неизвестен, то подключающиеся ридеры начинают свою работу с вызова метода твоего мега-объекта, который проверит, что твой райтер не пишет, и увеличит ReaderCount на 1.
А завершаются наоборот, причём без проверки райтера на "пишет". Но с локом мутекса всё равно.

ppplva

По-моему, перемудрил.
В инте под мьтексом хранится количество активных ридеров, или -1, если активен райтер.
Все, никаких ReaderCount. Никаких супер-методов мега-объектов.

sergey_m

чтобы читающие треды ВООБЩЕ не блокировались.
Но ведь это не возможно. Что они будут делать когда наткнутся на врайтера?

Julie16

Имеется ввиду чтобы читающие треды не мешали друг другу. Понятное дело что с пишущим тредом они так или иначе будут блокироваться.

sergey_m

То есть что бы получение ридер доступа производилось без mutexа? Так?

Julie16

Нет. Чтобы лок этого мьютекса не влиял на остальные читающие треды. Если у нас есть один мьютекс на все читающие треды - это уже плохо. Я предложил решение в котором на каждый тред по одному мьютексу, и для записи надо залочить их все. Тогда читающие треды не мешают друг другу - они лочат каждый свой мьютекс.

garikus

Чё-то я не понял..
pthread_rwlock_rdlock - читающий thread будет блокирован только в том случае, если блокировка чтения-записи установлена записывающим thread-ом. Читающие threads не будут мешать друг другу.
pthread_rwlock_wrlock - блокировка только для записи.
(pthread_rwlock_unlock - снятие блокировки любого типа)

sergey_m

То есть тебе не подходит решение, когда мьютекс берется на очень короткое время?
У тебя очень много одновременных получение ридер доступа? Если так, то конечно можно сказать, что кривой дизайн, но это не ответ. То, что ты хочешь можно сделать через атомарные операции, но я не знаю существует ли релизация для userland. Даже если существует, то она не стандартна.

bleyman

Любая атомарная операция это лок всех остальных процессов. Привет!
2Пэже. Короче, в однопроцессорном случае в кратковременном локе нет ничего страшного вообше. Какая разница, в каком порядке процессор будет делать то, что он делает, если всё равно это надо сделать? Особенно что насколько я помню освобождение мутекса может вызвать пульс - то есть непосредственную передачу управления другим тредам.
Небольшое повышения латентности запроса на чтение сосёт перед латентностью от записи врайтера.
Накладные расходы на лок всё равно будут потрачены на то, чем ты будешь синхронизироваться.
Если у тебя процессоров существенно много - штук 16 или больше - тогда эта задержка начнёт влиять, да. Но в этом случае как правило есть железные барьеры и прочая поебень, спрашивать о решении на основе стандартных осей как-то глупо.
Короче возьми бумажку, сделай правдоподобные предположения о времени выполнения своих операций, и посчитай процентный рост латентности. Его не должно быть, насколько я понимаю.
Ещё можешь попытаться подогнать "правдоподобные предположения" таким образом, чтобы процентный рост латентности был меньше чем в случае многих мутексов. Не удастся, насколько я понимаю.

sergey_m

> Любая атомарная операция это лок всех остальных процессов. Привет!
Привет! Расскажи теперь почему атомарная операция - лок.

Julie16

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

sergey_m

Всякая ассемблерная операция атомарна для данного процессора. В случае многопроцессорной системы нужно еще защитить себя от того, что другой процессор прервет нашу операцию записи в память, а также инвалидайтить данную страницу памяти в кэшах других процессоров. На i386 последнее делается хардварно. Первое требует инструкции lock, которая распостраняется только на следующую. При этом другой процессор действительно залочится только в том случае, если он тоже в это мгновение захочет сделать оперцию с памятью. И он будет залочен только на время выполнения одной инструкции.
Вот к примеру:

c05f643c <atomic_add_int>:
c05f643c: 8b 44 24 04 mov 0x4(%esp%eax
c05f6440: 8b 54 24 08 mov 0x8(%esp%edx
c05f6444: f0 01 10 lock add %edx%eax)
c05f6447: c3 ret

имхо, это нельзя сравнивать с той блокировкой, которую вызывают слип или спин мьютексы.

Marinavo_0507

> имхо, это нельзя сравнивать с той блокировкой, которую вызывают слип или спин мьютексы
взятие спинлока - это тоже одна-две операции с памятью (uncontended case сравнивать можно и нужно
rwlocks в стандартной библиотеке вполне могут быть реализованы через те же атомарные операции, кстати

sergey_m

> взятие спинлока - это тоже одна-две операции с памятью (uncontended case сравнивать можно и нужно
А в contested - блокировка на непредсказуемое время. Как можно сравнивать гарантированное с непредсказуемым?

Marinavo_0507

> А в contested - блокировка на непредсказуемое время. Как можно сравнивать гарантированное с непредсказуемым?
А если у тебя N процессоров одновременно полезут обновлять одну и ту же переменную,
какое будет время блокировки?
Есть ли вообще гарантии, что каждый процессор сможет обновить? На всех архитектурах?
При реализации rwlock через мьютексы при взятом мьютексе производится строго определённые операции,
поэтому с предсказуемостью дело обстоит ровно так же.
Кстати, на некоторых архитектурах поддержки атомарных операций в нужном объёме нет,
их реализуют через спинлоки.

sergey_m

А если у тебя N процессоров одновременно полезут обновлять одну и ту же переменную,
какое будет время блокировки?
Всё равно N не больше чем число процессоров.
Есть ли вообще гарантии, что каждый процессор сможет обновить? На всех архитектурах?
Да, при условии, что в реализации нет ошибок.
При реализации rwlock через мьютексы при взятом мьютексе производится строго определённые операции,
поэтому с предсказуемостью дело обстоит ровно так же.
А в линуксе не может быть так, что процесс наткнувшийся на мьютекс преемптится другим, что бы просто так не крутить процессор? Я в курсе, что в линуксе нет слиплоков. Означает ли это, что ждущий мьютекса крутится бесконечно?
Кстати, на некоторых архитектурах поддержки атомарных операций в нужном объёме нет,
их реализуют через спинлоки.
Пример пожалуйста. Я вот вижу ряд архитектур, где мьютексы реализованы через атомарные операции.

Marinavo_0507

> Всё равно N не больше чем число процессоров.
Ну а вдруг 2 процессора будут доступаться по очереди, а третьему ничего не достанется?
Откуда ты знаешь, что во всех-всех реализациях есть гарантированно справедливый доступ?
> А в линуксе не может быть так, что процесс наткнувшийся на мьютекс преемптится другим, что бы просто так не крутить процессор?
Если хочешь гарантий, придётся обеспечить невозможность такого дела.
Вне зависимости, линукс это или ещё что.
> Пример пожалуйста. Я вот вижу ряд архитектур, где мьютексы реализованы через атомарные операции.
Достаточной поддержки, это например 32-битный счётчик.
На sparc32 например проблемы с этим.
В линуксе реализованы 24-битный счётчик с помощью спинлока.

sergey_m

> Ну а вдруг 2 процессора будут доступаться по очереди, а третьему ничего не достанется?
Такого быть не может. Как только процессор завершит инструкцию, которая под инструкцией lock, то все остальные процессоры сразу получат доступ к памяти.
> Откуда ты знаешь, что во всех-всех реализациях есть гарантированно справедливый доступ?
Не знаю я про все-все реализации. Я говорю про i386.
> > А в линуксе не может быть так, что процесс наткнувшийся на мьютекс преемптится другим, что бы просто так не крутить процессор?
> Если хочешь гарантий, придётся обеспечить невозможность такого дела.
Вот я и предлагаю её обеспечить, путем не использования мьютексов. И это именно то, что просит Антон.
> > Пример пожалуйста. Я вот вижу ряд архитектур, где мьютексы реализованы через атомарные операции.
> Достаточной поддержки, это например 32-битный счётчик.
> На sparc32 например проблемы с этим.
> В линуксе реализованы 24-битный счётчик с помощью спинлока.
Который в свою очередь сделан с помощью атомарных оперций над словами длины, которую поддерживает данный процессор.

Marinavo_0507

> Такого быть не может. Как только процессор завершит инструкцию, которая под инструкцией lock, то все остальные процессоры сразу получат
> доступ к памяти.
Хватит тупить.
Какой из них, если таких несколько?
> Не знаю я про все-все реализации. Я говорю про i386.
Отвечаешь за все чипсеты, поддерживающие больше двух процессоров?
Приведи ссылку на мануал хотя бы к одному.
> Вот я и предлагаю её обеспечить, путем не использования мьютексов. И это именно то, что просит Антон.
...
> Который в свою очередь сделан с помощью атомарных оперций над словами длины, которую поддерживает данный процессор.
Вот мы и договорились, что принципиальной разницы между этими реализациями с точки зрения низкоуровневых операций не будет.
Мьютексы всегда реализованы какими-то атомарными операциями, которые и обеспечивают блокировку.

sergey_m

> Хватит тупить.
Сначала ты прекращай тупить.
> Какой из них, если таких несколько?
Задавай вопросы понятнее. Обходись без местоимений.
> Отвечаешь за все чипсеты, поддерживающие больше двух процессоров?
С каких пор чипсет выполняет то, что написано в спецификации на процессор?
> от мы и договорились, что принципиальной разницы между этими реализациями с точки зрения низкоуровневых операций не будет.
Мы не договорились. Атомарная операция блокирует на определенное число тактов, а мьютекс на неопределенное.
> Мьютексы всегда реализованы какими-то атомарными операциями, которые и обеспечивают блокировку.
Блокировку обеспечивает кручение в цикле (спинмьютекс либо преемпция другим процессом (слипмьютекс а не атомарные операции.

Dasar

> Какой из них, если таких несколько?
есть процессоры - они постоянно обращаются к одной и той же памяти через lock
какие есть гарантии, что все три процессора будет одиннаково загружены?
а не только, например, первый?
> Задавай вопросы понятнее. Обходись без местоимений.
Как будет распределятся процессорное время между 3 (4, 10 и т.д.) процессорами, если на них на всех загрузить программу вида:

c05f643c: 8b 44 24 04 mov 0x4(%esp%eax
c05f6440: 8b 54 24 08 mov 0x8(%esp%edx
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
c05f6444: f0 01 10 lock add %edx%eax)
какие есть гарантии работы? зависит ли это от чипсета?

sergey_m

Ты когда нибудь такой код видел? Обычно между lock операциями еще много-много-много другого кода. У тебя несуществующий вырожденный случай.

Marinavo_0507

Если говорить о гарантиях, то нужно рассматривать все случаи.
Если хочешь говорить о среднем времени ожидания, то приводи другие аргументы.

bleyman

>Мы не договорились. Атомарная операция блокирует на определенное число тактов, а мьютекс на неопределенное.
Блиа, вот ты тупишь, а?
Атомарная операция блокирует на неопределённое число тактов, если процессоров много, и все лезут в одну и ту же память. Тебе привели пример. Более того, я не поручусь за то, насколько велик кусок "одной и той же памяти", в случае с мутексом он по крайней мере точно ограничен этим мутексом.
Едем дальше. Удачный лок держится не более чем одну инструкцию. В коде для семафора удачный лок мьютекса для запроса на чтение тоже держится не более чем К инструкций. Ровно столько, во сколько компилируется соответствующий код уменьшения/увеличения счётчика. "К" не зависит от числа процессоров и количества тредов. Поэтому я назвал "К" _накладными_расходами_на_блокировку_. Поведение системы в целом никак качественно не зависит от того, у нас К инструкций или одна тратится.

sergey_m

> Блиа, вот ты тупишь, а?
Тупишь ты.
> Атомарная операция блокирует на неопределённое число тактов, если процессоров много, и все лезут в одну и ту же память. Тебе привели пример.
Такого как в примере не бывает. Дискуссия будет продолжена после приведения какой нибудь осмысленной программы, которая после компиляции содержит в коде две lock инструкции подряд.
Удачный лок держится не более чем одну инструкцию. В коде для семафора удачный лок мьютекса для запроса на чтение тоже держится не более чем К инструкций. Ровно столько, во сколько компилируется соответствующий код уменьшения/увеличения счётчика. "К" не зависит от числа процессоров и количества тредов. Поэтому я назвал "К" _накладными_расходами_на_блокировку_. Поведение системы в целом никак качественно не зависит от того, у нас К инструкций или одна тратится.
Повторю еще раз. Когда тред ждёт на мьютексе, то он может (и часто именно так и происходит) быть отложен в пользу другого треда, который может сейчас выполняться. Когда скедулер вернется к отложенному треду не гарантируется ничем.

bastii

Повторю еще раз. Когда тред ждёт на мьютексе, то он может (и часто именно так и происходит) быть отложен в пользу другого треда, который может сейчас выполняться. Когда скедулер вернется к отложенному треду не гарантируется ничем.
А разве в винде порядок не гарантирован, т.е. тот тред, что первый мьютекс запросил, первым и получит?

sergey_m

А разве в винде порядок не гарантирован, т.е. тот тред, что первый мьютекс запросил, первым и получит?
Это не противоречит тому, что он может ждать на мьютексе неопределенное время.

Marinavo_0507

> Такого как в примере не бывает.
Посмотри в словаре, что такое гарантия.
> Дискуссия будет продолжена после приведения какой нибудь осмысленной программы, которая после компиляции содержит в коде две lock инструкции подряд.
Повторяю в хз какой раз:
не нужно двух инструкций подряд, а нужно N (N>=2) процессоров, каждый из которых иногда
выполняет lock инструкцию, а все вместе они непрерывно занимают ячейку памяти в течение
неопределённо долгого времени.
Так что (N+1)-ому процессору не достаётся доступа.
Учитывая, что синхронизация доступа к памяти может занимать много тактов процессора,
то при достаточно большом N вызов атомарных операций для достижения описанного эффекта
нужно делать не так уж часто.
> Когда тред ждёт на мьютексе, то он может (и часто именно так и происходит) быть отложен в пользу другого треда, который может сейчас
> выполняться. Когда скедулер вернется к отложенному треду не гарантируется ничем.
В рассматриваемых ОС тред может быть прерван в любое время, независимо от используемых блокировок.
Когда выполнение треда продолжится, не гарантируется ничем (если не рассматривать RT-фичи).
Если же говорить про контекст ядра, то в нём легко оформить критическую секцию (в случае Linux+preempt например
а то и вообще ничего не делать не надо.

bleyman

Блиааа.
Ну говорю же я тебе, тут разница только в константах. Если у тебя используется лок на одну инструкцию, и он вызывается раз в 1000 инструкций, то тебе достаточно 1000 тредов, чтобы получить потенциально неисполняющийся тред.
Если ты используешь мьютекс, который локается/анлокается не более чем за 20 инструкций, и вызывается раз в 20000 инструкций, то тебе нужны те же самые 1000 тредов.
Какая нах разница?
Оставить комментарий
Имя или ник:
Комментарий: