Lazy threads: опціональний паралелізм

Стаття-гіпотеза. Описане ніде не було реалізовано, хоча, в принципі, ніщо не заважає запив таке Фантоми.

Ця ідея прийшла мені в голову дуже давно і навіть десь була описана мною. Тригер до того, щоб її описати сьогодні — обговорення мережевих драйверів Лінукса в коментарях до Анатомії драйвера.

Сформулюю проблему, описану там, як я її розумію: мережевий драйвер Лінукса працює в окремої нитки, яка читає прийняті пакети з пристрою і синхронно їх обробляє. Проганяє через роутинг, файрволл і, якщо пакет не нам, відправляє його у вихідний інтерфейс.

Зрозуміло, що деякі пакети обслуговуються швидко, а інші можуть потребувати багато часу. У такій ситуації хотілося б мати механізм, який динамічно породжує обслуговуючі нитки по мірі необхідності, і механізм досить дешевий в ситуації, коли зайві нитки не потрібні.

Тобто хотілося б такого виклику функції, який при необхідності можна конвертувати в старт нитки. Але за ціною виклику функції, якщо нитка реально не виявилася потрібна.

Мені ця ідея прийшла, коли я розглядав абсолютно фантастичні моделі для Фантом, включаючи акторную модель з запуском нитки взагалі на будь-який виклик функції/методу. Саму модель я відкинув, а ось ідея lazy threads залишилася і досі здається цікавою.

Як це.

Припустимо, що ми запускаємо функцію void worker( packet ), яка повинна щось мовчки здійснити. Нас не цікавить код повернення (або він віддається нам асинхронно), і ми б хотіли виконати функцію в рамках нашої нитки, якщо вона коротка, і в рамках окремої, якщо довга.

Поняття «довга» тут відкрито, але розумно було б для нього застосувати просту точку оцінки — якщо ми вклалися у власний квант планування — функція коротка. Якщо протягом життя функції стався preemption і у нас забирали процесор — довга.

Для цього запустимо її через проксі lazy_thread( worker, packet ), який виконує дуже просту операцію — фіксує посилання на стек в момент перед викликом функції worker у спеціальній черзі lazy_threads_queue, і замінює стек на новий:

push( lazy_threads_queue, arch_get_stack_pointer() ); 
arch_set_stack_pointer(allocate_stack())


Якщо worker повернувся, то скасуємо цю операцію:

tmp = arch_get_stack_pointer()
arch_set_stack_pointer( pop( lazy_threads_queue ) ); 
deallocate_stack(tmp)


І продовжимо як ні в чому не бувало. Все обійшлося нам в пару рядків коду.

Якщо ж пройшов значний час, а worker все ще працює, зробимо просту операцію — в точці зміни стека виконаємо роздвоєння ниток постфактум: зробимо вигляд, що всередині lazy_thread() відбулося повноцінне створення нитки: скопіюємо властивості старої нитки в нову, адреса повернення на новому стеку (який ми виділили в lazy_thread) переставимо так, щоб він вказував на функцію thread_exit(void), а в старій нитки покажчик наступної інструкції встановимо на точку виходу з функції lazy_thread.

Тепер стара нитка продовжує роботу, а нова виконає почате, і знищиться там, де вона в оригінальному сценарії повернулася б з lazy_thread.

Тобто: фактичне рішення про запуск нитки для обробки певного запиту сталося вже після того, як ми почали його виконувати і змогли оцінити фактичну важкість цього запиту. Можна накласти на точку прийняття рішення про запуск ледачою нитки додаткові обмеження — наприклад, середній load average за 15 сек менше 1.5/процесор. Якщо він вище — розпаралелювання навряд чи допоможе, ми витратимо більше ресурсів на старти безглуздих ниток.

У сучасному світі, коли звичайна справа — 4 процесора в кишенькової машини і 16 в настільній, явно потрібні механізми, які допомагають кодом адаптуватися до навантажувальної здатності апаратури. Може бути — так?

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.