Коли використовувати пул потоків у C #? [зачинено]


127

Я намагався вивчити багатопотокове програмування в C #, і я збентежений, коли найкраще використовувати пул потоків порівняно зі створенням власних потоків. Одна книга рекомендує використовувати пул потоків лише для невеликих завдань (що б це не означало), але я не можу знайти реальних вказівок. Які міркування ви використовуєте при прийнятті цього програмного рішення?

Відповіді:


47

Якщо у вас є багато логічних завдань, які потребують постійної обробки, і ви хочете, щоб це було зроблено паралельно, використовуйте пул + планувальник.

Якщо вам потрібно одночасно виконувати завдання, пов’язані з IO, такі як завантаження матеріалів з віддалених серверів або доступ до диска, але це потрібно робити раз на кілька хвилин, а потім створюйте власні теми та вбивайте їх, коли закінчите.

Редагувати: З деяких міркувань я використовую пули потоків для доступу до бази даних, фізики / моделювання, AI (ігри) та для сценаріїв, що виконуються на віртуальних машинах, які обробляють безліч заданих користувачем завдань.

Зазвичай пул складається з 2-х потоків на один процесор (напевно, це сьогодні 4), проте ви можете встановити потрібну кількість потоків, якщо ви знаєте, скільки вам потрібно.

Редагувати. Причина створювати власні потоки - це через зміну контексту (саме тоді, коли потокам потрібно підмінятись і виходити з процесу разом із їх пам'яттю). Скажімо, не змінюючи контекст, скажіть, коли ви не використовуєте свої теми, просто залишаючи їх сидіти, як можна сказати, може легко знизити продуктивність вашої програми (скажімо, у вас є 3 сплячих нитки та 2 активних нитки). Таким чином, якщо ті завантажуючі нитки просто чекають, вони з'їдають тонни процесора і охолоджують кеш для вашого реального застосування


2
Гаразд, але чи можете ви пояснити, чому саме так ви підходите до цього? Наприклад, у чому полягає мінус використання пулу потоків для завантаження з віддалених серверів або зробити IO диска?

8
Якщо потік чекає на об'єкт синхронізації (подія, семафор, mutex тощо), то потік не споживає ЦП.
Браннон

7
Як сказав Бреннон, загальним міфом є ​​те, що створення декількох ниток впливає на продуктивність. Насправді невикористані нитки споживають дуже мало ресурсів. Контекстні комутатори починають бути проблемою лише на серверах з дуже високим попитом (у цьому випадку альтернативу див. У портах завершення вводу / виводу).
FDCastel

12
Чи впливають холості потоки на продуктивність? Це залежить від того, як вони чекають. Якщо вони добре написані і чекають на об'єкт синхронізації, вони не повинні споживати жодні ресурси процесора. Якщо ви чекаєте в циклі, який періодично прокидається, щоб перевірити результати, то це витрачає процесор. Як завжди, це зводиться до хорошого кодування.
Білл

2
Холості потоки, що управляються, їдять пам'ять для їх стека. За замовчуванням - 1 МіБ на потік. Тож краще, щоб усі потоки працювали.
Вадим Стецяк

48

Я б запропонував вам використовувати пул потоків у C # з тих же причин, що і будь-яка інша мова.

Якщо ви хочете обмежити кількість запущених потоків або не хочете накладних витрат на їх створення та знищення, використовуйте пул потоків.

Невеликими завданнями книга, яку ви читаєте, означає завдання з коротким терміном експлуатації. Якщо для створення потоку, який працює лише одну секунду, потрібно десять секунд, це одне місце, де ви повинні використовувати пули (ігноруйте мої фактичні цифри, це значення враховується).

Інакше ви витрачаєте основну частину свого часу на створення та руйнування ниток, а не просто виконуючи ту роботу, яку вони мають намір робити.


28

Ось приємний підсумок пулу потоків у .Net: http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx

У публікації також є деякі моменти, коли ви не повинні використовувати пул потоків і замість цього починати власну нитку.


8
-1 за посиланням. Я впевнений, що це гарне посилання, але я сподіваюся, що ТА буде самодостатнім.
Джон Девіс

26
@ stimpy77 - це неправильне очікування тоді. ТАК ніколи не може бути самодостатньою, оскільки він не є ані найвищим повноваженням з усіх питань, ані всією глибокою інформацією по кожній темі не може (і повинна) дублюватися в кожній відповіді ТА, яка стосується цієї теми. (і я не думаю, що у вас навіть достатньо репутації, щоб спростувати кожну відповідь Джона Скіта поодинці, яка має вихідне посилання, не кажучи вже про всі відповіді від усіх користувачів
ПС,

2
Можливо, я був надто лаконічним, можливо, я повинен уточнити. Я не проти посилань. Я проти відповідей, які містять лише посилання. Я не думаю, що це відповідь. Тепер, якби було розміщено коротке узагальнення відповіді, щоб узагальнити, як застосовується пов’язаний вміст, це було б прийнятним. Крім того, я прийшов сюди шукати відповідь на ту саму проблему, і ця відповідь роздратувала мене, оскільки це було ще одним посиланням, на яке я повинен був натиснути, щоб мати будь-яке уявлення про те, що це може сказати у зв'язку з конкретною проблемою. У будь-якому випадку, де до цього відноситься Джон Скіт? І чому я повинен дбати?
Джон Девіс

8
"Ви прийшли на цю посаду через два роки після того, як вона була розміщена, і все, що я скопіював тут, можливо, уже застаріло." Так може бути посилання. Опублікуйте стислий, але повний підсумок, коли публікуєте посилання, ви ніколи не знаєте, чи посилання вимкнене чи мертве.
Джон Девіс

2
Я не згоден із стимулом: ні ідея публікацій, що містять багато інформації через зараженість, ні виклик когось із цього приводу. Я б сказав, що більш імовірно, що посилання стає непрацездатним, ніж вміст стає застарілим / закритим, хоча. Тож, більше вмісту приємно, коли це дозволяє. Ми всі (в основному) волонтери, тож будьте вдячні за те, що отримаєте - дякую Franci :)
zanlok

14

Я настійно рекомендую прочитати цю безкоштовну електронну книгу: Threading in C # by Joseph Albahari

Принаймні прочитайте розділ "Початок роботи". Електронна книга надає чудовий вступ та включає також безліч передової інформації про нарізку.

Знання того, використовувати чи ні використовувати пул ниток - це лише початок. Далі вам потрібно буде визначити, який спосіб введення пулу потоків найбільше відповідає вашим потребам:

  • Паралельна бібліотека завдань (.NET Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • Асинхронні делегати
  • BackgroundWorker

Ця електронна книга пояснює все це та радить коли їх використовувати порівняно зі створенням власної нитки.


8

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

Тепер, якби кожен з цих компонентів мав чергування елементів до пулу потоків, у вас буде набагато менше контекстних комутацій накладних.

Пул потоків призначений для максимізації роботи, яка виконується у ваших процесорах (або ядрах процесора). Ось чому за замовчуванням пул потоків закручує кілька потоків на один процесор.

Є деякі ситуації, коли ви не хочете використовувати пул потоків. Якщо ви чекаєте вводу-виводу або очікуєте події тощо, тоді ви зв’яжете цю нитку пулу ниток, і її ніхто не може використовувати. Ця ж ідея стосується і довгих завдань, що виконуються, хоча те, що становить тривале завдання, є суб'єктивним.

Pax Diablo також добре підходить. Скручування ниток не є безкоштовним. Це потребує часу, і вони споживають додаткову пам’ять для свого простору. Пул потоків повторно використовуватиме потоки для амортизації цієї вартості.

Примітка. Ви запитували про використання потоку пулу потоків для завантаження даних або виконання вводу-виводу диска. Не слід використовувати для цього нитку пулу ниток (з причин, які я описав вище). Замість цього використовуйте асинхронний введення-вивід (він же методи BeginXX і EndXX). Бо таке, FileStreamщо було б BeginReadі EndRead. Бо це HttpWebRequestбуло б BeginGetResponseі EndGetResponse. Вони складніші у використанні, але вони є правильним способом виконання багатопотокового вводу / виводу.


1
ThreadPool - це розумна автоматизація. "Якщо його черга залишається нерухомою більше півсекунди, вона реагує, створюючи більше потоків - кожну кожні півсекунди - до ємності пулу потоків" ( albahari.com/threading/#_Optimizing_the_Thread_Pool ). Також майже асинхронні операції з BeginXXX-EndXXX використовуються через ThreadPool. Тому нормально використовувати ThreadPool для завантаження даних і часто неявно використовується.
Артру

6

Остерігайтеся пулу потоків .NET для операцій, які можуть блокувати будь-яку значну, змінну або невідому частину їх обробки, оскільки це схильне голодування ниток. Подумайте про використання паралельних розширень .NET, які забезпечують велику кількість логічних абстракцій над потоковими операціями. Вони також включають новий планувальник, який повинен бути вдосконаленням у ThreadPool. Дивіться тут


2
Ми виявили це важким шляхом! ASP.Net використовує Threadpool з'являється, і тому ми не могли використовувати його так агресивно, як хотілося б.
нооцит

3

Однією з причин використовувати пул потоків лише для невеликих завдань є те, що існує обмежена кількість потоків пулу потоків. Якщо він використовується тривалий час, він зупиняє використання цієї нитки іншим кодом. Якщо це трапляється багато разів, тоді пул потоків може бути використаний.

Використання пулу потоків може мати тонкі ефекти - деякі .NET-таймери використовують нитки пулу потоків і, наприклад, не спрацьовують.


2

Якщо у вас є фонове завдання, яке триватиме довгий час, як і впродовж усього терміну служби вашої програми, то створити власну нитку - справа розумна. Якщо у вас є короткі завдання, які потрібно виконати в потоці, то використовуйте об'єднання ниток.

У програмі, де ви створюєте багато ниток, накладні витрати на створення ниток стають істотними. Використання пулу ниток створює нитки один раз і повторно використовує їх, таким чином уникаючи створення потоку над головою.

У програмі, над якою я працював, перехід від створення ниток до використання пулу ниток для короткотривалих потоків справді допоміг пройти через додаток.


Будь ласка, уточніть, якщо ви маєте на увазі "пул потоків" або "пул потоків". Це дуже різні речі (принаймні в MS CLR).
bzlm

2

Для отримання максимальної продуктивності з одночасно виконаними одиницями, напишіть власний пул потоків, де пул об’єктів нитки створюється при запуску і переходить до блокування (раніше призупинено), очікуючи запуску контексту (об'єкт зі стандартним інтерфейсом, реалізований ваш код).

Так багато статей про Tasks vs. Threads vs. .NET ThreadPool насправді не дають тобі, що потрібно для прийняття рішення про ефективність. Але коли їх порівнювати, нитки виграють і особливо пул ниток. Вони розподіляються найкраще по процесорам, і вони запускаються швидше.

Що слід обговорити, це той факт, що головним блоком виконання Windows (включаючи Windows 10) є потік, а контекстна комутація накладних режимів ОС зазвичай незначна. Простіше кажучи, я не зміг знайти переконливі докази багатьох цих статей, чи вимагає ця стаття більш високу ефективність завдяки збереженню переключення контексту або кращому використанню процесора.

Тепер трохи реалізму:

Більшості з нас не потрібно, щоб наш додаток був детермінованим, і більшість з нас не має жорсткого тла з нитками, що, наприклад, часто відбувається при розробці операційної системи. Те, що я писав вище, не для початківців.

Тож, що може бути найважливішим, - це обговорити, що легко запрограмувати.

Якщо ви створюєте власний пул потоків, вам доведеться трохи написати, оскільки вам потрібно буде займатися відстеженням стану виконання, як імітувати призупинення та відновлення та скасування виконання - в тому числі в додатку закрити. Можливо, вам також доведеться перейматися тим, чи хочете ви динамічно розростати свій пул, а також обмеженням потужності вашого пулу. Я можу написати таку рамку за годину, але це тому, що я робив це стільки разів.

Мабуть, найпростіший спосіб написання одиниці виконання - це використання Завдання. Краса Завдання полягає в тому, що ви можете створити її і відкинути її в рядку у своєму коді (хоча може бути обережним). Ви можете передати маркер скасування для обробки, коли ви хочете скасувати завдання. Крім того, він використовує підхід, який обіцяє прив'язувати події, і ви можете мати можливість повернути йому певний тип значення. Більше того, з асинхронізацією та очікуванням існує більше варіантів, і ваш код стане більш портативним.

По суті, важливо зрозуміти плюси і мінуси Завдання проти ниток проти .NET ThreadPool. Якщо мені потрібна висока продуктивність, я збираюся використовувати теми, і я вважаю за краще використовувати власний пул.

Простий спосіб порівняння - це запуск 512 ниток, 512 завдань та 512 ниток потоку пулу. Ви знайдете затримку на початку з Threads (отже, чому писати пул потоків), але всі 512 теми будуть запущені за кілька секунд, тоді як Tasks і .NET ThreadPool запускають до декількох хвилин, щоб усі почалися.

Нижче наводяться результати такого тесту (чотириядерний i5 з 16 ГБ оперативної пам’яті), що дає кожні 30 секунд для запуску. Виконаний код виконує прості введення / виведення файлів на SSD-накопичувачі.

Результати тесту


1
FYI, забув згадати, що Завдання та .NET Threads моделюються одночасністю в .NET і при виконанні управління в .NET, а не в ОС - остання набагато ефективніша в управлінні одночасними виконаннями. Я використовую Завдання для багатьох речей, але використовую нитку ОС для високої продуктивності виконання. MS стверджує, що Завдання та .NET потоки кращі, але вони, як правило, для збалансування одночасності серед програм .NET. Однак серверне додаток найкраще дасть змогу операційній системі керувати одночасністю.

Хочеться побачити реалізацію вашого власного Threadpool. Приємно писати!
Френсіс

Я не розумію ваших результатів тесту. Що означають дати "Units Ran"? Ви порівнюєте 34 такси з 512 нитками? Чи поясніть, будь ласка, це?
Елмуе

Блок - це лише метод одночасного виконання робочої нитки Task, Thread або .NET ThreadPool, мій тест, порівнюючи продуктивність запуску / запуску. Кожен тест має 30 секунд для нерестування 512 ниток з нуля, 512 завдань, 512 робочих ниток ThreadPool або відновлення пулу 512 запущених ниток, які очікують контексту для виконання. Робочі потоки Tasks and ThreadPool мають повільне згортання, тому 30 секунд не вистачає часу, щоб розкрутити їх. Однак якщо кількість робочих ниток ThreadPool min спочатку встановлено на 512, і завдання Task, і робочі нитки ThreadPool закрутяться майже так само швидко, як 512 ниток з нуля.


1

Пули ниток чудові, коли у вас більше завдань для обробки, ніж доступні теми.

Ви можете додати всі завдання до пулу потоків і вказати максимальну кількість потоків, які можуть працювати в певний час.

Перевірте цю сторінку в MSDN: http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx


Гаразд, я думаю, це пов'язане з моїм іншим питанням. Звідки ви знаєте, скільки доступних тем у вас в даний момент?

Ну, важко сказати. Вам доведеться зробити тестування продуктивності. Після точки додавання більше ниток не дасть вам більшої швидкості. Дізнайтеся, скільки процесорів на машині, це буде хорошою відправною точкою. Потім перейдіть звідти, якщо швидкість обробки не покращиться, не додайте більше потоків.
lajos

1

Завжди використовуйте пул ниток, якщо можете, працюйте на максимально можливому рівні абстрагування. Басейни з нитками приховують створення та руйнування ниток, це, як правило, добре!


1

Більшу частину часу ви можете використовувати пул, оскільки уникнете дорогого процесу створення нитки.

Однак у деяких сценаріях ви можете створити потік. Наприклад, якщо ви не єдиний, хто використовує пул потоків, і створений вами потік є довговічним (щоб уникнути споживання спільних ресурсів) або, наприклад, якщо ви хочете контролювати розмір нитки потоку.


1

Не забудьте дослідити фонового працівника.

Я знаходжу для багатьох ситуацій, це дає мені просто те, що я хочу без важкого підйому.

Ура.


коли це проста програма, яка залишається запущеною, і у вас є ще одне завдання зробити, дуже легко зробити цей код. Ви не надали посилань: специфікація та навчальний посібник
zanlok

0

Зазвичай я використовую Threadpool, коли мені потрібно щось зробити на іншій нитці і мені зовсім не байдуже, коли він працює чи закінчується. Щось на кшталт реєстрації або, можливо, навіть фонового завантаження файлу (хоча є кращі способи зробити це в стилі асинхрон). Я використовую власну нитку, коли мені потрібно більше контролю. Також те, що я знайшов, - це використання черги Threadsafe (зламати свій власний) для зберігання "командних об'єктів", це добре, коли у мене є кілька команд, над якими мені потрібно працювати над> 1 потоком. Таким чином, ви можете розділити Xml-файл і поставити кожен елемент у чергу, а потім мати кілька потоків, які працюють над деякою обробкою цих елементів. Я написав таку чергу назад у uni (VB.net!), Що перетворився на C #. Я включив його нижче без особливих причин (цей код може містити деякі помилки).

using System.Collections.Generic;
using System.Threading;

namespace ThreadSafeQueue {
    public class ThreadSafeQueue<T> {
        private Queue<T> _queue;

        public ThreadSafeQueue() {
            _queue = new Queue<T>();
        }

        public void EnqueueSafe(T item) {
            lock ( this ) {
                _queue.Enqueue(item);
                if ( _queue.Count >= 1 )
                    Monitor.Pulse(this);
            }
        }

        public T DequeueSafe() {
            lock ( this ) {
                while ( _queue.Count <= 0 )
                    Monitor.Wait(this);

                return this.DeEnqueueUnblock();

            }
        }

        private T DeEnqueueUnblock() {
            return _queue.Dequeue();
        }
    }
}

Деякі проблеми з цим підходом: - Виклики до DequeueSafe () будуть чекати, поки елемент не стане EnqueuedSafe (). Спробуйте скористатися одним із перевантажень Monitor.Wait () із зазначенням тайм-ауту. - Блокування цього не відповідає найкращим практикам, скоріше створіть об'єктне поле лише для читання. - Навіть незважаючи на те, що Monitor.Pulse () легкий, його виклик, коли в черзі міститься лише 1 елемент, був би більш ефективним. - DeEnqueueUnblock () бажано перевірити чергу.Count> 0. (потрібно, якщо використовується Monitor.PulseAll або очікують очікування)
Craig Nicholson

0

Я хотів, щоб потоковий пул розподіляв роботу по ядрах з якомога меншою затримкою, і це не повинно було добре грати з іншими програмами. Я виявив, що продуктивність пулу потоків .NET не була такою хорошою, як могла бути. Я знав, що хочу один потік на ядро, тому написав власний клас замінника пулу потоків. Код надається як відповідь на інше питання StackOverflow тут .

Що стосується оригінального питання, пул потоків корисний для розбиття повторюваних обчислень на частини, які можна виконати паралельно (якщо припустити, що вони можуть виконуватися паралельно без зміни результату). Ручне управління нитками корисно для таких завдань, як UI та IO.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.