LINUX.ORG.RU

Posix threads: rwlock

 posix threads


0

3

Задача: нужно создать кучу потоков, которые читают разделяемые данные. И есть один, который иногда (очень редко) меняет эти данные. Напрашивающееся решение: использовать rwlock. Это точно лучше, чем mutex. Но есть засада: при попытке получить rwlock на чтение может быть возвращена ошибка: достигнут предел на количество читателей.

Вопрос: как мне узнать заранее этот предел? Если я буду его знать, то у меня будут варианты: либо молча уменьшить макс. кол-во потоков до этой величины, либо сообщить юзеру, что я не могу обеспечить заданное им кол-во потоков, и завершиться после этого. Так как мне узнать (или задать) эту величину? Google даёт только ссылки либо на тот же man, который у меня и так есть, либо на пересказ этого же man’а своими словами с опечатками.

У меня программы должны работать десятилетиями, я не могу методом тыка определять такие величины: создавать потоки, лочить rwlock, затем при ошибке выдать сообщение о предельном кол-ве потоков, которое вбить гвоздями в программу. Сегодня это одна величина, а через 20 лет на другой версии GNU/linux она будет другой. Есть ли тут аналог sysconf(3)? Вот я могу запросить в run-time sysconf(_SC_OPEN_MAX). И исходя из этого не наступить на проблему, когда мне нужно держать открытыми много файлов одновременно.

А в случае POSIX threads вместо предсказуемого поведения программы мне предлагаются грабли. На которые я могу наступить в неизвестный мне заранее момент времени. Кто вообще писал такую спецификацию? Спек должен быть максимально конкретным, а не вот это вот расплывчатое, невнятное говно. Туда же до кучи: где мне получить величину PTHREAD_STACK_MIN? В man’е pthread_attr_setstacksize(3) она указана как 16384 байта (linux-specific). Я должен вбить гвоздями в программу константу 16384? Где, чёрт возьми, она за-define-нена? В pthread.h её нет. Ни sysconf(3), ни getrlimit(2) не дают её мне. Кто мне гарантирует, что через 10 лет она не изменится, и моя программа не перестанет работать? А где мне взять константу PTHREAD_THREADS_MAX, на которую ссылается man pthread_create(3p)?

Чем больше думаю на эту тему, тем меньше хочется использовать POSIX threads, а больше хочется заюзать напрямую clone(2). И получить предсказуемое поведение. И гори он огнём, этот POSIX с его невнятной спецификацией! Программа, написанная лет 20-25 назад с использованием linux-специфичного packet socket до сих пор работает и есть не просит.

Но всё-таки хочется же переносимости – по возможности. Может кто знает, где нарыть документацию на POSIX threads? Более внятную, чем тупо перепечатка man’ов на функции. И конкретно на rwlock: как получить (указать?) максимальное количество потоков-читателей? Opengroup не предлагать, там те же man’ы.

★★
Ответ на: комментарий от SkyMaverick

Если уж на то пошло, так @nobody в принципе может кастомные rwlock-и сделать и не париться. Тоже, кроме атомиков, ничего не нужно и кода там не то, чтобы дофига писать.

Вопрос: atomic_compare_exchange_weak() – это откуда функция? Она из системной библиотеки? man про неё ничего не знает.

nobody ★★
() автор топика
Ответ на: комментарий от alnkapa

Используйте trylock с таймаутом.

И что делать по time-out? Ещё раз попытаться? Производительность просядет. Вылететь посреди вычислений с ошибкой? Это вряд ли кому-то понравится.

Обновите системные библиотеки и ядро.

Вышла новая версия стандарта, где можно заранее узнать ограничение на кол-во читателей?

nobody ★★
() автор топика
Ответ на: комментарий от nobody

Нет, мютекс это штука от ядра операционной системы. С++ный атомик из его библиотеки - надстройка над нативным компиляторным атомиком, а тот, в свою очередь - ассемблерный. Атомики есть не только в С++, в Си они тоже есть ничуть не хуже (основа везде одна и та же).

Есть такая инструкция CMPXCHG. Она сравнивает заданную величину с содержимым переменной, и, если они совпадают, записывает в переменную другую заданную величину, а если нет - возвращает то самое отличающееся содержимое переменной. Поскольку это одна ассемблерная инструкция, переключатель процессов ОС не может вклиниться в середине и разрушить её атомарность. Ещё в ассемблере есть префикс LOCK - он указывает процу на время выполнения инструкции сообщить остальным процам (ядрам), что им нельзя трогать память. Таким образом «LOCK CMPXCHG» атомарно и в многоядерных системах. Блокировка тут зашита в сам проц, никак не зависит от операционной системы и делается ровно на время выполнения инструкции, что очень эффективно.

Как с её помощью сделать атомарный инкремент, декремент и другие изменения переменной, думаю не надо объяснять.

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

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

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от nobody

