Как распределять потоки при асинхронном программировании?
Приветствую. Представьте такое приложение, где все возможные ветвления выполняются в отдельном потоке (если это безопасно, например обработка массива по элементам), при этом потоков выделяется на всё приложение ровно столько, сколько доступно ядер процессора. Если кому-то не хватает потока, колбэк добавляется в очередь и ждёт свободный поток.
Вопрос: как можно было бы спроектировать такую систему?
В идеале систему вижу так. Когда очередной поток собирается ветвиться, все колбэки добавляются в общую очередь приложения. Когда какой-то поток завершается, ему передаётся на выполнение следующий колбэк из очереди сообщений. Проблема такого подхода в том, что тратится время на синхронизацию и при большой очереди бывает быстрее обработать маленькие колбэки последовательно в одном потоке.
Другой вариант: не синхронизируемый массив потоков, при этом в новый активный поток передаётся какой-то диапазон свободных потоков, которые могут выдаваться по цепочке дальше (диапазон дробится), а если кому-то не хватит - подождут в родительском потоке. Тут проблема в том, что количество ядер всегда ограничено и при таком подходе чаще будут простаивать неиспользуемые потоки в каком-то длинном однопоточном колбэке, которому потоков выделили несколько (пусть даже они пригодятся, но не надолго).
Тут же пришла мысль: можно минимизировать время на синхронизацию, если добавлять в очередь сразу весь массив сообщений, который постепенно накапливается в активном потоке, а именно - в глобальном массиве этого потока. Но во время накопления может простаивать свободный поток...
А ещё - выдавать очередному потоку сразу несколько колбэков. Например, в очереди 10 сообщений, пришёл за добавкой новый поток (1 из 4х), ему выдаётся сразу 3 колбэка. Но тут тоже возникает опасность того, что первый из них будет выполняться дольше, чем остальные 7 вместе взятые, и оставшиеся 2 будут простаивать в ожидании. Можете для большего размаха увеличить эти цифры в несколько раз.
То, что вы описываете, весьма подходит под классический producer/consumer pattern (производитель/потребитель) Почему-то находится мало источников на русском языке, но англоязычных - полно.
Есть и готовые реализации данного шаблона.
Да, и вовсе не обязательно ограничивать количество потоков количеством ядер. Вы вполне можете использовать большее количество потоков, советую поэкспериментировать с количеством, замеряя скорости исполнения, пока не добьётесь оптимального результата.
Не понимаю, зачем делать больше потоков, чем ядер, если мы используем асинхронность. В моём понимании это приводит к лишним затратам на переключение контекста, которое должно использоваться только при необходимости (если нужно остановить поток при синхронном программировании).
Виталий, Ваша операционная система (любая) - и так распараллеливает исполнение задач. Ваша программа не будет иметь эксклюзивного доступа к процессору, поэтому переключения контекста происходит гораздо чаще, чем вы думаете. Имеется куча служб, фоновых задач, прерываний и работа самой операционной системы. И всё это происходит на тех же ядрах. Более того, без доп. ухищрений, ОС вам не гарантирует, что все ваши потоки будут исполняться на разных ядрах. Многие программы создают по несколько сотен потоков одновременно и достигают прироста производительности. В общем, оставьте эту головную боль операционной системе и железу, они прекрасно справятся и без вас.
Вам же советую подбирать число потоков экспериментальным путём.