Я новичок в параллельном программировании. В .NET доступны два класса: Task
и Thread
.
Итак, мои вопросы:
- В чем разница между этими классами?
- Когда лучше использовать
Thread
а когдаTask
?
Я новичок в параллельном программировании. В .NET доступны два класса: Task
и Thread
.
Итак, мои вопросы:
Thread
а когда Task
? Thread
представляет собой концепцию более низкого уровня: если вы непосредственно начинаете поток, вы знаете, что это будет отдельный поток, а не выполнение в пуле потоков и т.д.
Task
- это больше, чем просто абстракция "где запускать какой-то код", хотя это действительно просто "обещание результата в будущем". Итак, как несколько разных примеров:
Task.Delay
не требует реального времени процессора; это точно так же, как установка таймера в будущем.WebClient.DownloadStringTaskAsync
, не займет много времени процессора локально; он представляет собой результат, который, скорее всего, потратит большую часть времени на сетевую задержку или удаленную работу (на веб-сервере).Task.Run()
, действительно говорит: "Я хочу, чтобы вы выполняли этот код отдельно"; точный поток, на котором выполняется этот код, зависит от ряда факторов.Обратите внимание, что абстракция Task<T>
является ключевой для поддержки async в С# 5.
В общем, я бы рекомендовал вам использовать абстракцию более высокого уровня, где бы вы ни находились: в современном коде С# вам редко нужно явно запускать собственный поток.
Тема
Thread представляет собой фактический поток уровня ОС, имеющий собственный стек и ресурсы ядра. (технически, реализация CLR могла бы использовать волокна вместо этого, но никакая существующая CLR не делает этого) Thread обеспечивает наивысшую степень контроля; вы можете отключить() или Приостановить() или Возобновить() поток (хотя это очень плохая идея), вы можете наблюдать его состояние, и вы можете установить свойства уровня потока, такие как размер стека, состояние квартиры или культура.
Проблема с Thread заключается в том, что потоки ОС являются дорогостоящими. Каждый поток, который вы потребляете нетривиальным объемом памяти для своего стека, и добавляет дополнительные накладные расходы процессора в качестве перекрестного контекста процессора между потоками. Вместо этого лучше иметь небольшой пул потоков, выполняющих ваш код, когда работа станет доступной.
Бывают случаи, когда нет альтернативы Thread. Если вам нужно указать имя (для целей отладки) или состояние квартиры (для отображения пользовательского интерфейса), вы должны создать свой собственный поток (обратите внимание, что наличие нескольких потоков пользовательского интерфейса, как правило, является плохой идеей). Кроме того, если вы хотите поддерживать объект, который принадлежит одному потоку и может использоваться только этим потоком, гораздо проще явно создать экземпляр Thread для него, чтобы вы могли легко проверить, работает ли код, пытающийся его использовать. на правильной нити.
ThreadPool
ThreadPool - это оболочка вокруг пула потоков, поддерживаемых CLR. ThreadPool не дает вам никакого контроля; вы можете отправить работу на выполнение в какой-то момент, и вы можете контролировать размер пула, но вы ничего не можете установить. Вы даже не можете сказать, когда пул начнет работу, которую вы ему подадите.
Использование ThreadPool позволяет избежать накладных расходов при создании слишком большого количества потоков. Тем не менее, если вы отправляете слишком много длительных задач в threadpool, они могут быть заполнены, а позже работа, которую вы отправляете, может оказаться в ожидании завершения более ранних длинных элементов. Кроме того, ThreadPool не дает возможности узнать, когда рабочий элемент завершен (в отличие от Thread.Join()), и способ получить результат. Поэтому ThreadPool лучше всего подходит для коротких операций, когда вызывающему абоненту не нужен результат.
Task
Наконец, класс Task из параллельной библиотеки задач предлагает лучшее из обоих миров. Как и ThreadPool, задача не создает собственный поток ОС. Вместо этого задачи выполняются TaskScheduler; планировщик по умолчанию просто запускается на ThreadPool.
В отличие от ThreadPool, Task также позволяет вам узнать, когда он заканчивается, и (через общую задачу) вернуть результат. Вы можете вызвать ContinueWith() в существующей Задаче, чтобы запустить больше кода после завершения задачи (если она уже завершена, она немедленно вызовет обратный вызов). Если задача является общей, ContinueWith() передаст вам результат задачи, позволяя вам запускать больше кода, который ее использует.
Вы также можете синхронно дождаться завершения задачи, вызвав Wait() (или для общей задачи, получив свойство Result). Подобно Thread.Join(), это блокирует вызывающий поток, пока задача не завершится. Синхронное ожидание задачи - это, как правило, плохая идея; он запрещает вызывающему потоку выполнять какую-либо другую работу и может также приводить к взаимоблокировкам, если задача заканчивается ожиданием (даже асинхронно) для текущего потока.
Так как задачи все еще выполняются на ThreadPool, их нельзя использовать для длительных операций, поскольку они все равно могут заполнить пул потоков и заблокировать новую работу. Вместо этого Task предоставляет параметр LongRunning, который подскажет TaskScheduler развернуть новый поток, а не работать в ThreadPool.
Все новые высокоуровневые API concurrency, включая методы Parallel.For *(), ожидают PLINQ, С# 5 и современные методы async в BCL, построены на Task.
Заключение
Суть в том, что задача почти всегда является лучшим вариантом; он обеспечивает гораздо более мощный API и позволяет избежать потоков ОС.
Единственными причинами явного создания ваших собственных потоков в современном коде являются установка параметров для потока или сохранение постоянного потока, который должен поддерживать свою собственную идентификацию.
Обычно вы слышите, что Task является понятием более высокого уровня, чем thread... и что означает эта фраза:
Вы не можете использовать Abort/ThreadAbortedException, вы должны поддерживать событие отмены в вашем "бизнес-коде", периодически проверяя флаг token.IsCancellationRequested
(также избегайте длинных или безвременных соединений, например, с db, иначе у вас никогда не будет возможности проверить этот флаг). По той же причине Thread.Sleep(delay)
следует заменить Task.Delay(delay, token)
.
Нет потоковых функций Suspend
и Resume
с задачами. Экземпляр задачи также нельзя использовать повторно.
Но вы получаете два новых инструмента:
а) продолжения
// continuation with ContinueWhenAll - execute the delegate, when ALL
// tasks[] had been finished; other option is ContinueWhenAny
Task.Factory.ContinueWhenAll(
tasks,
() => {
int answer = tasks[0].Result + tasks[1].Result;
Console.WriteLine("The answer is {0}", answer);
}
);
б) вложенные/дочерние задачи
//StartNew - starts task immediately, parent ends whith child
var parent = Task.Factory.StartNew
(() => {
var child = Task.Factory.StartNew(() =>
{
//...
});
},
TaskCreationOptions.AttachedToParent
);
Таким образом, системный поток полностью скрыт от задачи, но все же код задачи выполняется в конкретном системном потоке. Системные потоки являются ресурсами для задач, и, конечно же, под капотом параллельного выполнения задач все еще есть пул потоков. Там могут быть разные стратегии, как поток получить новые задачи для выполнения. Об этом заботится еще один общий ресурс TaskScheduler. Некоторые проблемы, которые решает TaskScheduler: 1) предпочитают выполнять задачу и ее объединение в одном потоке, сводя к минимуму затраты на переключение - также как встроенное выполнение) 2) предпочитают выполнять задачи в порядке их запуска - также как PreferFairness 3) более эффективное распределение задач между неактивными потоками в зависимости от "предшествующего знания задач деятельности" - ака кража работы. Важно: в целом "асинхронный" не то же самое, что "параллельный". Играя с опциями TaskScheduler, вы можете настроить синхронное выполнение асинхронных задач в одном потоке. Для выражения параллельного выполнения кода можно использовать более высокие абстракции (чем Задачи): Parallel.ForEach
, PLINQ
, Dataflow
.
Задачи интегрированы с С# async/await функциями, также известными как Promise Model, например, requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName));
выполнение client.RequestAsync
не будет блокировать поток пользовательского интерфейса. Важно: Clicked
вызов делегата Clicked
является абсолютно регулярным (все потоки выполняются компилятором).
Этого достаточно, чтобы сделать выбор. Если вам требуется поддержка функции отмены вызова устаревшего API, который имеет тенденцию зависать (например, безвременное соединение), и в этом случае поддерживает Thread.Abort(), или если вы создаете многопоточные фоновые вычисления и хотите оптимизировать переключение между потоками с помощью Suspend/Resume, значит, управлять параллельным выполнением вручную - оставайтесь с Thread. В противном случае перейдите к Задачам, потому что они позволят вам легко манипулировать их группами, интегрировать в язык и сделать разработчиков более продуктивными - Task Parallel Library (TPL).
Класс Thread
используется для создания и управления потоком в Windows.
A Task
представляет собой асинхронную операцию и является частью параллельной библиотеки задач, набор API для выполнения задач асинхронно и параллельно.
В дни старого (т.е. до TPL) раньше было то, что использование класса Thread
было одним из стандартных способов запуска кода в фоновом режиме или параллельно (лучшей альтернативой часто было использование ThreadPool
), однако это было громоздким и имело несколько недостатков, не в последнюю очередь из которых были накладные расходы на производительность создания всего нового потока для выполнения задачи в фоновом режиме.
В настоящее время использование задач и TPL - это гораздо лучшее решение в 90% случаев, поскольку оно обеспечивает абстракции, которые позволяют гораздо более эффективно использовать системные ресурсы. Я предполагаю, что существует несколько сценариев, в которых вам нужен явный контроль над потоком, на котором вы запускаете свой код, однако, вообще говоря, если вы хотите запустить что-то асинхронно, ваш первый порт захода должен быть TPL.