Если вы претендуете на то, чтобы быть называться программистом, это надо обязательно прочитать. Ну то есть много кто про это знает, но много кто, увы, нет.
Я в свое время славно походил по этим граблям, и у других их видел в количестве.
Оригинал взят у
zamotivator в
Обманчиво простая задачка или немного про системное программированиеВыношу из своего поста и комментариев на 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 у него "виснет".
И читайте комменты у оригинала -
http://zamotivator.livejournal.com/606936.html#comments