PHP: эксклюзивный счетчик в Shared Memory

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

Разделяемая память — особая область памяти, которая доступа ВСЕМ процессам операционной системы. Доступ к определенному сегменту можно получить с помощью двух параметров: имени существующего файла и одного символа, который будет ключом System V IPC. Это нужно для того, чтобы несколько сегментов могли быть выделены основываясь на одном и том же файле. Звучит пока очень туманно, но сейчас я приведу пример, и надеюсь все станет понятно.

$t_key = ftok(__FILE__, ‘g’); $shmid = shmop_open($t_key, ‘c’, 0755, 64); shmop_write($shmid, 1, 0); shmop_close($shmid);

Первой командой, специальной функцией ftok мы получили ключ, по которому будем получать доступ к сегменту разделяемой памяти. Первым параметром мы передали ему имя файла скрипта, а вторым букву ‘g’. Почему ‘g’? А просто так, можно взять любой символ.
Во второй строке мы собственно открываем доступ к сегменту памяти. Первый параметр — наш ключ, второй параметр ‘c’ — флаг указывающий на то, что мы создаем сегмент памяти, при том, если он уже существуем — он открывается на чтение/запись. Третий параметр — права на доступ, четвертый — размер в байтах.
В третьей строке мы записываем туда единицу, последний параметр — смещение.
И наконец закрываем сегмент.
Теперь по адресу, который определяется файлом скрипта и буквой ‘g’ в сегменте размером 64 байта лежит единица, и ее может достать любой процесс.

Теперь представим, что у нас программа работает в 50 потоков, она обрабатывает файл строчка за строчкой. Мы не хотим, чтобы программа два раза обрабатывала одну строку, поэтому кладем в разделяемую память номер строки, которая была обработана последней. И каждый процесс перед тем, как считать строчку берет из разделяемой памяти номер последней обработанной строки, прибавляет единицу, записывает обратно в разделяемую память и идет обрабатывать эту строчку. Следующий процесс возьмет уже строчку с номером на один больше. Все хорошо, все радуются, работает быстро. Однако случается так, что два процесса одновременно решили взять значение из памяти и взяли одинаковое значение и записали одинаковое. А представьте, что сразу 50 процессов одновременно считают значение. В общем дело ясное, что нам надо ограничить доступ к памяти одним процессом.

Как раз для таких случаев придуманы семафоры. Семафор (semaphore) — это такая сущность, которая позволяет ограниченному количеству процессов одновременно получать «зеленый свет» на выполнение чего-либо. Семафор, у которого такое количество равно 1 называется мьютексом (mutex). Именно мьютекс позволит нам не допускать в наш сегмент памяти больше одного процесса и остановит анархию.

$sem = sem_get(ftok(__FILE__, ‘g’), 1); sem_acquire($sem); //тут может быть только один процесс sem_release($sem);

Мы получаем ключ для семафора точно таким же образом, как и для разделяемой памяти. Третий параметр — автоматическое «отпускание» семафора, если поток, который был впущен завершился. Те потоки, которые попытаются получить доступ к семафору, будут блокированы до тех пор, пока семафор не освободится. Это похоже на очередь в туалет. Один внутри, остальные снаружи. Один выходит, другой заходит и закрывается, другие продолжают ждать.

Таким образом, создав ячейку памяти и создав семафор мы получаем счетчик, который позволит нам обрабатывать файл построчно не боясь, что одна строка будет обработана два раза. Для использования этого принципа я сделал класс Counter, который работает в моем многопоточном приложении. Взять его можно вот здесь. А тут я приведу пример использования:

//Ноль это наше начальное состояние счетчика $counter = Counter::_new(‘g’, 0); //Порождаем потомков for ($i = 0; $i < PROCESS_AMOUNT; $i++) { $pid = pcntl_fork();   if ($pid == -1) { echo ‘Error spawning new process’; } elseif ($pid) { //Родительский процесс у нас ничего не будет делать } else { //Первый параметр – имя счетчика, и заодно часть ключа //Второй параметр – увеличивать ли значение счетчика //В данном случае мы просто читаем значение $counter = Counter::get(‘g’, false); //TOTAL_LINES – количество строк в файле while ($counter < TOTAL_LINES) { //Берем значение счетчика и увеличиваем его на 1 $counter = Counter::get(‘g’, true); //Обарабатываем файл } } }

Leave a Reply

Your email address will not be published. Required fields are marked *