Что такое пул потоков
Перейти к содержимому

Что такое пул потоков

  • автор:

Пул управляемых потоков

Класс System.Threading.ThreadPool обеспечивает приложение пулом рабочих потоков, управляемых системой, позволяя пользователю сосредоточиться на выполнении задач приложения, а не на управлении потоками. Если имеются небольшие задачи, которые требуют фоновой обработки, пул управляемых потоков — это самый простой способ воспользоваться преимуществами нескольких потоков. В Framework 4 и более поздних версиях использовать пул потоков стало значительно проще, так как вы можете создавать объекты Task и Task , которые выполняют в потоках пула асинхронные задачи.

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

Характеристики пула потоков

Потоки из пула являются фоновыми. Для каждого потока используется размер стека по умолчанию, поток запускается с приоритетом по умолчанию и находится в многопотоковом подразделении. Когда поток в пуле завершает свою задачу, он возвращается в очередь потоков в состоянии ожидания. С этого момента его можно использовать вновь. Повторное использование позволяет приложениям избежать дополнительных затрат на создание новых потоков для каждой задачи.

Для каждого процесса существует только один пул потоков.

Исключения в потоках из пула потоков

Необработанные исключения в потоках из пула приводят к завершению процесса. Есть три исключения из этого правила:

  • Исключение System.Threading.ThreadAbortException возникает в потоке пула вследствие вызова Thread.Abort.
  • Исключение System.AppDomainUnloadedException возникает в потоке пула вследствие выгрузки домена приложения.
  • Среда CLR или процесс ведущего приложения прерывает выполнение потока.

Максимальное число потоков в пуле потоков

Число операций, которое можно поставить в очередь в пуле потоков, ограничено только доступной памятью. Однако пул потоков имеет ограничение на число потоков, которое можно активировать в процессе одновременно. Если все потоки в пуле заняты, дополнительные рабочие элементы помещаются в очередь и ожидают их освобождения. Размер по умолчанию пула потоков для процесса зависит от нескольких факторов, таких как размер виртуального адресного пространства. Процесс может вызвать метод ThreadPool.GetMaxThreads для определения количества потоков.

Вы можете управлять максимальным количеством потоков с помощью методов ThreadPool.GetMaxThreads и ThreadPool.SetMaxThreads.

В коде, содержащем среду CLR, этот размер можно задать с помощью метода ICorThreadpool::CorSetMaxThreads .

Минимальные значения пула потоков

Пул потоков предоставляет новые рабочие потоки или потоки завершения ввода-вывода по запросу, пока не будет достигнут заданный минимум для каждой категории. Для получения этих минимальных значений можно использовать метод ThreadPool.GetMinThreads.

Если потребность низкая, фактическое количество потоков из пула потоков может быть ниже минимальных значений.

При достижении минимума пул потоков может создавать дополнительные потоки или ожидать завершения некоторых задач. Пул потоков создает и уничтожает рабочие потоки в целях оптимизации пропускной способности, которая определяется как количество задач, завершаемых за единицу времени. Слишком малое количество потоков может препятствовать оптимальному использованию доступных ресурсов, тогда как слишком большое их количество может усиливать конкуренцию за ресурсы.

Для увеличения минимального количества бездействующих потоков можно использовать метод ThreadPool.SetMinThreads. Однако необоснованное увеличение этих значений может привести к снижению производительности. Если одновременно запускается слишком много задач, все они могут выполняться слишком медленно. В большинстве случаев пул потоков работает наилучшим образом, если он использует собственный алгоритм выделения потоков.

Использование пула потоков

Самым простым способом использования пула потоков является применение библиотеки параллельных задач (TPL). По умолчанию такие типы TPL, как Task и Task , используют потоки из пула для выполнения задач.

Пул потоков также можно использовать путем вызова ThreadPool.QueueUserWorkItem из управляемого кода (или ICorThreadpool::CorQueueUserWorkItem из неуправляемого кода) и передачи делегата System.Threading.WaitCallback, представляющего метод, который выполняет задачу.