Не, не будет такой ситуации. Читателю незачем держать у себя указатель на постоянной основе (кэшировать). Быстро прошёлся по разделяемым данным..

Быстро прошёлся по разделяемым данным - это не является атомарной операцией, undestand?

anonymous2 ★★★★★
()
Ответ на: комментарий от firkax

Ну, как самый простой рабочий вариант, почему-бы и нет. Можно и усложнить по-желанию, но общая логика функционирования RWLock-ов не нарушится ИМХО.

SkyMaverick ★★★★★
()
Ответ на: комментарий от r--r--r--

OMG, откуда я знаю, какой из результатов гуглопоиска ты считаешь валидным определением/критерием «блокирующего» и/или «синхронного» мьютекса.

Я по-моему уже вполне понятно объяснил, что coro::shared_mutex позволяет избежать блокировки потока выполнения (std::thread). И вообще необходимости в отдельном потоке, кроме тредпульных. А футпринт выполняющейся корутины небольшой.

MirandaUser2 ★★
()
Ответ на: комментарий от anonymous2

Быстро прошёлся по разделяемым данным - это не является атомарной операцией, undestand?

А я и не писал, что она атомарная, я писал, что быстрая. По сравнению с сильно медленным писателем. При наличии 2 буферов вероятность нарваться на неконсистентные данные у читателя околонулевая. Но всё-же не 0, так что решение тоже не идеальное.

nobody ★★
() автор топика
Ответ на: комментарий от SkyMaverick

stdatomic.h (в стандарте C11). По-идее это libc.

Вот это то, что доктор прописал! Пойду читать С11. Пока что твоя идея в сочетании с идеями @KivApple и @Manhunt выглядит наилучшим решением. 2 буфера с разделямыми данными: один рабочий, другой резервный. В обоих по счётчику с атомарным доступом.

Читатель пытается инкрементировать счётчик в рабочем буфере. Если не получилось, пытается ещё. После успеха работает с буфером. Поработав, так же атомарно декрементирует счётчик. Вполне можно использовать просто цикл, как у тебя. Потому что переменная-счётчик занята только на время инкремента/декремента, а не на всё время работы потока с буфером.

Писатель просто проверяет счётчик в резервном буфере на 0. Если не 0, уходит поспать, после чего снова проверяет – пока она не обнулится. После этого набивает буфер данными. Вписывает его адрес вместо рабочего (запись машинного слова в память – это атомарная операция), а бывший рабочий записывает как резервный. После этого ни один читатель к нему уже не обращается, потому что буфер не рабочий. Так что при следующем запуске писателя новых читателей у резервного буфера не появится. Писателю нужно лишь дождаться, пока перестанут работать те, кто начал, когда буфер ещё был рабочим.

Вобщем, идея с подсчётом пользователей буфера – не трогать его, пока пользователи есть. Но не на rwlock, а на gcc-шных функциях атомарного инкремента/декремента – как у тебя в примере, с предсказуемой реализацией (в отличие от). Логика работы (псевдокодом) примерно такая:

разделяемый буфер {
  атомарный счётчик
  полезные данные
  } *рабочий, *резервный

цикл читателя:
  работа со своими данными
  когда нужны разделяемые:
    буфер = адрес рабочего
    инкрементировать счётчик в буфере, пока не получится
    работать с разделяемыми данными в буфере
    декрементировать счётчик в буфере, пока не получится

цикл писателя:
  select(socket)
  пока счётчик в резервном буфере != 0: поспать(немножко), снова проверить
  набить резервный буфер
  tmp = адрес рабочего буфера
  рабочий = адрес резервного
  резервный = tmp
nobody ★★
() автор топика
Ответ на: комментарий от nobody

2 буфера с разделямыми данными: один рабочий, другой резервный. В обоих по счётчику с атомарным доступом.

посмотри как у игроделов сделано-организованно, фреймы/кадры, один уходит в отображение, остальные заполняются и так по циклу. Их время жёстко давит, поэтому микро-алгоритмы конкуренции-смены_кадров там хорошо проработаны.

MKuznetsov ★★★★★
()
Ответ на: комментарий от nobody

Я не вчитывался, но кажется ты хочешь навелосипедить свой rwlock, при том что ты скорее всего не сможешь упереться в лимиты pthreads, потому что лимиты потоков ОС исчерпаются раньше.

Я предлагал вообще lock free, потому что в данном случае его несложно реализовать. А если делать полноценный rwlock, то лучше готовый.

KivApple ★★★★★
()
Ответ на: комментарий от MirandaUser2

OMG, откуда я знаю, какой из результатов гуглопоиска ты считаешь валидным определением/критерием «блокирующего» и/или «синхронного» мьютекса.

Подсказка - тот же, что и всё остальное прогрессивное человечество.

coro::shared_mutex позволяет избежать блокировки потока выполнения (std::thread)

А это совершенно ортогонально (вне-)блоковости мьютекса.

r--r--r--
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария