Таск что это в программировании

Содержание

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

В основу TPL положен класс Task. Элементарная единица исполнения инкапсулируется в TPL средствами класса Task, а не Thread. Класс Task отличается от класса Thread тем, что он является абстракцией, представляющей асинхронную операцию. А в классе Thread инкапсулируется поток исполнения. Разумеется, на системном уровне поток по-прежнему остается элементарной единицей исполнения, которую можно планировать средствами операционной системы. Но соответствие экземпляра объекта класса Task и потока исполнения не обязательно оказывается взаимно-однозначным.

Кроме того, исполнением задач управляет планировщик задач, который работает с пулом потоков. Это, например, означает, что несколько задач могут разделять один и тот же поток. Класс Task (и вся остальная библиотека TPL) определены в пространстве имен System.Threading.Tasks.

Создание задачи

Создать новую задачу в виде объекта класса Task и начать ее исполнение можно самыми разными способами. Для начала создадим объект типа Task с помощью конструктора и запустим его, вызвав метод Start(). Для этой цели в классе Task определено несколько конструкторов. Ниже приведен тот конструктор, которым мы собираемся воспользоваться:

где действие обозначает точку входа в код, представляющий задачу, тогда как Action — делегат, определенный в пространстве имен System. Форма делегата Action, которой мы собираемся воспользоваться, выглядит следующим образом:

Таким образом, точкой входа должен служить метод, не принимающий никаких параметров и не возвращающий никаких значений. (Как будет показано далее, делегату Action можно также передать аргумент.)

Как только задача будет создана, ее можно запустить на исполнение, вызвав метод Start(). После вызова метода Start() планировщик задач запланирует исполнение задачи.

В приведенной ниже программе все изложенное выше демонстрируется на практике. В этой программе отдельная задача создается на основе метода MyTask(). После того как начнет выполняться метод Main(), задача фактически создается и запускается на исполнение. Оба метода MyTask() и Main() выполняются параллельно:

Следует иметь в виду, что по умолчанию задача исполняется в фоновом потоке. Следовательно, при завершении создающего потока завершается и сама задача. Именно поэтому в рассматриваемой здесь программе метод Thread.Sleep() использован для сохранения активным основного потока до тех пор, пока не завершится выполнение метода MyTask(). Как и следовало ожидать, организовать ожидание завершения задачи можно и более совершенными способами.

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

Применение идентификатора задачи

В отличие от класса Thread, в классе Task отсутствует свойство Name для хранения имени задачи. Но вместо этого в нем имеется свойство Id для хранения идентификатора задачи, по которому можно распознавать задачи. Свойство Id доступно только для чтения и относится к типу int.

Каждая задача получает идентификатор, когда она создается. Значения идентификаторов уникальны, но не упорядочены. Поэтому один идентификатор задачи может появиться перед другим, хотя он может и не иметь меньшее значение.

Идентификатор исполняемой в настоящий момент задачи можно выявить с помощью свойства CurrentId. Это свойство доступно только для чтения, относится к типу static и объявляется следующим образом:

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

В приведенном ниже примере программы создаются две задачи и показывается, какая из них исполняется:

Источник

Параллельное программирование в C#: создание и выполнение задач (Task)

Класс Task

Варианты определение и запуска задач Task

1. Вызова конструктора задачи и запуск путем вызова метода Start()

В качестве параметра в конструкторе Task используется делегат Action :

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

Таким образом, в примере выше, в консоль будет выведена строка «Action», хотя, никто не запрещает вам наполнить метод MyAction какими-либо более сложными действиями.

2. Создание задачи и её запуск с использованием метода StartNew()

3. Создание задачи и её запуск с использованием метода Run()

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

4. Создание задачи и её синхронный запуск с использованием метода RunSynchronously()

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

Выполнение и ожидание выполнения задач

Когда вы используете в своей работе приемы параллельного программирования, то без вашего прямого указания последовательности выполнения задач, никто не гарантирует, что задачи будут выполняться в той же последовательности, в которой они создавались. Чтобы продемонстрировать это напишем вот такой небольшой пример:

В итоге, в консоли можно увидеть следующие строки:

task2 Task=2, Thread=6

task Task=1, Thread=5

task4 Task=4, Thread=4

task3 Task=3, Thread=7

Обратите внимание на три момента:

Вывод в консоли в итоге будет следующий:

LongAction ещё выполняется

LongAction выполнена, запускаем остальные задачи

task2 Task=2, Thread=5

task3 Task=3, Thread=6

task4 Task=4, Thread=8

Итого

Сегодня мы, в общих чертах, познакомились с тем как работать с задачами при параллельном программировании в C# с использованием библиотеки TPL. Механизм использования задач (Task) позволяет достаточно просто и интуитивно понятно организовать параллельное выполнение нескольких задач в вашей программе, не прибегая к использованию потоков (Thread).

Источник

Tasks и Back Stack

Может возникнуть вопрос: а как же фрагменты? Как они сохраняются в стеке? У них всё устроено несколько иначе, чем у активити: фрагмент помещается в back stack, управляемый активити и то, только если был вызван соответствующий метод ( addToBackStack() ) во время транзакции.

В версии Android 7.0 была добавлена поддержка многооконного режима: пользователь может разделить экран и таким образом работать с несколькими приложениями. В таком режиме система управляет task’ами отдельно для каждого окна, т.е. у каждого окна может быть несколько task’ов.

Визуально task’и можно увидеть на экране последних запущенных задач:

Управление task’ами

Некоторые приложения спроектированы таким образом, что есть несколько точек перемещения к одной и той же активити. Несмотря на то, что такая активити уже может находится в стеке, каждый раз будет создаваться её новый экземпляр и также сохраняться в стек. Таким образом, когда пользователь решит переместиться к самой первой активити, он увидит все открытые им, казалось бы одинаковые активити, но в разном состоянии. Подобного эффекта можно избежать при помощи специальных атрибутов манифеста и флагов для Intent.

Обратите внимание, что иногда атрибуты в манифесте и флаги в Intent могут противоречить друг другу. В этом случаи флаги Intent будут более приоритетны.

Атрибуты

launchMode
Данный атрибут можно указать для каждой активити в манифесте. Имеет несколько значений:

Флаги

Виды флагов:

Очистка стека

Если task долгое время находится в фоне, то система сама чистит его стек, оставляя только корневую активити. Подобное поведение объясняется тем, что по прошествии длительного времени пользователь, вероятно, забыл, что он делал в приложении и открыл его повторно уже с иной целью.

У активити существует три атрибута для изменения такого поведения:

Источник

Как работать с классом Task в C#: разбираем на примерах

«Чего так синхронно?»

Содержание статьи:

Задачи класса Task в С#

Давайте рассмотрим каждый случай подробнее:

Run(Action)

Например, можно использовать Run(Action) для программы, которая реализует цикл в цикле, а потом выводит на экран количество произведенных итераций. Код при этом будет выглядеть вот так:

TaskFactory.StartNew

Также в классе Task предусмотрены конструкторы, инициализирующие задачу, которые при этом не планируют ее выполнение.

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

Реализация задач в потоке ведется асинхронно. Поток продолжает свою работу сразу же после формирования экземпляра. В определенных ситуациях вызывающий поток — главный в программе, поэтому он может завершиться еще до того, как задача начнет выполняться.

Task.Wait

Метод Task.Wait ставит блок на вызывающий поток до окончания действия экземпляра класса. Если метод активировать без дополнительных параметров, произойдет ожидание окончания задачи без условий. Рассмотрим пример, в котором вызов метода Thread.Sleep.Task переключит режим сна на несколько секунд:

Ожидать окончание выполнения задачи можно и другим образом. Например, методы Wait(Int32) и Wait(TimeSpan) устанавливают блокировку на вызывающий поток до момента окончания задачи или периода ожидания:

Давайте рассмотрим пример с временем ожидания запуска в 1 секунду. Это значит, что код блокирует поток до истечения этого времени:

Task.lock

Тask lock в C# — это блокировка задач, которая позволяет им оставаться синхронизированными с другой или с общей задачей даже при запуске других задач. Например:

Токен отмены

Task.WaitAny(tasks)

Task.WaitAll(tasks)

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

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

Класс Task обладает рядом свойств. Давайте познакомимся с ними.

Task vs Thread в С#

Давайте посмотрим на фрагмент кода, в котором генерируются две задачи:

Task parallel library в С# (example)

Также Task используется в библиотеке параллельных задач TPL, которая представляет собой набор открытых типов и API-интерфейсов. При его использовании необходимо поверх вашего класса подключать пространство имен, для чего используется:

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

Вложенные задачи

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

Это означает, что внутренняя задача присоединена к наружной. Она будет выполняться как ее часть, а значит внешняя может завершить свою работу только после вложенной.

Массив задач

Напомним, что если требуется, чтобы определенный код работал только после завершения всех задач, то используется метод Task.WaitAll(tasks) :

Task result в С#: результаты работы

Задачи могут выполняться не только как процедуры. Они также могут возвращать результаты.

Давайте разберем подробнее:

Заключение

В статье мы рассмотрели класс Task в языке C#. Подробно изучили задачи и их выполнение, а также принципы работы с классом. Он является основой для асинхронного программирования. Такой подход позволяет полностью использовать мощность многоядерных процессоров.

Видео: вводный рассказ про класс Task и его использование для параллелизации выполнения задач

Источник

.NET: Инструменты для работы с многопоточностью и асинхронностью. Часть 1

Публикую на Хабр оригинал статьи, перевод которой размещен в блоге Codingsight.
Вторая часть доступна здесь

Терминология

Процесс (Process) — объект ОС, изолированное адресное пространство, содержит потоки.
Поток (Thread) — объект ОС, наименьшая единица выполнения, часть процесса, потоки делят память и другие ресурсы между собой в рамках процесса.
Многозадачность — свойство ОС, возможность выполнять несколько процессов одновременно
Многоядерность — свойство процессора, возможность использовать несколько ядер для обработки данных
Многопроцессорность — свойство компьютера, возможность одновременно работать с несколькими процессорами физически
Многопоточность — свойство процесса, возможность распределять обработку данных между несколькими потоками.
Параллельность — выполнение нескольких действий физически одновременно в единицу времени
Асинхронность — выполнение операции без ожидания окончания завершения этой обработки, результат же выполнения может быть обработан позднее.

Метафора

Не все определения хороши и некоторые нуждаются в дополнительном объяснении, потому к формально введенной терминологии добавлю метафору о приготовлении завтрака. Приготовление завтрака в этой метафоре — process.

Готовя завтрак с утра я (CPU) прихожу на кухню (Компьютер). У меня 2 руки (Cores). На кухне есть ряд устройств (IO): печь, чайник, тостер, холодильник. Я включаю газ, ставлю на него сковородку и наливаю туда масло, не дожидаясь пока она разогреется (асинхронно, Non-Blocking-IO-Wait), я достаю из холодильника яйца и разбиваю их в тарелку, после чего взбиваю одной рукой (Thread#1), а второй (Thread#2) придерживаю тарелку (Shared Resource). Сейчас бы еще включить чайник, но рук не хватает (Thread Starvation) За это время разогревается сковородка (Обработка результата) куда я выливаю то что взбил. Я дотягиваюсь до чайника и включаю его и тупо смотрю как вода в нем закипает (Blocking-IO-Wait), хотя мог бы за это время вымыть тарелку, где взбивал омлет.

Я готовил омлет используя всего 2 руки, да больше у меня и нет, но при этом в момент взбивания омлета происходило сразу 3 операции: взбивание омлета, придерживание тарелки, разогревание сковородки.CPU — является самой быстрой частью компьютера, IO это то, что чаще всего тормозит, потому часто эффективным решением является занять чем-то CPU пока идет получение данных от IO.

Под инструментами я имею ввиду как программные интерфейсы (API) предоставляемые фреймворком и сторонними пакетами, так и целый программные решения упрощающий поиск каких-либо проблем связанных с многопоточным кодом.

Запуск потока

С момента старта приложение в фоне создает несколько потоков про запас и предоставляет возможность брать их в пользование. Если потоки используются часто и в большом количестве, то пул расширяется, чтобы удовлетворить потребность вызывающего кода. Когда в пуле в нужный момент времени не оказывается свободных потоков он или дождется возврата одного из потоков, либо создаст новый. Из этого следует, что пул потоков отлично подходит для неких коротких действий и плохо подходит, для операций работающих как службы на протяжении всей работы приложений.

Для использования потока из пула, есть метод QueueUserWorkItem, который принимает делегат типа WaitCallback, что по сигнатуре совпадает с ParametrizedThreadStart, а передаваемый в него параметр выполняет туже функцию.

Менее известный метод пула потоков RegisterWaitForSingleObject служит для организации неблокирующих IO операций. Делегат переданный в этот метод будет вызван тогда, когда WaitHandle переданный в метод будет “отпущен”(Released).

Так же есть довольно экзотический способ отправить делегат на выполнение в поток из пула — метод BeginInvoke.

Хочу еще вскользь остановится на функции к вызову которой сводится многие из вышеуказанных методов — CreateThread из Kernel32.dll Win32 API. Существует способ, благодаря механизму extern методов вызвать эту функцию. Я видел такой вызов лишь однажды в жутчайшем примере legacy кода, а мотивация автора сделавшего именно так все еще остается для меня загадкой.

Просмотр и отладка потоков

Task Parallel Library

Рассмотрим варианты запуска и использования Task’а. На примере кода ниже, мы создаем новый таск, который не делает ничего полезного (Thread.Sleep(10000)), но в реальной жизни это должна быть некая сложная задействующая CPU работа.

Task создается с рядом опций:

Последним параметром передан объект scheduler типа TaskScheduler. Этот класс и его наследники предназначены для управления стратегиями распределения Task’ов по потокам, по умолчанию Task будет выполнен на случайном потоке из пула.

К созданному Task’у применен оператор await, а значит код написанный после него, если такой есть будет выполнен в том же контексте (часто это означает что на том же потоке), что и код до await.

Метод помечен как async void, это значит, что в нем допустимо использование оператора await, но вызывающий код не сможет дождаться выполнения. Если такая возможность необходима, то метод должен возвращать Task. Методы помеченные async void встречаются довольно часто: как правило это обработчики событий или другие методы, работающие по принципу выполнить и забыть (fire and forget). Если необходимо не только дать возможность дождаться окончания выполнения, но и вернуть результат, то необходимо использовать Task.

На Task’е что вернул метод StartNew, впрочем как и на любом другом, можно вызвать метод ConfigureAwait с параметром false, тогда выполнение после await продолжится не на захваченном контексте, а на произвольном. Это нужно делать всегда, когда для кода после await не принципиален контекст выполнения. Также это является рекомендацией от MS при написании кода, что будет поставляться упакованном в библиотеку виде.

Давайте еще немного остановимся на том, как можно дождаться окончания выполнения Task’и. Ниже пример кода, с комментариями, когда ожидание сделано условно хорошо и когда условно плохо.

В первом примере мы дожидаемся выполнения Task’и не блокируя вызывающий поток, к обработке результата вернемся лишь когда он уже будет, до тех пор вызывающий поток предоставлен себе.

Во втором варианте мы блокируем вызывающий поток до тех пор пока не будет подсчитан результат метода. Это плохо не только потому, что мы заняли поток, столь ценный ресурс программы, простым безделием, но еще и потому, что если в коде метода что мы вызываем есть await, а контекст синхронизации предполагает возвращение в вызывающий поток после await, то мы получим deadlock: вызывающий поток ждет пока будет вычислен результат асинхронного метода, асинхронный метод тщетно пытается продолжить свое выполнение в вызывающем потоке.

Еще одним недостатком такого подхода является усложненная обработка ошибок. Дело в том, что ошибки в асинхронном коде при использовании async/await обрабатывать очень легко — они ведут себя так же как если бы код был синхронным. В то время, как если мы применяем экзорцизм синхронное ожидание к Task’e оригинальное исключение оборачивается в AggregateException, т.о. Для обработки исключения придется исследовать тип InnerException и самому писать цепочку if внутри одного catch блока или использовать конструкцию catch when, вместо более привычной в C# мире цепочки catch блоков.

Третий и последний примеры так же отмечены плохими по той же причине и содержат все те же проблемы.

Методы WhenAny и WhenAll крайне удобны в ожидании группы Task’ов, они оборачивают группу Task’ов в один, который сработает либо по первому срабатыванию Task’а из группы, либо когда свое выполнение закончат все.

Остановка потоков

Метод Interrupt работает более предсказуемо. Он может прервать поток исключением ThreadInterruptedException только в те моменты, когда поток находится в состоянии ожидания. В такое состояние он переходит подвисая в ожидании WaitHandle, lock или после вызова Thread.Sleep.

Оба описанных выше варианта, плохи своей непредсказуемостью. Выходом является использование структуры CancellationToken и класса CancellationTokenSource. Суть в следующем: создается экземпляр класса CancellationTokenSource и только тот кто им владеет, может остановить операцию вызвав метод Cancel. В саму же операцию передается только лишь CancellationToken. Владельцы CancellationToken не могут сами отменить операцию, а могут лишь проверить не была ли операция отменена. Для этого есть булево свойство IsCancellationRequested и метод ThrowIfCancelRequested. Последний сгенерирует исключение TaskCancelledException если на пародившем CancellationToken экземпляре CancellationTokenSource был вызван метод Cancel. И именно этот метод я рекомендую использовать. Это лучше предыдущих вариантов получением полного контроля над тем в какие моменты исключение операция может быть прервана.

Самым жестоким вариантом остановки потока, является вызов функции Win32 API TerminateThread. Поведение CLR после вызова этой функции может быть непредсказуемым. На MSDN же про эту функцию написано следующее: “TerminateThread is a dangerous function that should only be used in the most extreme cases. “

Преобразование legacy-API в Task Based с помощью метода FromAsync

Это лишь пример и делать такое со встроенными типами вам вряд ли придется, но любой старый проект просто кишит методами BeginDoSomething возвращающими IAsyncResult и методами EndDoSomething его принимающими.

Преобразование legacy-API в Task Based с помощью класса TaskCompletionSource

Еще один важный для рассмотрения инструмент, это класс TaskCompletionSource. По функциям, назначению и принципу работы он чем-то может напомнить метод RegisterWaitForSingleObject класса ThreadPool о котором я писал выше. С помощью этого класса можно легко и удобно оборачивать старые асинхронные API в Task’и.

TaskCompletionSource как раз отлично подходит для обертки в Task’и legacy-API построенных вокруг событийной модели. Суть его работы в следующем: у объекта этого класса есть публичное свойство типа Task состоянием которого можно управлять через методы SetResult, SetException и пр. Класса TaskCompletionSource. В местах же где был применен оператор await к этому Task’у он будет выполнен или обрушен с исключением в зависимости от примененного к TaskCompletionSource метода. Если все еще не понятно, то давайте посмотрим на этот пример кода, где некое старое API времен EAP заворачивается в Task при помощи TaskCompletionSource: при срабатывании события Task будет переведен в состояние Completed, а метод применивший к этому Task’у оператор await возобновит свое выполнение получив объект result.

TaskCompletionSource Tips & Tricks

Обертка старых API это не все что можно провернуть с помощью TaskCompletionSource. Использование этого класса открывает интересную возможность проектирования различных API, на Task’ах, что не занимают потоки. А поток, как мы помним ресурс дорогой и количество их ограничено (в основном объемом RAM). Этого ограничения легко достичь разрабатывая, например, нагруженное web-приложение со сложной бизнес логикой. Рассмотрим те возможности о которых я говорю на реализации такого трюка как Long-Polling.

Если коротко суть трюка вот в чем: вам нужно получать от API информацию о некоторых событиях происходящих на его стороне, при этом API по каким-то причинам не может сообщить о событии, а может лишь вернуть состояние. Пример таких — все API построенные поверх HTTP до времен WebSocket или при невозможности по какой-то причине использовать эту технологию. Клиент может спросить у HTTP сервера. HTTP сервер не может сам спровоцировать общение с клиентом. Простым решением является опрос сервера по таймеру, но это создает дополнительную нагрузку на сервер и дополнительную задержку в среднем TimerInterval / 2. Для обхода этого был изобретен трюк получивший название Long Polling, которые предполагает задержку ответа от сервера до тех пор пока не истечет Timeout или не произойдет событие. Если событие произошло, то оно обрабатывается, если нет, то запрос посылается заново.

Но такое решение покажет себя ужасно, как только число клиентом ожидающих событие вырастет, т.к. Каждый такой клиент в ожидании события занимает целый поток. Да и получаем дополнительную задержку в 1мс на срабатывании события, чаще всего это не существенно, но зачем делать ПО хуже чем оно может быть? Если же убрать Thread.Sleep(1), то зазря загрузим одно ядро процессора на 100% в холостую вращаясь в бесполезном цикле. С помощью TaskCompletionSource можно легко переделать этот код и решить все обозначенные выше проблемы:

Этот код не является production-ready, а лишь демонстрационным. Для использования в реальных случаях нужно еще, как-минимум, обработать ситуацию когда сообщение пришло в момент, когда его никто не ожидает: в таком случае метод AsseptMessageAsync должен вернуть уже завершенный Task. Если же этот случай и является наиболее частым, то можно подумать и об использовании ValueTask.

При получении запроса на сообщение мы создаем и помещаем в словарь TaskCompletionSource, а далее ждем что произойдет первее: истечет заданный интервал времени или будет получено сообщение.

ValueTask: зачем и как

Из-за желания немного оптимизировать, и легкой боязни по-поводу того что сгенерирует Roslyn компилируя этот код, можно этот пример переписать следующим образом:

Действительно же оптимальным решением в этом случае будет оптимизировать hot-path, а именно получение значения из словаря вообще без лишних аллокаций и нагрузки на GC, в то время когда в тех редких случаях, когда нам все таки нужно идти в IO за данными все останется плюс/минус по старому:

Давайте подробнее разберем этот фрагмент кода: при наличиии значения в кеше мы создаем структуру, в противном случае реальный же таск будет завернут в значимый. Вызывающему коду все равно по какому пути выполнялся этот код: ValueTask с точки зрения синтаксиса C# будет вести себя так же как и обычный Task в этом случае.

TaskScheduler’ы: управление стратегиями запуска Task’ов

Для удобной отладки всего связанного с Task’ами в Visual Studio есть окно Tasks. В этом окне можно увидеть текущее состояние задачи и перейти к выполняемой в данный момент строчке кода.

PLinq и класс Parallel

Статический класс Parallel предоставляет методы для параллельного перебора коллекции Foreach, выполнения цикла For и выполнения нескольких делегатов в параллель Invoke. Выполнение текущего потока будет остановлено до окончания выполнения расчетов. Количество потоков можно сконфигурировать передав ParallelOptions последним аргументом. С помощью опций также можно указать TaskScheduler и CancellationToken.

Выводы

Когда я начинал писать эту статью по материалам своего доклада и информации что собрал за время работы после него, я не ожидал, что ее получится так много. Сейчас, когда текстовый редактор в котором я набираю эту статью укоризненно говорит мне о том, что пошла 15я страница, я подведу промежуточные итоги. Другие трюки, API, визуальные инструменты и подводные камни будут рассмотрены в следующей статье.

Источник

Операционные системы и программное обеспечение