Статья-гипотеза.
То, что описано, нигде не реализовано, хотя, в принципе, в Phantom ничего не мешает сделать это.
Эта идея пришла мне в голову уже давно и даже где-то мной была описана.
Поводом для описания сегодня является обсуждение сетевых драйверов Linux в комментариях к Анатомия водителя .
Сформулирую описанную там проблему так, как я ее понимаю: сетевой драйвер Linux работает в отдельном потоке, который читает полученные от устройства пакеты и обрабатывает их синхронно.
Он проходит через маршрутизацию, межсетевой экран и, если пакет не для нас, отправляет его на исходящий интерфейс.
Понятно, что некоторые посылки обслуживаются быстро, а другие могут занять много времени.
В такой ситуации хотелось бы иметь механизм, который динамически порождает служебные потоки по мере необходимости, и достаточно дешевый механизм в ситуации, когда дополнительные потоки не нужны.
То есть хотелось бы вызов функции, который при необходимости можно преобразовать в запуск потока.
Но ценой вызова функции, если нить особо не нужна.
Эта идея пришла ко мне, когда я рассматривал совершенно фантастические модели для Phantom, включая модель актера с потоком, выполняющимся для любого вызова функции/метода.
Саму модель я отбросил, но идея ленивых потоков осталась и до сих пор кажется интересной.
Так.
Давайте предположим, что мы запускаем функцию void worker(packet), которая должна молча что-то выполнить.
Код возврата нас не интересует (или он нам дается асинхронно), и нам хотелось бы выполнить функцию внутри нашего потока, если он короткий, и внутри отдельного потока, если он длинный.
Понятие «длинный» здесь открыто, но для него было бы разумно применить простую оценочную точку – если мы встретили свой собственный квант планирования – функция короткая.
Если бы при жизни функции произошло вытеснение и у нас отобрали процессор, это было бы долго.
Для этого запустим его через прокси lazy_thread(worker, package), который выполняет очень простую операцию — фиксирует ссылку на стек в момент перед вызовом рабочей функции в специальной lazy_threads_queue и заменяет стек на новый:
Если рабочий вернулся, то отмените эту операцию:push( lazy_threads_queue, arch_get_stack_pointer() ); arch_set_stack_pointer(allocate_stack())
tmp = arch_get_stack_pointer()
arch_set_stack_pointer( pop( lazy_threads_queue ) );
deallocate_stack(tmp)
И мы продолжим, как ни в чем не бывало.
Все обошлось нам в пару строк кода.
Если прошло значительное количество времени, а воркер все еще работает, мы проведем простую операцию — в момент изменения стека разделим потоки постфактум: сделаем вид, что внутри произошло полноценное создание потока.
lazy_thread(): копируем свойства старого потока в новый, адрес возврата находится в новом стеке (который мы выделили в lazy_thread) переставляем так, чтобы он указывал на функцию thread_exit(void), а в старом thread мы установим указатель следующей инструкции на точку выхода из функции lazy_thread. Теперь старый поток продолжает работать, а новый завершит начатое и будет уничтожен там, где в исходном сценарии он вернулся бы из lazy_thread. То есть: фактическое решение запустить поток для обработки конкретного запроса произошло после того, как мы начали его выполнять и смогли оценить реальную серьезность этого запроса.
Вы можете наложить дополнительные ограничения на точку принятия решения о запуске ленивого потока — например, средняя загрузка в течение 15 секунд будет меньше 1,5/процессор.
Если оно выше, распараллеливание вряд ли поможет; мы потратим больше ресурсов на создание бессмысленных тредов.
В современном мире, когда принято иметь 4 процессора в карманной машине и 16 в настольной машине, очевидна потребность в механизмах, которые помогают коду адаптироваться к нагрузочной способности оборудования.
Может быть и так? Теги: #threads #threads #parallelism #phantom #phantomos #phantom OS #программирование #Системное программирование
-
Протокол Разработки Un-Fuckup (Udp)
19 Oct, 24