Пул потоков

На днях один хороший знакомый набросал прогу для того, чтобы проверить как работают два потока с одинаковым приоритетом на одно- и многопроцессорных машинах. Сделал он это на 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), но работать с ними было гораздо сложнее. И в маленьких задачах, "игра не стоила свеч". Но теперь работать с ними очень легко, так что - пользуйтесь!

Коментарі

Unknown каже…
Сначала о такси:Ты прав, лучше чтобы все ездили на автобусах. Но даже такси дает некоторый эффект. Ведь такси не стоит весь рабочий день возле работы, мешая ходить пешоходам. И в случае, если пацан приглашает подругу в театр, они могут вызвать одно такси и поехать туда, а если каждый из них приехал на машине?

Теперь о TheadPool:Изначально в нем нет никаких потоков. Они создаются по-требованию. То есть, если ты их не используешь, то их нет и никакие ресурсы зря не расходуются.
Unknown каже…
Оказывается, пока я правил ошибку в шаблоне своего блога, люди писали комментарии в собственных блогах. Например в Open BeOS
Maxim Sokhatsky каже…
Фишер совершенно прав. К потоку и его созданию нужно относиться не как к ресурсоемкой операции, а наооборот, даже в Windows (где они действительно не маленькие, хотя соизмеримы с потоками, например, Mach).

Имеено в системах которые поддежривают многопоточность нужно активно использовать потоки в приложениях, что бы на многопроцессорных машинах аппликуха максимально грузила ваш паравоз. На однопроцессорных машинах тоже будет достигнут эффект "большего отклика системы", что является показателем "эргономичности". Конечно все зависит от планировщика, например в Линуксе он рваный, поэтому там с этим сложнее, хотя потоки которые делал Инго Молнар имеют довольно современную реализацию. В виндовсе довольно мягкий скедулер, поэтому в виндовсе нужно использовать потоки максимально.

Конечно, должна соблюдаться культура написания многопоточных программ. Самые важные правила:

Асинхронный ввод ввывод, как и вывод вообще должны управляться потоками читалками/писалками. Разделите всю работу с ресурсами посредсвом семафором и вынесете в управляющие потоки работу с данными.

Окно всегда должны разделяться на потоки которые рисуют и потоки которые создают информацию (скачивают). Причем любое окно, любого виндовс приложения (кроме совсем тривиальных хеллоу-ворлд).

Неплохой пример - MTTTY, которое идет как пример в MSDN.

Более того нужно не просто не бояться, а максимально эфективно использовать потоки в приложениях, вследствии чего ваши приложения будут иметь очень хороший отклик для системы и пользователя. Согласитесь не очень приятно, когда программа отработана, а результаты не показываются потому что какие-то проблемы с главным окном приложения.
Unknown каже…
1. Fischer:К сожалению, вся проблема не в объектах потоков, а в самой ф-ии
CreateThread. Для создания нового потока приходится переключаться в режим ядра. Это и есть самая ресурсоемкая операция. Неважно, какие надстройки (обертки вокруг CreateThread) еще участвуют в этом действе. Я не думаю, что хотя-бы одна из них (в популярных библиотеках) сравнима по временным затратам с переходом в режим ядра и обратно. А я думаю, что ф-ия CreateThread делает это не один раз :) Конечно, бывают еще облегченные потоки (CreateFiber), которые полностью в пользовательском режиме делают, но у них другие недостатки...
Я не знаю как это организовано в Java, но думаю что вариантов немного:
- Создавать вызовом CreateThread
- Создавать вызовом CreateFiber
- Использовать на заднем плане пул потоков (основанный на IoCompletionPort или нет, это не так важно)
ИМХО, последний достаточно сильный вариант, но лучше все-таки дать возможность программисту самому решать что делать. После того, как в .NET 2.0 будет поддержка "фибров", можно будет сказать, что .NET это делает. Но самое главное, это два момента:
1. Использовать потоки!
2. Хотя бы 1 секунду обдумывать способ, которым использовать потоки.
2. Joric:Придумай себе другой пример, где два человека приехали в одну и ту же точку на двух машинах, а надо уехать на одной.
Unknown каже…
Неправда. Нет никаких системных потоков и потоков приложения. Они все - системные. Приложение просто ими владеет. Это означает, что если процессу сказать: "УМРИ!", то умрут и все его потоки. А раз все системные, - значит все потоки ресурсоемкие! И работать с ними можно всего двумя способами:
1. Каждый раз создавать новый (прямо или опосредованно).
2. Использовать заранее заготовленные (пул потоков).

Исключение - Fibers. Вот это действительно ненастоящие потоки. Они не требуют переключения в режим ядра. Но они и не планируются системным планировщиком задач. Их планирует само приложение. До недавнего времени в UNIX-ах были только такие. Теперь (уже лет 5-6) есть всякие. А в Windows 95 их по-моему небыло. Может в Java именно такие? Надо спросить у какого-нить спеца.
Unknown каже…
Fischer said...
"В Win есть поток ядра, задача которого давать жить всем тем потокам, которые имеют право получить процессорное время..."
Это не совсем так, хотя так его можно себе представить. Дело в точ, что именно планировщик дает жить потокам, никакого потока ядра нет.

Все потоки в системе одинаковые и ничем кроме базового приоритета не отличаются. Базовый приоритет это значение которое рассчитывается из приоритета потока (заданного при создании потока или установленного ф-ией
SetThreadPriority
) и класса приоритета процесса в котором поток создан.
Например, есть процесс с классом IDLE_PRIORITY_CLASS и процесс с классом HIGH_PRIORITY_CLASS. Оба они создают поток с приоритетом THREAD_PRIORITY_NORMAL. Но поток, который принадлежит процесса с классом приоритета HIGH_PRIORITY_CLASS будет планироваться рашьше чем тот, что принадлежит процессу с IDLE_PRIORITY_CLASS.
Но все они находятся в одной очереди.

P.S. Я знаю, что JVM - рядовая программа, но никто не мешает разработчикам JVM, когда ты их просишь стартовать поток, брать его из какого-то пула или стартовать вместо него фибер. У тебя очень мало возможностей проверить что он сделал на самом деле (также как и в .NET :) )

Популярні дописи з цього блогу

Посчитать количество вхождений каждого слова в текстовом файле

Українська мова