Привет всем. Пишу на Delphi, но этот сайт просто посоветовали. Необходимо синхронизировать блоки кода с разными типами доступа, а именно : 1. Read - Несколько(сколько угодно) потоков могут входить с таким доступом, при этом потоки на Write должны ожидать пока все потоки с Read "выйдут" из блока. 2. Write - только 1 поток может войти в блок. При этом остальные потоки, хоть Write, хоть Read потоки должны ожидать выхода захватившего потока из блока. Захвативший поток далее может входить как Read. Есть такой класс в Delphi - TMultiReadExclusiveWriteSynchronizer. Но прямо скажем он гумно, слишком медленный, причем несколько не подходит к задаче. Подскажите пожалуйста статьи, примеры по данному поводу или алгоритмы, с использованием только событий и критических секций. Необходима максимальная скорость выполнения входа и выхода из блока. Спасибо!
Apocalypse Да ну? Вот ну ни разу не читал. l_inc Спасибо, но там минимум Windows Vista. У меня минимум для дедиков с Windows 2000.
JingoBo Можно и для систем по-младше придумать: Код (Text): var counter:integer; while (counter <> 0) or (InterlockedCompareExchange(@counter,-1,0) <> 0) do ; {writer} InterlockedIncrement(@counter); while True do begin if counter < 0 then Continue; tmp := InterlockedExchange(@counter,-1); if tmp < 0 then Continue; Inc(tmp); InterlockedExchange(@counter,tmp); Break; end; {reader} InterlockedDecrement(@counter); На паскале я сто лет назад писал. А на делфи, можно сказать, вообще никогда не писал. Надеюсь, из паскаля ещё правильно помню. И гугл говорит, что нужна ещё директива {$B-}. P.S. Нет. Тут я явно прогнал. Нужно защищать counter лучше. Ну значит возьмите реализацию из Таненбаума (Операционные системы. Разработка и реализация. В 3-ем издании точно есть). Только вместо мьютексов используйте спинлоки на основе InterlockedExchange.
Гугли "read-write lock". Когда поймешь матчасть, то без проблем напишешь свою реализацию. Можешь посмотреть, как это реализовано у других, готового кода очень много.
JingoBo Водишь версионность. Делается просто, переменная для каждого потока который read. 0)ставим блокировку на чтение 1) Из блока для чтения читаешь в эту переменную 2) далее снимаешь блокировку 3) затем работа идёт с этой переменной. А запись обычная с блокировкой. Будет работать быстро. В зависимости от задачи от блокировок можно отказаться, построив на атомарных инструкциях. Можно раскидать потоки по разным участкам блока. ИХМО пока свои шишки не набьёшь параллельной обработке не научишься. Второй способ уйти от потоков, сделать кооперативную многозадачность, тогда блокировки вообще не понадобятся.
На синхронизации собаку съел еще в яве. Как реализовать своими путями Read/Write Lock знал(на событиях), но что то мне подсказывало, что это далеко не самый лучший вариант, т.к. работаю в одном процессе(и не в Ring-0 хехе) и цеплять объекты ядра не очень хорошо, т.к. они медленнее чем критические секции. Спасибо, я про это почитаю. Вчера скачал эту книжку, ОЧЕНЬ понравилась. Реализация Read/Write Lock(стр. 116) там до банальности проста и с ресурсами не емка(есстесна мутексы на КС поменять), там на одном мутексе и парочке атомарных спинлоков, буду использовать ее. Спасибо всем!
JingoBo Критические секции тоже легко могут уйти в ядро, отправив поток ожидать следующий квант, если секция занята. Поэтому я и предлагал (если действительно нужна максимальная скорость) все объекты синхронизации ограничить только спинлоками: Код (Text): {acquire} while True do begin if lock <> 0 then Continue; tmp := InterlockedExchange(@lock,1); if tmp <> 0 then Continue; Break; end; {release} InterlockedExchange(@lock,0); P.S. Реализацию можно, естесственно, улучшить, если сделать asm-вставку (см. третий том учебника, раздел "8.10.6.1 Use the PAUSE instruction in Spin-Wait loops").
l_inc Более разумное решение - это некоторое ограниченное кол-во спинлоков + sleep(0) или крит.секция в случае неудачи, иначе может быть бестолковый разогрев проца в ожидании у моря погоды
leo Так как раз это EnterCriticalSection и делает. А упомянутая pause разве именно это и не предотвращает (причём более эффективно, чем отдача своего кванта)? Ну по-крайней мере на современных процессорах.
крит. секция - это и есть несколько спинлоков, а потом WaitForSingleObject. Там как бы есть много нюансов внутри EnterCriticalSection, но суть именно такая.
l_inc "Бесконечные" спинлоки имеют смысл при выполнении двух условий: 1) время захвата ресурса (т.е. чтения\записи) сравнительно мало, и 2) все конкурирующие потоки с высокой вероятностью исполняются одновременно. Например, какой смысл тратить весь квант потока на бестолковые спинлоки, если поток, захвативший ресурс, в это время банально усыплен шедулером - не лучше ли не тратить бесполезно время, а передать управление этому потоку?
leo Для максимальной скорости входа/выхода из критического участка, естесственно, важно иметь число потоков, работающих с этим участком, не превышающее числа процессоров/ядер, и при этом каждый из этих потоков должен быть назначен на свой процессор/ядро. Хотя в зависимости от частоты попадания потоков в критический участок можно слегка послабить это условие. По поводу первого условия неплохо бы уточнить, "сравнительно" относительно чего. Например, чтение-запись в зависимости от других условий могут быть и сравнимы с длительностью кванта (хотя от двух квантов уже явно не в пользу простых спинлоков), т.к. другой поток может чуть-чуть постоять на спинлоке, пока система поймёт, что больше ей ставить на ядро кроме прерванного в критическом участке потока некого (это значительно меньше длительности кванта). Другое "сравнительно" может относиться к длительности исполнения прочего кода потока. Если потоки вообще кроме этого критического участка ничего не исполняют, то здесь мы наталкиваемся на второе условие: потоки практически не могут исполняться параллельно. Тогда даже критические секции будут менее удачной идеей, чем мьютексы, т.к. критические секции не придерживаются FIFO, в результате чего неизбежно голодание других потоков.
l_inc Спасибо за ликбез, но адресовать его лучше не мне, а автору, у которого "Несколько(сколько угодно) потоков" - вот ему и объсни, будет ли предлагаемое тобой решение наилучшим при любых длительностях блокировки и при любом числе потоков
leo Я не сомневаюсь, что Вам ликбез не нужен. Но было бы странно отвечать автору на Ваше возражение. Будем считать, что мы разыграли сценку в лицах для ТС, если Вы не против.
Простейший read-write lock без spin-wait циклов, без проверки валидности структуры, порядка вызовов и переполнения счётчиков: Код (Text): HANDLE hEvent; DWORD lockState; // LOCK_STATE record readCounter :31, isLocked :1 DWORD waitCounter; // init: NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE); lockState = 0; waitCounter = 0; // readLock: if (lockState & 1 == 1) { wait: InterlockedAdd(&waitCounter, 1); while (lockState & 1 == 1) NtWaitForSingleObject(hEvent, FALSE, NULL); InterlockedSub(&waitCounter, 1); } InterlockedAdd(&lockState, 2); if (lockState & 1 == 1) { InterlockedSub(&lockState, 2); goto wait; } // readUnlock: InterlockedSub(&lockState, 2); if (ZF) if (waitCounter) NtSetEvent(hEvent, NULL); // writeLock: if (InterlockedCompareExchange(&lockState, 1, 0) != 0) { InterlockedAdd(&waitCounter, 1); while (InterlockedCompareExchange(&lockState, 1, 0) != 0) NtWaitForSingleObject(hEvent, FALSE, NULL); InterlockedSub(&waitCounter, 1); } // writeUnlock: lockState = 0; if (waitCounter) NtSetEvent(hEvent, NULL); //destroy: NtClose(hEvent); Соответственно, на однопроцессорных системах, в блокировке шины нет нужды, а вместо ожидания событий необходимо использовать NtYieldExecution() (информацию о количстве процессоров можно получить с помощью NtQuerySystemInformation()). Для обеспечения безопасности необходимо задействовать TLS, проверки на переполнение счётчиков и валидности структуры, и как следствие таких проверок, заменить непостредственное сложение/вычитание расшареных переменных (посколько они приводят к невалидному состоянию), на чтение в локальную переменную, проверку, сложение/вычитание и обмен с сравнением (хз как одним словом называется), ну и аварийный выход из ожидания по таймауту. Структуру необходимо размещать на одной кэш-линии для минимизации эффекта false sharing. _________________ upd: Незнаю как на других языках, но на асме вышеописанный алгоритм очень просто пишется. В большинстве случаев, на входы в блоки read приходятся по одной блокировке шины. Вероятность фэйла вторых проверок в двойных проверках крайне мала, что обеспечит высокую скорость. _________________ upd: Просто не получится все-таки: Код (Text): if (lockState & 1 == 1) { ... } InterlockedAdd(&lockState, 2); if (lockState & 1 == 1) { InterlockedSub(&lockState, 2); goto wait; } необходимо заменить на Код (Text): do { DWORD temp = lockState; if (temp & 1) goto wait; } while (InterlockedCompareExchange(&lockState, temp + 2, temp) != temp + 2) Но и в таком случае — всего одна блокировка шины в большинстве случаев.