Using system threading tasks что это

Using system threading tasks что это

Пространство имен System.Threading.Tasks предоставляет типы, которые упрощают работу по написанию параллельного и асинхронного кода. The System.Threading.Tasks namespace provides types that simplify the work of writing concurrent and asynchronous code. Основные типы: Task, представляющий асинхронную операцию, которую можно ожидать и отменить, и Task , представляющий собой задачу, которая может вернуть значение. The main types are Task which represents an asynchronous operation that can be waited on and cancelled, and Task , which is a task that can return a value. Класс TaskFactory предоставляет статические методы для создания задач, а класс TaskScheduler предоставляет инфраструктуру планирования потоков по умолчанию. The TaskFactory class provides static methods for creating and starting tasks, and the TaskScheduler class provides the default thread scheduling infrastructure.

Классы

Предоставляет планировщики задачи, которые координируются для выполнения задач, обеспечивая то, что параллельные задачи могут выполняться одновременно, а эксклюзивные задачи — нет. Provides task schedulers that coordinate to execute tasks while ensuring that concurrent tasks may run concurrently and exclusive tasks never do.

Предоставляет поддержку параллельных циклов и областей. Provides support for parallel loops and regions.

Позволяет итерациям параллельных циклов взаимодействовать с другими итерациями. Enables iterations of parallel loops to interact with other iterations. Экземпляр этого класса предоставляется каждому циклу классом Parallel; невозможно создавать экземпляры в коде. An instance of this class is provided by the Parallel class to each loop; you can not create instances in your code.

Хранит параметры, настраивающие работу методов класса Parallel. Stores options that configure the operation of methods on the Parallel class.

Представляет асинхронную операцию. Represents an asynchronous operation.

Представляет асинхронную операцию, которая может вернуть значение. Represents an asynchronous operation that can return a value.

Предоставляет набор статических методов для настройки задач, связанных с асинхронными перечислимыми и высвобождаемыми объектами. Provides a set of static methods for configuring task-related behaviors on asynchronous enumerables and disposables.

Представляет исключение, используемое для передачи отмены задачи. Represents an exception used to communicate task cancellation.

Предоставляет набор статических методов (Shared в Visual Basic) для работы с определенными типами экземпляров Task. Provides a set of static (Shared in Visual Basic) methods for working with specific kinds of Task instances.

Предоставляет поддержку создания и планирования объектов Task. Provides support for creating and scheduling Task objects.

Представляет объект, обрабатывающий низкоуровневую постановку задач в очередь на потоки. Represents an object that handles the low-level work of queuing tasks onto threads.

Представляет исключение, используемое для передачи недопустимой операции планировщиком TaskScheduler. Represents an exception used to communicate an invalid operation by a TaskScheduler.

Предоставляет данные для события, создаваемого, если происходит непредвиденное исключение задачи с ошибкой Task. Provides data for the event that is raised when a faulted Task’s exception goes unobserved.

Структуры

Предоставляет состояние выполнения цикла Parallel. Provides completion status on the execution of a Parallel loop.

Предоставляет ожидаемый результат асинхронной операции. Provides an awaitable result of an asynchronous operation.

Перечисления

Задает флаги, которые управляют необязательным поведением создания и выполнения задач. Specifies flags that control optional behavior for the creation and execution of tasks.

Представляет текущий этап жизненного цикла задачи Task. Represents the current stage in the lifecycle of a Task.

Недавно на ресурсе Medium были опубликованы две статьи от одного и того же автора, затрагивающие функциональность C# async/await.

Основными выводами были:

  • рекурсивный вызов асинхронного метода в C# подвержен StackOverflowException
  • goroutine’ы лучше задач (тасков) в .NET в плане производительности

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

Далее в статье я попытаюсь раскрыть суть проблемы более подробно с примерами решения.

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

NB: использоваться будут свежевыпущенный .NET Core 2.0 и Go 1.8.3.

Stack overflow & async

Перейдем сразу к рассмотрению примера #1:

Консоль упадет со StackOverflowException . Печаль!

Вариант реализации tail-call оптимизации здесь не подходит, т.к. мы не собираемся править компилятор, переписывать байт-код и т.п.

Поэтому решение должно подходить для максимально общего случая.

Обычно рекурсивный алгоритм заменяют на итерационный. Но в данном случае нам это не подходит также.

На помощь приходит механизм отложенного выполнения.
Реализуем простой метод Defer :

Для того, чтобы поставить задачу в очередь необходимо указание планировщика.
Методы Task.Run и Task.Factory.StartNew позволяют его использовать (По-умолчанию — TaskScheduler.Default , который для данного примера и так подойдет), а последний позволяет передать объект-состояние в делегат.