Другим способом использования пула потоков является помещение в очередь рабочих элементов, которые имеют отношение к операции ожидания, с помощью метода ThreadPool.RegisterWaitForSingleObject и передача дескриптора System.Threading.WaitHandle, который вызывает метод, представленный делегатом System.Threading.WaitOrTimerCallback, при получении сигнала или истечении времени ожидания. Потоки из пула потоков используются для вызова методов обратного вызова.

Примеры см. по ссылкам на страницы API.

Пропуск проверок безопасности

Пул потоков также предоставляет методы ThreadPool.UnsafeQueueUserWorkItem и ThreadPool.UnsafeRegisterWaitForSingleObject. Используйте эти методы только в том случае, если вы уверены, что стек вызывающего объекта не важен для проверок безопасности, осуществляемых во время выполнения задачи в очереди. ThreadPool.QueueUserWorkItem и ThreadPool.RegisterWaitForSingleObject перехватывают стек вызывающего объекта, который объединяется со стеком потока из пула потоков, когда поток начинает выполнять задачу. Если требуется проверка безопасности, проверяется весь стек. Несмотря на обеспечение безопасности, такая проверка также влияет на производительность.

Когда не следует использовать потоки из пула потоков

Существует ряд сценариев, в которых следует создавать собственные потоки и работать с ними, а не использовать потоки из пула:

  • Необходим основной поток.
  • Поток должен иметь определенный приоритет.
  • Имеются задачи, которые приводят к блокировке потока на длительное время. Для пула потоков определено максимальное количество потоков, поэтому большое число заблокированных потоков в пуле может препятствовать запуску задач.
  • Необходимо поместить потоки в однопотоковое подразделение. Все потоки ThreadPool находятся в многопотоковом подразделении.
  • Необходимо иметь постоянное удостоверение, сопоставленное с потоком, или назначить поток задаче.

См. также

  • System.Threading.ThreadPool
  • System.Threading.Tasks.Task
  • System.Threading.Tasks.Task
  • Библиотека параллельных задач (TPL)
  • Практическое руководство. Возвращение значения из задачи
  • Объекты и функциональные возможности работы с потоками
  • Потоки и работа с потоками
  • Асинхронный файловый ввод-вывод
  • Таймеры

Совместная работа с нами на GitHub

Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

Пулы потоков

Пул потоков — это коллекция рабочих потоков, которые эффективно выполняют асинхронные обратные вызовы от имени приложения. Пул потоков в основном используется для уменьшения количества потоков приложения и обеспечения управления рабочими потоками. Приложения могут ставить рабочие элементы в очередь, связывать работу с дескрипторами ожидания, автоматически ставить в очередь на основе таймера и выполнять привязку с помощью операций ввода-вывода.

Архитектура пула потоков

Использование пула потоков может быть полезным для следующих приложений:

  • Приложение с высокой степенью параллелизма и может асинхронно отправлять большое количество небольших рабочих элементов (например, распределенный поиск по индексу или сетевой ввод-вывод).
  • Приложение, которое создает и уничтожает большое количество потоков, каждый из которых выполняется в течение короткого времени. Использование пула потоков может снизить сложность управления потоками и снизить затраты, связанные с созданием и уничтожением потоков.
  • Приложение, которое обрабатывает независимые рабочие элементы в фоновом режиме и параллельно (например, загружает несколько вкладок).
  • Приложение, которое должно выполнять монопольное ожидание объектов ядра или блокировать входящие события в объекте. Использование пула потоков может снизить сложность управления потоками и повысить производительность за счет уменьшения количества переключений контекста.
  • Приложение, которое создает пользовательские потоки ожидания для ожидания событий.

Исходный пул потоков был полностью перепроектирован в Windows Vista. Новый пул потоков улучшен, так как он предоставляет один тип рабочего потока (поддерживает как операции ввода-вывода, так и другие), не использует поток таймера, предоставляет одну очередь таймера и выделенный постоянный поток. Он также предоставляет группы очистки, более высокую производительность, несколько пулов для каждого процесса, которые планируются независимо, и новый API пула потоков.

