З цим питанням я надіслав Стівену Тубу, члену команди PFX . Він повернувся до мене дуже швидко, з великою кількістю деталей - тому я просто скопіюю та вставлю сюди його текст. Я не все цитував, оскільки читання великої кількості цитованого тексту в кінцевому підсумку стає менш комфортним, ніж ванільний чорно-білий, але насправді, це Стівен - я не знаю так багато речей :) Я зробив ця вікі-відповідь спільноти відповідей на те, що все добро нижче не є справді моїм вмістом:
Якщо ви зателефонуєте Wait()
до завдання, яке виконано, блокування не буде (воно просто видасть виняток, якщо завдання виконане зі статусом TaskStatus, відмінним від RanToCompletion
, або іншим чином повернеться як nop ). Якщо ви зателефонуєте Wait()
до завдання, яке вже виконується, воно повинно заблокувати, оскільки нічого іншого він розумно не може зробити (коли я кажу, що блокувати, я включаю як справжнє очікування, так і обертання на основі ядра, оскільки воно зазвичай робить суміш обох ). Подібним чином, якщо ви викликаєте Wait()
Завдання, яке має статус Created
або WaitingForActivation
, воно буде блокувати, поки завдання не буде виконане. Жодна з цих ситуацій не обговорюється цікавою справою.
Цікавий випадок, коли ви викликаєте Wait()
Завдання в WaitingToRun
штаті, це означає, що воно раніше стояло в черзі до TaskScheduler, але що TaskScheduler ще не встиг фактично запустити делегат Завдання. У цьому випадку дзвінок до Wait
запитає планувальник, чи нормально запустити Завдання тоді і там на поточному потоці, через виклик TryExecuteTaskInline
методу планувальника . Це називається вкладанням . Планувальник може вибрати або вбудувати завдання за допомогою виклику base.TryExecuteTask
, або може повернути 'false', щоб вказати, що він не виконує завдання (часто це робиться з логікою, як ...
return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
Причиною TryExecuteTask
повернення логічного значення є те, що він обробляє синхронізацію, щоб забезпечити виконання даного Завдання лише один раз). Отже, якщо планувальник хоче повністю заборонити вбудовування Завдання під час Wait
, його можна просто реалізувати як return false;
Якщо планувальник хоче завжди дозволяти вбудовування, коли це можливо, його можна просто реалізувати як:
return TryExecuteTask(task);
У поточній реалізації (і .NET 4, і .NET 4.5, і я особисто не сподіваюся, що це зміниться) планувальник за замовчуванням, який націлений на ThreadPool, дозволяє вбудовувати, якщо поточний потік є потоком ThreadPool, і якщо цей потік був той, хто раніше поставив завдання в чергу.
Зверніть увагу, що тут немає довільного повернення, оскільки планувальник за замовчуванням не буде прокачувати довільні потоки під час очікування завдання ... він дозволить лише вбудовувати це завдання, і, звичайно, будь-яке вбудовування цього завдання, у свою чергу, вирішує зробити. Також зверніть увагу, що Wait
навіть не буде запитувати планувальник за певних умов, натомість воліє блокувати. Наприклад, якщо ви передаєте CancellationToken , що скасовується , або якщо ви передаєте нескінченний тайм-аут, він не намагатиметься вбудувати, оскільки це може зайняти довільно тривалий проміжок часу для встановлення виконання завдання, що є все або нічого , і це може призвести до значної затримки запиту на скасування або часу очікування. Загалом, TPL намагається знайти гідний баланс тут між марнотратством потоку, який робитьWait
занадто багато використання та повторне використання цієї теми. Цей тип вкладеності дійсно важливий для рекурсивних проблем "поділи і владай" (наприклад, QuickSort ), коли ти створюєш кілька завдань, а потім чекаєш їх завершення. Якби це робилося без вкладання, ви дуже швидко зайшли б у глухий кут, вичерпуючи всі нитки в басейні та будь-які майбутні, які він вам хотів дати.
Окремо від Wait
того, також можливо (віддалено), що виклик Task.Factory.StartNew може в кінцевому підсумку виконати завдання тоді і там, якщо використовуваний планувальник вирішив виконати завдання синхронно як частину виклику QueueTask. Жоден із планувальників, вбудованих у .NET, ніколи цього не зробить, і я особисто вважаю, що це був би поганий дизайн для планувальника, але теоретично це можливо, наприклад:
protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
return TryExecuteTask(task);
}
Перевантаження Task.Factory.StartNew
цього не приймає a TaskScheduler
використовує планувальник від TaskFactory
, що у випадку Task.Factory
цілей TaskScheduler.Current
. Це означає, що якщо ви телефонуєте Task.Factory.StartNew
зсередини Завдання, яке знаходиться в черзі до цього міфічного RunSynchronouslyTaskScheduler
, воно також потрапляє в чергу RunSynchronouslyTaskScheduler
, в результаті чого StartNew
виклик виконує Завдання синхронно. Якщо вас це взагалі турбує (наприклад, ви впроваджуєте бібліотеку і не знаєте, звідки вам буде викликано), ви можете явно перейти TaskScheduler.Default
до StartNew
виклику, використовуйте Task.Run
(який завжди надходить TaskScheduler.Default
), або використовуйте TaskFactory
створене для націлювання TaskScheduler.Default
.
EDIT: Гаразд, схоже, я був абсолютно помилковим, і нитку, яка зараз чекає на завдання, можна викрасти. Ось простіший приклад цього:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1 {
class Program {
static void Main() {
for (int i = 0; i < 10; i++)
{
Task.Factory.StartNew(Launch).Wait();
}
}
static void Launch()
{
Console.WriteLine("Launch thread: {0}",
Thread.CurrentThread.ManagedThreadId);
Task.Factory.StartNew(Nested).Wait();
}
static void Nested()
{
Console.WriteLine("Nested thread: {0}",
Thread.CurrentThread.ManagedThreadId);
}
}
}
Вихідні дані:
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Як бачите, є багато випадків, коли потік очікування використовується повторно для виконання нового завдання. Це може статися, навіть якщо нитка придбала замок. Неприємне повторне входження. Я в повній мірі вражений і стурбований :(
StartNew
. Завдання визначається як асинхронна операція, яка не обов'язково передбачає новий потік. Може десь також використовувати існуючий потік або інший спосіб зробити асинхронізацію.