На даный момент Task.Factory.StartNew не подерживает обобщенные перегрузки и вряд ли будет. Если необходимо передать состояние, то либо Action , либо Func .

Перепишем пример, используя новый метод Defer :

Читайте также:  Canon color network scangear canon ir2520

Оно не то, чем кажется

Для начала ознакомимся с кодом бенчмарков из этой статьи.

Что брасается сразу в глаза:

  1. Сам пример (что для Go, что для C#) весьма странен. Все сводится к эмуляции цепочки действий и их лавинообразном ‘спуске’. Более того в Go создается chan int на каждую итерацию из 1 млн. Это вообще best-practice??
  2. автор использует Task.Yield() , оправдывая это тем, что иначе пример упадет с StackOverflowException. С таким же успехом мог бы и Task.Delay задействовать. Зачем мелочиться-то?! Но, как увидели ранее, все проистекает из-за ‘неудачного’ опыта с рекурсивными вызовами асинхронных методов.
  3. Изначально в примерах также фигурирует бета-версия System.Threading.Tasks.Channels для сравнения с каналами в Go. Я решил оставить только пример с тасками, т.к. библиотека System.Threading.Tasks.Channels еще не выпущена официально.
  4. Вызов GC.Collect() после прогрева. Боюсь, я откажусь от такого сомнительного преимущества.

Go использует понятие goroutine — легковесных потоков. Соответственно каждая горутина имеет свой стек. На данный момент размер стека равен 2KB. Поэтому при запуске бенчмарков будьте осторожны (более 4GB понадобиться)!

С одной стороны, это может быть полезно CLR JIT’у, а с другой — Go переиспользует уже созданные горутины, что позволяет исключить замеры трат на выделение памяти системой.

Результаты до оптимизации

  • Core i7 6700HQ (3.5 GHz)
  • 8 GB DDR4 (2133 MHz)
  • Win 10 x64 (Creators Update)

Ну что ж, у меня получились следующие результаты:

Warmup (s) Benchmark (s)
Go 9.3531 1.0249
C# 1.3568

NB: Т.к. пример реализует просто цепочку вызовов, то ни GOMAXPROCS, ни размер канала не влияют на результат (уже проверено опытным путем). В расчет берем наилучшее время. Флуктуации не совсем важны, т.к. разница большая.

Да, действительно: Go опережает C# на

Используй TaskScheduler, Luke!

Если не использовать что-то наподобие Task.Yield , то снова будет StackOverflowException.

На этот раз не будем использовать Defer !

Мысль реализации проста: запускаем доп. поток, который слушает/обрабатывает задачи по очереди.

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

Как мы видим, TaskScheduler уж реализует подобие очереди: QueueTask и TryDequeue .

Дабы не изобретать велосипед, воспользуемся уже готовыми планировщиками от команды .NET.

Внимание! Камера! Мотор!

Перепишем это дело на C# 7, делая его максимально приближенным к Go:

Здесь необходимо сделать пару ремарок:

  • GC.Collect() убираем как и говорилось выше
  • Используем StaTaskScheduler с двумя вспомогательными потоками, чтобы избежать блокировки: один ждет результата из главной/последней задачи, а др. обрабатывает саму цепочку задач.

Проблема рекурсивных вызовов исчезает автоматически. Поэтому смело убираем из метода f(input) вызов Task.Yield() . Если этого не сделать, то можно ожидать чуть более лучший результат по сравнению с исходным, т.к. дефолтный планировщик использует ThreadPool.

Теперь публикуем релизную сборку:
dotnet publish -c release -r win10-x64

Внезапно получаем около 600 ms вместо прежних 1300 ms. Not bad!
Go, напомню, отрабатывал на уровне 1000 ms. Но меня не покидает чувство неуместности использования каналов как средство кооперативной многозадачности в исходных примерах.

p.s.
Я не делал огромного количества прогонов тестов с полноценной статистикой распределения значений замеров специально.
Цель статьи заключалась в освещении определенного use-case’a async/await и попутного развенчания мифа о невозможности рекурсивного вызова асинхронных методов в C#.

p.p.s.
Причиной изначального отставания C# было использование Task.Yield() . Постоянное переключение контекста — не есть гуд!

Ой, у вас баннер убежал!

Читают сейчас

Похожие публикации

  • 15 ноября 2019 в 10:00

.NET Core с блокнотами Jupyter — Preview 1

Представляем .NET Core 3.1 Preview 2

.NET Core 3 для Windows Desktop

Заказы

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Комментарии 34

// Оффтоп
Хватит, горшочек, не вари! Шарп, родненкий, ну хорошо же жили вместе уже почти два десятка лет, всё у нас получалось, и космические корабли бороздили просторы вселенной, и сервера с клиентами любили друг друга не смотря на разделяющие их моря и океаны. Но то, что щас происходит — за гранью моего восприятия. Я не знаю, сколько часов должно быть в сутках и в рабочей неделе, чтобы успевать вникать/изучать в новые конструкции и приёмы. Особенно учитывая, что среднестатистический программист разве что курьером не дорабатывает в фирме дабы экономить бюджет и всё совсем не загнулось. Или на западе реально в каждом стартапе/фирме получается выбить деньги на гору программистов, которые могут себе позволить быть настолько специализированными, что таки способны изучать и использовать все эти бесконечные навороты?

Если хочется продвигаться в карьере и расти в з/п — надо постоянно учиться.

Читайте также:  Deepcool smarter led black

"Пока ты спишь, кто-то качается".

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

StackOverflow можно достичь и вполне естественным способом, например, при написании сетевого пинг-понг приложения. C# по умолчанию предлагает вызвать продолжение метода немедленно, если задача уже выполнена. Обычно это полезно, т.к. сокращает накладные расходы, но может привести и к казусам.

Да, эту проблему можно решить «костылём» в виде `Task.Yield`, а проблему с накладными расходами при переключении контекста — собственным планировщиком. Но просто обойтись без `Task.Yield`, к сожалению, не получится.

Боюсь, ValueTask здесь не поможет: мы создаем цепочку вызовов, а не просто возвращаем скалярное либо др. примитивное значение.

.NET потребляет примерно

Я запустил Ваш оптимизированный через StaTaskScheduler код и он выдал

1300мс.
Я набросал то же самое на F#

Код
Картинка из консоли.
Первый запуск — релизная сборка Вашей версии, второй запуск — моей.
Ссылка на .Net dll + pdb

Возможно я что-то сделал не так если разница аж в два раза между языками одной и той же платформы.

P.S. я там в for ошибся, мой код на 1 итерацию больше делает 🙂

Ссылка на аплоад dll битая. Вот верная

Может быть, изначально использовался неоптимизированный C#-пример?

Подозреваю что есть ещё что-то, т.к. я точно использовал оптимизированный пример.
Учитывая такую непостоянность результатов, нельзя сказать что они у кого-то из нас достоверные.

можно поинтересоваться характеристиками Вашей тестовой среды?
хотелось бы увидеть в таком случае результаты Go-бенчмарка как базиса тоже.

Processor=Intel Core i5-3450 CPU 3.10GHz (Ivy Br > Есть предложение лучше — прогнать с BenchmarkDotNet:
F# — у меня выдаёт 56.29 ms (я снизил кол-во итераций в коде до 100к)
C# — у меня выдаёт 355.1 ms

Попробуйте запустить тот же код в релизе (я собирал всё под 4.6.1, т.к. ParallelExtensionsExtra под netcore нет)

занятно! я попробую это дело прогнать у себя тоже.

p.s.
очевидно, что можно просто скопировать исходник StataskScheduler из ParallelExtensionsExtra прямо в проект 😀

изначально я запускал пример на F# без Server GC ((
вот что у меня получилось после правки:

чуть добавил конфигурации для

Спасибо за результаты. Я правда не знаю о чём это говорит.
Могу только предположить что asyncWorkflow в F# менее оверхедный чем Task в C#.

мысль здравая. я и сам по возможности рекомендую библиотечку для бенчмарков.
При этом у меня не возникает чувство стыда из-за её неиспользования в данном случае.

вычислять персентили и т.п. по отношению к примеру на Go (выступающим базисом), где всего-то используется пакет time , выглядит, по-моему, неспортивно.
Особенно при выдимой невооруженным глазом разнице примерно в 2 раза в плане производительности.

Думаю, рекомендовать книги Рихтера — вещь очевидная 🙂
Из блогов/публикаций настоятельно рекомендую PFX Team. Stephen Toub с командой весьма подробно разбирают подходы и принципы использования TPL и все, что с ним связано.
Серия статей по C# async от Stephen Cleary (автора библиотеки AsyncEx) также интересна.

Не знаю, Ритхера все нахваливают, но по сути он очень странно и непонятно объсняет. Тот же Гольдштейн по кишкам мне зашел в разы лучше.

В первом случае мне кажется что это переписывание в стиле "Сделайте фибоначчи не рекурсивным, используя 4 аргумента". На прологе баловались, знаем, плавали 🙂

А вот второй вот не совсем понятен, зачем нам left и right если они на одно и то же всегда указывают. В чем смысл этой чехарды?

ну это можно переписать как next = f(next) . по сути, я оставил этот странный кусок, чтобы максимально синтаксически приблизить к примеру на Go.

Hi, the author of two original posts is here. I speak Russian, but unfortunately, I don’t type well in Russian, so:

Hi, thanks for looking into this! First, let me comment on a few points you’ve mentioned:
— “The example uses a kind of anti-pattern for async usage in C#” — that’s true, and I’ve mentioned it’s an unfair benchmark for C#. I intentionally took a well-known benchmark for goroutines knowing it’s highly disadvantageous for C# to demonstrate that async/await should be comparable even in this case in terms of performance.
— This explains why I didn’t try to use other schedulers — frankly, I should, but that’s not what you normally do by default.
— This also explains why I had to use Task.Yield(). It was clear to me that you can get rid of it w/ your own scheduler, and moreover, it will also improve the speed a lot.
— I am also 90% sure that getting rid of “await Task.Yield()” is the main driver of performance improvement in your version.
— As for GC.Collect() after warmup, true, it’s really not that important, all you might expect is a bit better + more consistent timings if your working set is fully fitting into CPU caches. Unfortunately, I’ve forgot to do the same in Go test — clearly, it has to be done the same way at least.

Читайте также:  Indesit idl 40 коды ошибок

As a side note, I never used StaTaskScheduler before — this is also why I was hesitant to invest into researching on what can be achieved with other schedulers. But thanks to you, I’ll take this into account next time, though I still lean to implementing a “perfectly slim” scheduler + probably, tasks as well. Based on your research, this should demonstrate that in this case C# should be light-years ahead, which is actually what I’d love to show: the asynchronous computation model there is way more extendable than in Go, so if it’s also better in terms of performance — even though you need some special tuning — this basically undermines the core promise Go makes.

Finally, performance isn’t the only focus point in this article. I mostly wanted to show how these languages differ in terms of their approach to asynchronous computation, and obviously, you can’t do this w/o such benchmarks.

No matter what, thanks a lot for quite useful feedback!

В эпоху многоядерных машин, которые позволяют параллельно выполнять сразу несколько процессов, стандартных средств работы с потоками в .NET уже оказалось недостаточно. Поэтому во фреймворк .NET была добавлена библиотека параллельных задач TPL (Task Parallel Library), основной функционал которой располагается в пространстве имен System.Threading.Tasks . Данная библиотека позволяет распараллелить задачи и выполнять их сразу на нескольких процессорах, если на целевом компьютере имеется несколько ядер. Кроме того, упрощается сама работа по созданию новых потоков. Поэтому начиная с .NET 4.0. рекомендуется использовать именно TPL и ее классы для создания многопоточных приложений, хотя стандартные средства и класс Thread по-прежнему находят широкое применение.

Задачи и класс Task

В основе библиотеки TPL лежит концепция задач, каждая из которых описывает отдельную продолжительную операцию. В библиотеке классов .NET задача представлена специальным классом — классом Task , который находится в пространстве имен System.Threading.Tasks . Данный класс описывает отдельную задачу, которая запускается асинхронно в одном из потоков из пула потоков. Хотя ее также можно запускать синхронно в текущем потоке.

Для определения и запуска задачи можно использовать различные способы. Первый способ создание объекта Task и вызов у него метода Start:

В качестве параметра объект Task принимает делегат Action, то есть мы можем передать любое действие, которое соответствует данному делегату, например, лямбда-выражение, как в данном случае, или ссылку на какой-либо метод. То есть в данном случае при выполнении задачи на консоль будет выводиться строка "Hello Task!".

А метод Start() собственно запускает задачу.

Второй способ заключается в использовании статического метода Task.Factory.StartNew() . Этот метод также в качестве параметра принимает делегат Action, который указывает, какое действие будет выполняться. При этом этот метод сразу же запускает задачу:

В качестве результата метод возвращает запущенную задачу.

Третий способ определения и запуска задач представляет использование статического метода Task.Run() :

Метод Task.Run() также в качестве параметра может принимать делегат Action — выполняемое действие и возвращает объект Task.

Определим небольшую программу, где используем все эти способы:

Ожидание задачи

Важно понимать, что задачи не выполняются последовательно. Первая запущенная задача может завершить свое выполнение после последней задачи.

Или рассмотрим еще один пример:

Класс Task в качестве параметра принимает метод Display, который соответствует делегату Action. Далее чтобы запустить задачу, вызываем метод Start: task.Start() , и после этого метод Display начнет выполняться во вторичном потоке. В конце метода Main выводит некоторый маркер-строку, что метод Main завершился.

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

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

Чтобы указать, что метод Main должен подождать до конца выполнения задачи, нам надо использовать метод Wait :

Свойства класса Task

Класс Task имеет ряд свойств, с помощью которых мы можем получить информацию об объекте. Некоторые из них:

AsyncState : возвращает объект состояния задачи

CurrentId : возвращает идентификатор текущей задачи

Exception : возвращает объект исключения, возникшего при выполнении задачи

Ссылка на основную публикацию
Adblock detector