Архитектура пула потоков состоит из следующих компонентов:

  • Рабочие потоки, выполняющие функции обратного вызова
  • Потоки официанта, ожидающие нескольких дескрипторов ожидания
  • Рабочая очередь
  • Пул потоков по умолчанию для каждого процесса
  • Фабрика рабочих ролей, управляющая рабочими потоками

Рекомендации

Новый API пула потоков обеспечивает большую гибкость и управление, чем исходный API пула потоков. Однако есть несколько незначительных, но важных различий. В исходном API сброс ожидания был автоматическим; в новом API ожидание должно быть явно сброшено каждый раз. Исходный API автоматически обрабатывал олицетворение, передав контекст безопасности вызывающего процесса в поток. При использовании нового API приложение должно явно задать контекст безопасности.

Ниже приведены рекомендации по использованию пула потоков.

  • Потоки процесса совместно используют пул потоков. Один рабочий поток может выполнять несколько функций обратного вызова по одной за раз. Этими рабочими потоками управляет пул потоков. Поэтому не следует завершать поток из пула потоков путем вызова TerminateThread в потоке или метода ExitThread из функции обратного вызова.
  • Запрос ввода-вывода может выполняться в любом потоке в пуле потоков. Для отмены ввода-вывода в потоке пула потоков требуется синхронизация, так как функция отмены может выполняться в потоке, отличном от потока, обрабатывающего запрос ввода-вывода, что может привести к отмене неизвестной операции. Чтобы избежать этого, всегда укажите структуру OVERLAPPED , с которой был инициирован запрос ввода-вывода при вызове CancelIoEx для асинхронного ввода-вывода, или используйте собственную синхронизацию, чтобы убедиться, что другие операции ввода-вывода не могут быть запущены в целевом потоке перед вызовом функции CancelSynchronousIo или CancelIoEx .
  • Очистите все ресурсы, созданные в функции обратного вызова, перед возвратом из функции. К ним относятся TLS, контексты безопасности, приоритет потока и регистрация COM. Функции обратного вызова также должны восстановить состояние потока перед возвратом.
  • Поддерживайте дескриптор ожидания и связанные с ними объекты, пока пул потоков не сообщит о завершении работы с дескриптором.
  • Пометьте все потоки, ожидающие длительных операций (таких как очистка операций ввода-вывода или очистка ресурсов), чтобы пул потоков мог выделять новые потоки вместо этого.
  • Перед выгрузкой библиотеки DLL, которая использует пул потоков, отмените все рабочие элементы, операции ввода-вывода, операции ожидания и таймеры и дождитесь завершения обратных вызовов.
  • Избегайте взаимоблокировок, устраняя зависимости между рабочими элементами и обратными вызовами, гарантируя, что обратный вызов не ожидает завершения, а также сохраняя приоритет потока.
  • Не помещайте слишком много элементов в очередь слишком быстро в процессе с другими компонентами, используя пул потоков по умолчанию. Для каждого процесса существует один пул потоков по умолчанию, включая Svchost.exe. По умолчанию каждый пул потоков имеет не более 500 рабочих потоков. Пул потоков пытается создать больше рабочих потоков, если число рабочих потоков в состоянии готовности или выполнения должно быть меньше числа процессоров.
  • Избегайте модели однопотокового подразделения COM, так как она несовместима с пулом потоков. STA создает состояние потока, которое может повлиять на следующий рабочий элемент потока. STA, как правило, является долгосрочным и имеет сходство потоков, которое является противоположностью пула потоков.
  • Создайте новый пул потоков для управления приоритетом и изоляцией потоков, создания пользовательских характеристик и, возможно, повышения скорости реагирования. Однако для дополнительных пулов потоков требуется больше системных ресурсов (потоков, памяти ядра). Слишком много пулов увеличивает вероятность состязания за ЦП.
  • По возможности используйте объект с возможностью ожидания вместо механизма на основе APC, чтобы сообщить потоку пула потоков. APC не работают так же хорошо с потоками пула потоков, как и другие механизмы сигнализации, так как система управляет временем существования потоков пула потоков, поэтому поток может быть прерван до доставки уведомления.
  • Используйте расширение отладчика пула потоков ! tp. Эта команда используется следующим образом:
    • флаги адресовпула
    • Флагиадреса obj
    • флагиадресов tqueue
    • адрес официанта
    • рабочий адрес

    Для пула, официанта и рабочей роли, если адрес равен нулю, команда создает дампы всех объектов. Для официанта и рабочей роли пропуск адреса создает дамп текущего потока. Определены следующие флаги: 0x1 (однострочный вывод), 0x2 (члены дампа) и 0x4 (рабочая очередь пула дампов).

    Как работает пул потоков?

    В .NET есть пул потоков, — это заготовленные потоки, готовые к выполнению какой-то задачи. Но при ручном создании потока нету возможности создать поток без вызова (т.е. на будущее) делегата и более того, если поток отработал, то ему нельзя дать другую задачу. Так как эта магия происходит? Даже в WinApi, вроде, нету функции, которой можно было бы передать уже созданному потоку некоторое задание.

    Отслеживать
    1,248 3 3 золотых знака 23 23 серебряных знака 51 51 бронзовый знак
    задан 26 дек 2019 в 14:03
    1 1 1 серебряный знак 1 1 бронзовый знак

    В .NET пул потоков контроллируется самой библиотекой, вы работаете с потоками с помощью стандартных ф-ций, и не беспокоитесь о пуле, библиотека автоматически ведёт пул.

    26 дек 2019 в 14:06

    @nick_n_a это понятно. Но мне интересно, как заранее заготовленному потоку передается моя задача, если Thread не имеет для этого спец. методов, а в WinApi, вроде, функций нету для привязки задачи к существующему потоку.

    26 дек 2019 в 14:09
    Более детально кажись тут docs.microsoft.com/ru-ru/dotnet/api/system.threading.threadpool
    26 дек 2019 в 14:11

    Библиотека нет — обверточная. В Thread будет передана ф-ция «прокладка», которая может подобрать любую «задачу» (как вы её назвали). Вы не видя код запуска Thread не можете сказать что он выполняет, вот в этот код и добавлено доп-функции.

    26 дек 2019 в 14:13

    Нет, условно говоря, коллекция свободных потоков (пул) из неё выбирается «свободный» поток, которому даётся именно то задание («задача»), через делегат, которое вы прямо сейчас требуете выполнить в паралеле. Выполнение выбраного потока «размораживается» (thread resume, либо через WaitForSingleObject не смотрел детально как сделано) после чего он после разморозки через делегат получит «задачу» , а паралельное выполнение подхватывает уже ОС.

    26 дек 2019 в 14:28

    1 ответ 1

    Сортировка: Сброс на вариант по умолчанию

    Пул потоков – это коллекция потоков для выполнения методов в фоновом режиме. Распределение пулов занимается CLR. Что бы выполнить метод асинхронно необходимо поместить его в очередь потока, пул потоков будет сам извлекать методы из очереди и выполнять их. При наличии большого количества методов, будет создан еще один фоновый поток. Когда поток отработает, он не уничтожается, а возвращается в пул поток для ожидания следующей задачи.

    ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomething)); 

    Для более гибкой работы с потоками есть Task(Задача), которая может использовать так называемое продолжение (ContinueWith)

    Task task = new Task(new Action(Method1)); Task continue = task.ContinueWith(new Action(Method2)); 

    Так же у задачи есть возможность «подготовки» к выполнению + отложенный стар.

    Task task = new Task(new Action(DoSomething), TaskCreationOptions.PreferFairness | TaskCreationOptions.LongRunning); 

    «Холодный» запуск задачи

    Task task = new Task(new Action(Method1)); //что-то делаем Task.Start(); 

    «Горячий» запуск задачи

    Task.Run(new Action(Method)); 

    Выполнение в основном потоке

    Task task = new Task(new Action(Method)); Tast.RunSynchronously(); 

    ну и нововведение ValueTask — обертка над самой задачей

    System.Treading.Tasks.Extensions 

    Пулы потоков

    Потоки (thread) в приложении можно разделить на три категории:

    1. Нагружающие процессор (CPU bound).
    2. Блокирующие ввод-вывод (Blocking IO).
    3. Неблокирующие ввод-вывод (Non-blocking IO).

    У каждой из этих категорий своя оптимальная конфигурация и применение.

    Для задач, требующих процессорного времени, нужен пул с заранее созданными потоками с количеством потоков равным числу процессоров. Единственная работа, которая будет выполняться в этом пуле, — вычисления на процессоре, и поэтому нет смысла превышать их количество, если только у вас не какая-то специфическая задача, способная использовать Hyper-threading (в таком случае вы можете использовать удвоенное количество процессоров). Обратите внимание, что в старом подходе «количество процессоров + 1» речь шла о смешанной нагрузке, когда объединялись CPU-bound и IO-bound задачи. Мы не будем такого делать.

    Проблема с фиксированным пулом потоков заключается в том, что любая блокирующая операция ввода-вывода (да и вообще любая блокирующая операция) «съест» поток, а поток — очень ценный ресурс. Получается, что нам нужно любой ценой избегать блокировки CPU-bound пула. Но к сожалению, это не всегда возможно (например, при использовании библиотек с блокирующим вводом-выводом). В этом случае всегда следует переносить блокирующие операции (ввод-вывод и другие) в отдельный пул. Этот отдельный пул должен быть кэшируемым и неограниченным, без предварительно созданных потоков. Честно говоря, такой пул очень опасен. Он не ограничивает вас и позволяет создавать все больше и больше потоков при блокировке других, что очень опасно. Обязательно стоит убедиться, что есть внешние ограничения, то есть существуют высокоуровневые проверки, гарантирующие выполнение в каждый момент времени только фиксированного количества блокирующих операций (это часто делается с помощью неблокирующей ограниченной очереди).

    Последняя категория потоков (если у вас не Swing / SWT) — это асинхронный ввод-вывод. Эти потоки в основном просто ожидают и опрашивают ядро на предмет уведомлений асинхронного ввода-вывода, и пересылают эти уведомления в приложение. Для этой задачи лучше использовать небольшое число фиксированных, заранее выделенных потоков. Многие приложения для этого используют всего один поток! У таких потоков должен быть максимальный приоритет, поскольку производительность приложения будет ограничена ими. Однако вы должны быть осторожны и никогда не выполнять какую-либо работу в этом пуле! Никогда, никогда, никогда. При получении уведомления вы должны немедленно переключиться обратно на CPU-пул. Каждая наносекунда, потраченная на поток (потоки) асинхронного ввода-вывода, добавляет задержки в ваше приложение. Поэтому производительность некоторых приложений можно немного улучшить, сделав пул асинхронного ввода-вывода в 2 или 4 потока, а не стандартно 1.

    Глобальные пулы потоков

    Я часто встречал советы о том, чтобы не использовать глобальные пулы потоков, такие как scala.concurrent.ExecutionContext.global . Этот совет основан на том, что к глобальным пулам потоков может получить доступ произвольный код (часто из библиотек), и вы не можете (легко) гарантировать, что этот код использует пул потоков правильно. Насколько это критично во многом зависит от вашего classpath . Глобальные пулы потоков довольно удобны, но можно создать свои глобальные пулы для приложения.

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

    В любом случае у вас почти всегда будет какой-то синглтон, который будет содержать эти, предварительно настроенные, три пула. Если вы используете неявный ExecutionContext , то неявным стоит сделать CPU-пул, а остальные указывать явно.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *