Обманчиво простая задачка или немного про системное программирование

топ 100 блогов zamotivator23.11.2013 Выношу из своего поста и комментариев на facebook. https://www.facebook.com/oleg.i.tsarev/posts/10202446013501463

"Когда я вижу в коде sleep, то первое что я думаю "автор либо очень хорошо понимает, что он делает, либо не понимает вообще".
Промежуточных состояний как-то не видел.

Во втором случае человек, как правило, не владеет примитивами синхронизации либо IO.
В первом случае, скорей всего, у него активное ожидание или lock-free структуры данных."

"Простейшая" задача - запустить дочерний процесс, кормить его данными через stdin, получать ответы через stdout.

Пример неправильного решения и неуместного использования sleep:
p = executeChildProcess();
for(..) 
{
    p.stdin.write(данные);
    while(!p.stdout.ready())
    {
        sleep(5);
    }
    p.stdout.read(..)
    // обрабатываем данные
}

Жалобы были вида: "блочится на write" (или на read)

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

Ликбез:

Человек запускает дочерний процесс. Ок.
Пишет данные размера X в stdin (standart input) дочернего процесса. Ок.
Дальше спрашивает, нету ли чего прочитать из stdout (standart ouput) дочернего процесса. Ок.
Если ничего - ждет пять секунд, и пытается снова.
Пока что-то не появилось.
Потому читает что-то и повторяет

Казалось бы, всё нормально. Но этот код ужасен, работать нормально он не будет, и блокировка на чтении или записи это иллюстрирует. Почему так происходит?...

stdin и stdout - это пайпы. Это что-то типа сокетов, но не между хостами в сети, а между процессами в пределах одной машины.

Каждый раз, когда вы делаете write в пайп, данные копируются в его внутренний буфер ядром.
Если/Когда на другом конце пайпа другой процесс (по)пытается сделать read, ядро скопирует данные из внутреннего буфера читателю.

Данный размер внутреннего буфера ограничен настройками ядра.
Если постоянно в него писать и не читать, то при очередном write системный вызов повиснет, и будет висеть до тех пор, пока из пайпа не начнут вычитывать данные (освобождая место в буфере).

Что происходит с текущим кодом?
Его автор записывает X байт в stdin. И дальше он ждёт, что дочерний процесс их прочитает, обработает, и что-то выдаст в stdout.
Не разбираясь детально с тем, что именно пишут, и что именно делает дочерний процесс мы сильно рискуем.

С чего мы взяли, что передали дочернему процессу достаточно данных?
Он вполне может ждать "Добавки" прежде чем начнёт что-то выдавать.

Сколько "Добавки" ему нужно? Да хуй его знает. Докидываем.
В какой-то момент дочерний процесс обожрался и начать выдавать результаты.
Только мы этого не узнаем.

Дочерний процесс пишет в свой stdout данные, и в какой-то момент он заполняет буфер и повисает на write.

Исходный процесс висит на write в stdin дочернего, дочерний пытается записать в свой stdout, который никто не читает.

Два дурака.

В другом сценарии мы пописали что-то в stdin дочернего и пытается сделать read из его stdout.
Если мы скормили в его пайп недостаточно данных , то дочерний процесс будет висеть на read и ждать данных от папы. Безуспешно

Папа опрашивает его stdout в надежде получить данные. В бесконечно цикле. Но их не будет.
Потому что дочерний процесс ждёт данных

Два дурака

Собственно, возникает вопрос - "Где деньги, Зин?".
В смысле, как с этим бороться

Тут есть несколько решений.

Самое простое - сделать две нитки (thread) - первая пишет данные непрерывно, вторая непрерывно читает.
При достижении лимита буфера читателем тот будет блокироваться на write в stdin ребёнка, пока место не появится снова.
Читатель будет вычитать и обрабатывать все данные из пайпа ребёнка, как только они появляются.

Оба пайпа утилизируются, и всё работает заебись

Это решение, но не сказать, что очень хорошее. Есть немало других.

Другое решение - это неблокирующиеся чтение/запись.

write/read можно сделать неблокирующимися. При попытка чтения или записи системный вызов не повисает, а сообщает, сколько данных он смог записать.
Анализируя эту информацию, мы можем спланировать запись в stdin и чтение из stdout.

Нам наверняка понадобятся sleep, потому что дочерний процесс работает не мгновенно
Но это не идиотский sleep из исходного примера, а вполне сознательный.
Мы жертвуем latency в угоду thoughput и scheduling.

Есть и третье решение . Асинхронное чтение запись.
Мы запускаем асинхронно запись и идём заниматься своими делами.
Асинхронно запускаем чтение.
Занимаемся своими делами.

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

Разница по сравнении с решением с нитками (threads) состоит в том, что эти нитки находятся внутри ядра, а пользовательская нитка - одна.

У этого решения свои плюсы и минусы
sleep тут тоже может быть нужен

Есть и четвёртое решение. Мультиплексированный ввод вывод.

Похож на неблокирующееся выполнение, но концептуально совсем другой.
Мы делаем чтение и запись, неблокирующиеся.
Опа пайпа суём в специальный системный вызов - select либо poll. Простите, select только для сетевых сокетов. Значит, poll. Там есть ещё ньюансы, типа создания евента, но это детали.

В тот момент, когда у нас данные записались (в буфере есть свободное место) либо прочитались (дочерний процесс что-то нам передал через пайпа и мы это прочитали), poll завершается.

Из его результаты и пары дополнительных вызовов мы узнаем, что именно завершилось и как именно.
Обрабатываем. И снова запускаем.

Нету sleep. Нету переполнений. Deadlockов.

Все задачи и решения такого плана относятся к области системного программирования.
Не стоит совокупляться с процессами и пайпами без чёткого понимания, что именно вы делаете и как это работает.

А то получится как у автора кода из начала поста... write у него "виснет".

Оставить комментарий

Архив записей в блогах:
Не смотрел «прямой линии» президента . И впервые за многие годы даже не читал стенограммы того, что наговорил Путин, отвечая на вопросы народонаселения... Много потерял? И много ли нас таких, кто не смотрел? Кинули ссылку на видео, где Путин говорит о количестве народонаселения, ...
Интересно, сколько раз в "цивилизованных странах" можно нарушать закон? Ну вот так, чтоб постоянно, с упорством достойным лучшего применения, постоянно на одни и те же грабли?... Знакомьтесь - Константин Котов Был оштрафован на 20 тыс. руб. за несанкционированную акцию 2 марта. 13 ...
Только я вчера немного успокоилась, что теперь знаю диагноз кошки, но не тут то было. Сегодня утром осмотрела глаза у Фелис в глазу в передней камере появились хлопья! Раньше просто было общее помутнение и как бы "загрязнение" радужки, а теперь внутри видны сгустки. И я незнаю, это ...
друзья, мне кажется что на вид проблема обычная, но решение её мне не найти много лет, а сейчас просто ПИК. Есть настоящая проблема. Мама . Мне 23,брату 30. Появились внуки, очаровательные двойняшки. 1.Но маме всё не нравится. ей не нравится, что ...
Хочу поделиться восторгом от одного письма. Хоть сейчас бери – и со сцены с выражением! Nerukami (nerukami) «Давно хотел вас порадовать названиями должностей в своей телевизионной компании. Когда что-то попадалось по отдельности, не так смешно, но ...