Интересности      Книги      Утилиты    

30 июня 2011 г.

Как работает Thread Pool?

clip_image001

Пул потоков

Известно, что создание, уничтожение, переключение между потоками – это дорогостоящая операция. Для того чтобы избежать накладных расходов связанных с этим, основной идеей пула потоков в .NET стало уменьшение числа задействованных потоков и увеличение выполненной ими работы. Поэтому в пуле нас всегда ждет определенное известное CLR количество потоков готовых на выполнение задач. И именно поэтому почти все книги про .NET говорят, что для быстрого создания и выполнения потока ThreadPool.QueueUserWorkItem годится, а метод Start класса Thread нет.

В .NET 3.5 и ранее Thread Pool состоит из глобальной очереди, в которую попадают задачи на выполнение из потока нашего приложения и определенного числа рабочих потоков – worker threads, всегда готовых прийти на помощь. Именно рабочие потоки разбирают задачи из глобальной очереди на выполнение, и когда они выполнят задачу – они обращаются в глобальную очередь за новой порцией. Но так как потоки разбирают задачи одновременно, то для того чтобы два рабочих не взяли одну и ту же порцию – существует синхронизация между ними посредством блокировок. Блокировки же могут стать «бутылочным горлышком» этого механизма и производительность из-за этого упадет.


clip_image003

С добавлением большего числа ядер задачи начинают делиться на более мелкие кусочки. Соответственно рабочие потоки чаще обращаются в глобальную очередь, нагрузка на которую явно возрастает – дает о себе знать блокировка.

Хорошая аналогия для демонстрации того как работает пул в .net 3.5 – это процесс разработки программного обеспечения (ПО). Написать ПО в более короткие сроки можно за счет увеличения людей на проекте. И если это один, два или три разработчика – то на синхронизацию совместных действий у них уйдет немного времени. Но что будет, если мы начнем добавлять все больше и больше разработчиков с целью как можно более сжать сроки? Тогда для координации действий им потребуются совещания, совещания, совещания… много совещаний (meetings). И теперь представим себе одно большое совещание на 40 человек, где каждый будет обсуждать свой кусок работы и координировать свои действия с коллегами. Интересно насколько долго будут затягиваться такие совещания и насколько мучительными они будут для участников.

clip_image004

Увеличение числа ядер в системе аналогично увеличению числа участников, так как задачи будут делиться на более мелкие кусочки и вследствие частого обращения в глобальную очередь – увеличится необходимость в синхронизации.

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

Наверное, используя эту аналогию, разработчики из Microsoft Reasearch (pfx team) пересмотрели пул потоков. Ведь число ядер растет, и нагрузка в будущем на глобальную очередь только возрастала бы. Поэтому нужно было что-то делать. И они сделали – новый пул потоков.

Новый пул потоков работает немного другим образом. Для каждого рабочего потока в пуле существует своя локальная очередь. Задача, которая попала в локальную очередь, может породить дочернюю и они размещаются в эту же локальную очередь. Также задачи выполняются в локальной очереди в LIFO (last-in-first-out) порядке, в отличие от пула в 3.5, где задачи из глобальной очереди выполняются рабочими потоками в FIFO (first-in-first-out) порядке. Так как только рабочий поток разгребает свою собственную кучу, вернее имеет доступ к ее началу (head), то на уровне локальной очереди не требуется никаких синхронизаций. Поэтому добавление и извлечение задачи из этой очереди происходят очень быстро. Побочным эффектом такого выполнения является то, что очередь разгребается в обратном порядке. Хотя делать какие-то допущения в программе о порядке выполнения задач в очереди нельзя – так как пул потоков своеобразный черный ящик.

clip_image006

Когда рабочий поток видит что делать ему больше нечего, то есть его локальная очередь пустая, он пытается своровать задачу у другого рабочего потока. Но ворует он элементы с хвоста чужой локальной очереди. Такое действие уже требует синхронизации. И производительность немного снизится. Если же все локальные очереди пустые, то рабочий поток попытается забрать задачу из глобальной очереди в FIFO порядке. Если же глобальная очередь окажется пустой, тогда рабочий поток «заснет» пока не появится работа для него. Если рабочий поток проспал слишком много времени и работы так и не получил, он проснется и уничтожится, освободив занимаемые ресурсы.

Почему глобальная очередь на картинке lock-free, если все равно нужна синхронизация для доступа к ней рабочих потоков из пула?. Потому что она действительно lock-free, теперь для синхронизации используются не lock-и (Monitor), а некий аналог interlocked операций и это позволило увеличить быстродействие очереди.

Task Parallel Library

Вместе с улучшенным пулом потоков в .NET 4 также появляется и расширенное API для работы с потоками и пулами потоков - Task Parallel Library (TPL), библиотека, которая обладает более широким набором средств для взаимодействия с потоками. Основным элементом TPL является тип Task.

Task – это по сути своей абстракция над потоками, которая была создана, чтобы дать возможность среде эффективно управлять распределением работы по потокам и их созданием. Поэтому Task дает пулу потоков значительно больше информации о работе, которую выполняет поток. Одна из особенностей работы с Task’ами – это связь parent/child, которая дает понять пулу потоков об общей структуре вычислений, которые производятся отдельными потоками или задачами (work items). Обладая такой информацией, пул потоков может более эффективно распределять работу между потоками. А значит, эффективность пула и общая производительность повышаются. А то, что между родительской и дочерней задачей существует связь, позволяет использовать преимущества процессорного кэша. Так как возможно данные, с которыми оперировала родительская задача, останутся в кэше. При отсутствии взаимосвязей между задачами или потоками вероятность этого значительно ниже.

7 комментариев:

  1. Бесполезная статья,конкретики мало

    ОтветитьУдалить
  2. А какая конкретника вам нужна?

    ОтветитьУдалить
  3. В ознакомительных целях статья очень даже полезна, спасибо автору.

    ОтветитьУдалить
  4. Если кому-то нужна конкретика, то - http://msdn.microsoft.com/ru-ru/library/dd460717.aspx
    А смысла переносить в блог MSDN - нет.

    ОтветитьУдалить
  5. я недавно делал презентацию по Task Menagement для коллег, жаль, что не нашел эту статью до этого, в ней все отлично расписано, не надо было перелопачивать литературу, просто удовольствие читать...

    ОтветитьУдалить
  6. Отличная статья. Спасибо!

    ОтветитьУдалить
  7. Много чего не ясно. Как, например, задачи перемещаются в локальные очереди и кто их перемещает и откуда, из глобальной очереди?
    Какие объекты синхронизации используются.
    Да, и между прочим, проверять что локальная очередь непуста / записывать в локальную очередь - это тоже синхронизацию нужно использовать. Короче, вопросы тонкие.
    В теории всё звучит хорошо)

    ОтветитьУдалить