Пул потоков
На днях один хороший знакомый набросал прогу для того, чтобы проверить как работают два потока с одинаковым приоритетом на одно- и многопроцессорных машинах. Сделал он это на C++ (кстати, все помнят что потоки в C++ надо создавать с помощью _beginthread/_beginthreadex, а не с помощью CreateThread). Другой, увидев это дело решил проверить какие особенности планирования потоков для управляемого кода. Ведь CLR-потоки не всегда однозначно соотносятся с потоками ОС. Оказалось что даже для CLR-потоков поведение достаточно ожидаемое (такое же как и для настоящих потоков). Но разговор не про это.
Глянув в код этого товарища я обнаружил примерно следующее:
class test {
void Method1() {
while (true) {
Console.WriteLine("x");
}
}
void Method1() {
while (true) {
Console.WriteLine("o");
}
}
static void Main() {
Thread t1 = new Thread(new ThreadStart(Method1));
Thread t2 = new Thread(new ThreadStart(Method2));
t1.Start();
t2.Start();
Console.ReadLine();
}
}
Конечно, Вы скажете, что с пивом, да еще и для тестовой задачи это покатит. Но я могу предложить более элегантный пример:
class test {
void Method1(object state) {
char ch = (char)state;
while (true)
Console.WriteLine(ch);
}
static void Main() {
ThreadPool.QueueUserWorkItem(new WaitCallback(Method1), 'x');
ThreadPool.QueueUserWorkItem(new WaitCallback(Method1), 'o');
Console.ReadLine();
}
}
Кроме того, что в моем варианте гораздо меньше написано, в нем есть еще одно важное отличие: он не создает новых потоков явно, а просит их у объекта ThreadPool. Давайте разберемся чем это выгодно в реальной задаче. Вводные такие: нам нужно быстренько создать поток и выполнить в нем небольшую задачу (причем, эта задача - не безконечный цикл). Если мы честно напишем
new Thread(new ThreadStart(Method1))
то система честно создаст для нас новенький, "с иголочки" поток и после того как мы скажем Start() выполнит в нем Method1. Мы все помним, что создание потока - очень дорогостоящая операция! В случае, когда мы просим поток у ThreadPool-а:
ThreadPool.QueueUserWorkItem(new WaitCallback(Method1), 'x');
нам могут дать не новый поток, а поток который уже кем-то использовался (будем надеятся у него не сильно темное прошлое), а после выполнения нашего Method1 поток не убьют, а приберегут на потом. Вдруг, он еще кому-то понадобиться! В итоге получаеться, что программа может заготовить себе 2-3 потока и использовать их на все случаи жизни. Кроме того, что мы избегаем дополнительных затрат на создание каждый раз нового потока есть еще одна бесплатная вкусность: пока нам этот поток не нужен, его может использовать кто-то другой (например библиотека CLR для выполнения в нем копирования файла). Пожалуй самой яркой аналогией будет мир автомобилей. В наше время почти у каждой семьи в развитых странах есть автомобиль. Это дает возможность быстро домчать куда угодно и не зависеть от общественного транспорта. Но это хорошо лишь до того момента, когда автомобилей становится слишком много (пробки, проблеммы парковки, экология и т.д.). А теперь представьте себе, что каждый вместо того чтобы воспользоваться своим авто - вызывает такси (Вы видели городские улицы в Нью-Йорке?), приежает на работу и такси свободно. Такси едет обслуживать другие вызовы! Общее количество машин уменьшается, улучшается экология, а проблема парковки отпадает вообще.
Вы поняли аналогию с машинами? Ведь, чем меньше в системе потоков, тем быстрее работает система (не тратит время на переключение потоков), тем больше квантов времени достается Вашему потоку.
До появления .NET Framework-а тоже были пулы потоков (читать про CreateIOCompletionPort), но работать с ними было гораздо сложнее. И в маленьких задачах, "игра не стоила свеч". Но теперь работать с ними очень легко, так что - пользуйтесь!
Коментарі
Теперь о TheadPool:Изначально в нем нет никаких потоков. Они создаются по-требованию. То есть, если ты их не используешь, то их нет и никакие ресурсы зря не расходуются.
Имеено в системах которые поддежривают многопоточность нужно активно использовать потоки в приложениях, что бы на многопроцессорных машинах аппликуха максимально грузила ваш паравоз. На однопроцессорных машинах тоже будет достигнут эффект "большего отклика системы", что является показателем "эргономичности". Конечно все зависит от планировщика, например в Линуксе он рваный, поэтому там с этим сложнее, хотя потоки которые делал Инго Молнар имеют довольно современную реализацию. В виндовсе довольно мягкий скедулер, поэтому в виндовсе нужно использовать потоки максимально.
Конечно, должна соблюдаться культура написания многопоточных программ. Самые важные правила:
Асинхронный ввод ввывод, как и вывод вообще должны управляться потоками читалками/писалками. Разделите всю работу с ресурсами посредсвом семафором и вынесете в управляющие потоки работу с данными.
Окно всегда должны разделяться на потоки которые рисуют и потоки которые создают информацию (скачивают). Причем любое окно, любого виндовс приложения (кроме совсем тривиальных хеллоу-ворлд).
Неплохой пример - MTTTY, которое идет как пример в MSDN.
Более того нужно не просто не бояться, а максимально эфективно использовать потоки в приложениях, вследствии чего ваши приложения будут иметь очень хороший отклик для системы и пользователя. Согласитесь не очень приятно, когда программа отработана, а результаты не показываются потому что какие-то проблемы с главным окном приложения.
CreateThread. Для создания нового потока приходится переключаться в режим ядра. Это и есть самая ресурсоемкая операция. Неважно, какие надстройки (обертки вокруг CreateThread) еще участвуют в этом действе. Я не думаю, что хотя-бы одна из них (в популярных библиотеках) сравнима по временным затратам с переходом в режим ядра и обратно. А я думаю, что ф-ия CreateThread делает это не один раз :) Конечно, бывают еще облегченные потоки (CreateFiber), которые полностью в пользовательском режиме делают, но у них другие недостатки...
Я не знаю как это организовано в Java, но думаю что вариантов немного:
- Создавать вызовом CreateThread
- Создавать вызовом CreateFiber
- Использовать на заднем плане пул потоков (основанный на IoCompletionPort или нет, это не так важно)
ИМХО, последний достаточно сильный вариант, но лучше все-таки дать возможность программисту самому решать что делать. После того, как в .NET 2.0 будет поддержка "фибров", можно будет сказать, что .NET это делает. Но самое главное, это два момента:
1. Использовать потоки!
2. Хотя бы 1 секунду обдумывать способ, которым использовать потоки.
2. Joric:Придумай себе другой пример, где два человека приехали в одну и ту же точку на двух машинах, а надо уехать на одной.
1. Каждый раз создавать новый (прямо или опосредованно).
2. Использовать заранее заготовленные (пул потоков).
Исключение - Fibers. Вот это действительно ненастоящие потоки. Они не требуют переключения в режим ядра. Но они и не планируются системным планировщиком задач. Их планирует само приложение. До недавнего времени в UNIX-ах были только такие. Теперь (уже лет 5-6) есть всякие. А в Windows 95 их по-моему небыло. Может в Java именно такие? Надо спросить у какого-нить спеца.
"В Win есть поток ядра, задача которого давать жить всем тем потокам, которые имеют право получить процессорное время..."
Это не совсем так, хотя так его можно себе представить. Дело в точ, что именно планировщик дает жить потокам, никакого потока ядра нет.
Все потоки в системе одинаковые и ничем кроме базового приоритета не отличаются. Базовый приоритет это значение которое рассчитывается из приоритета потока (заданного при создании потока или установленного ф-ией
SetThreadPriority) и класса приоритета процесса в котором поток создан.
Например, есть процесс с классом IDLE_PRIORITY_CLASS и процесс с классом HIGH_PRIORITY_CLASS. Оба они создают поток с приоритетом THREAD_PRIORITY_NORMAL. Но поток, который принадлежит процесса с классом приоритета HIGH_PRIORITY_CLASS будет планироваться рашьше чем тот, что принадлежит процессу с IDLE_PRIORITY_CLASS.
Но все они находятся в одной очереди.
P.S. Я знаю, что JVM - рядовая программа, но никто не мешает разработчикам JVM, когда ты их просишь стартовать поток, брать его из какого-то пула или стартовать вместо него фибер. У тебя очень мало возможностей проверить что он сделал на самом деле (также как и в .NET :) )