Публикую оригинал статьи на Хабре, перевод которой выложен в блоге.
Вторая часть доступна Здесь Необходимость сделать что-то асинхронно, не дожидаясь результата здесь и сейчас, или разделить большую работу между несколькими выполняющими ее подразделениями существовала и до появления компьютеров.
С их появлением эта потребность стала весьма ощутимой.
Сейчас, в 2019 году, я пишу эту статью на ноутбуке с 8-ядерным процессором Intel Core, на котором параллельно работает более ста процессов, а потоков еще больше.
Рядом лежит слегка потертый телефон, купленный пару лет назад, у него на борту 8-ядерный процессор.
Тематические ресурсы пестрят статьями и видеороликами, где их авторы восхищаются флагманскими смартфонами этого года с 16-ядерными процессорами.
MS Azure предоставляет виртуальную машину со 128-ядерным процессором и 2 ТБ оперативной памяти менее чем за 20 долларов в час.
К сожалению, невозможно извлечь максимум и использовать эту мощь, не умея управлять взаимодействием потоков.
Терминология
Процесс - Объект ОС, изолированное адресное пространство, содержит потоки.Нить — объект ОС, наименьшая единица выполнения, часть процесса, потоки разделяют между собой память и другие ресурсы внутри процесса.
Многозадачность - свойство ОС, возможность запуска нескольких процессов одновременно Многоядерный - свойство процессора, возможность использовать несколько ядер для обработки данных Многопроцессорность - свойство компьютера, возможность одновременно физически работать с несколькими процессорами.
Многопоточность — свойство процесса, возможность распределять обработку данных между несколькими потоками.
Параллелизм - выполнение нескольких действий физически одновременно в единицу времени Асинхронность — выполнение операции без ожидания завершения этой обработки; результат выполнения можно обработать позже.
Метафора
Не все определения хороши, а некоторые требуют дополнительных пояснений, поэтому к формально введенной терминологии добавлю метафору о приготовлении завтрака.Приготовление завтрака в этой метафоре — это процесс.
Утром готовя завтрак я ( Процессор ) Я прихожу на кухню ( Компьютер ).
У меня 2 руки( Ядра ).
На кухне есть несколько приборов ( ИО ): духовка, чайник, тостер, холодильник.
Включаю газ, ставлю на нее сковороду и наливаю в нее масло, не дожидаясь, пока оно нагреется( асинхронно, неблокирующее-IO-ожидание ), достаю яйца из холодильника и разбиваю их в тарелку, потом взбиваю одной рукой( Тема №1 ) и второй ( Тема №2 ), держа тарелку (Общий ресурс).
Сейчас я бы хотела включить чайник, но рук не хватает( Потодовое голодание ) За это время нагревается сковорода (Обработка результата) в которую я выливаю взбитое.
Я тянусь к чайнику, включаю его и тупо смотрю, как в нем закипает вода( Блокировка-IO-Ожидание ), хотя за это время он мог бы помыть тарелку, где взбивал омлет. Я готовила омлет всего двумя руками, а больше у меня нет, но при этом в момент взбивания омлета происходило сразу 3 операции: взбивание омлета, удерживание тарелки, нагрев сковороды .
ЦП — самая быстрая часть компьютера, IO — это то, из-за чего чаще всего все тормозит, поэтому часто эффективное решение — занять чем-то ЦП, одновременно получая данные от IO. Продолжая метафору: Если бы в процессе приготовления омлета я бы попробовал еще и переодеться, это был бы пример многозадачности.
Важный нюанс: компьютеры справляются с этим гораздо лучше, чем люди.
Кухня с несколькими поварами, например в ресторане – многоядерный компьютер.
Многие рестораны в фуд-корте в торговом центре - дата-центре
.
NET-инструменты .
NET хорошо работает с потоками, как и со многими другими вещами.
С каждой новой версией вводится всё больше новых инструментов для работы с ними, новые уровни абстракции над потоками ОС.
При работе с построением абстракций разработчики фреймворка используют подход, который оставляет возможность при использовании высокоуровневой абстракции спуститься на один или несколько уровней ниже.
Чаще всего в этом нет необходимости, фактически это открывает дверь к выстрелу себе в ногу из дробовика, но иногда, в редких случаях, это может быть единственным способом решения проблемы, не решаемой на текущем уровне абстракции.
.
Под инструментами я подразумеваю как интерфейсы прикладного программирования (API), предоставляемые фреймворком, так и сторонние пакеты, а также целые программные решения, упрощающие поиск любых проблем, связанных с многопоточным кодом.
Начало темы
Класс Thread — это самый простой класс в .NET для работы с потоками.
Конструктор принимает один из двух делегатов: ThreadStart — Нет параметров ParametrizedThreadStart — с одним параметром типа object. Делегат будет выполнен во вновь созданном потоке после вызова метода Start. Если конструктору был передан делегат типа ParametrizedThreadStart, то в метод Start необходимо передать объект. Этот механизм нужен для передачи в поток любой локальной информации.
Стоит отметить, что создание потока — дорогостоящая операция, а сам поток — тяжелый объект хотя бы потому, что он выделяет 1 МБ памяти в стеке и требует взаимодействия с API ОС.
Класс ThreadPool представляет концепцию пула.new Thread(.
).
Start(.
);
В .
NET пул потоков — это инженерная часть, и разработчики из Microsoft приложили немало усилий, чтобы обеспечить его оптимальную работу в самых разных сценариях.
Общая концепция: С момента запуска приложения оно создает в фоновом режиме несколько резервных потоков и предоставляет возможность взять их в использование.
Если потоки используются часто и в больших количествах, пул расширяется в соответствии с потребностями вызывающего объекта.
Когда в пуле в нужный момент нет свободных потоков, он либо дождется возврата одного из потоков, либо создаст новый.
Отсюда следует, что пул потоков отлично подходит для некоторых краткосрочных действий и плохо подходит для операций, которые выполняются как сервисы на протяжении всей работы приложения.
Для использования потока из пула существует метод QueueUserWorkItem, который принимает делегат типа WaitCallback, имеющий ту же сигнатуру, что и ParametrizedThreadStart, и переданный ему параметр выполняет ту же функцию.
ThreadPool.QueueUserWorkItem(.
);
Менее известный метод пула потоков RegisterWaitForSingleObject используется для организации неблокирующих операций ввода-вывода.
Делегат, переданный этому методу, будет вызван, когда WaitHandle, переданный методу, станет «Освобожденным».
ThreadPool.RegisterWaitForSingleObject(.
)
В .
NET есть таймер потока, и он отличается от таймеров WinForms/WPF тем, что его обработчик будет вызываться для потока, взятого из пула.
System.Threading.Timer
Еще есть довольно экзотический способ отправить делегата на исполнение в поток из пула — метод BeginInvoke. DelegateInstance.BeginInvoke
Хотелось бы вкратце остановиться на функции, к которой можно вызвать многие из вышеперечисленных методов — CreateThread из Kernel32.dll Win32 API. Благодаря механизму внешних методов есть способ вызвать эту функцию.
Подобный вызов я видел только один раз в ужасном примере легаси-кода, и мотивация автора, сделавшего именно это, до сих пор остается для меня загадкой.
Kernel32.dll CreateThread
Просмотр и отладка потоков
Созданные вами потоки, все сторонние компоненты и пул .NET можно просмотреть в окне «Потоки» Visual Studio. В этом окне будет отображаться информация о потоке только тогда, когда приложение находится в режиме отладки и в режиме приостановки.
Здесь можно удобно просмотреть имена стека и приоритеты каждого потока, а также переключить отладку на конкретный поток.
Используя свойство Priority класса Thread, вы можете установить приоритет потока, который OC и CLR будут воспринимать как рекомендацию при разделении процессорного времени между потоками.
Параллельная библиотека задач
Библиотека параллельных задач (TPL) была представлена в .NET 4.0. Сейчас это стандартный и основной инструмент для работы с асинхронностью.
Любой код, использующий более старый подход, считается устаревшим.
Базовой единицей TPL является класс Task из пространства имен System.Threading.Tasks. Задача — это абстракция потока.
В новой версии языка C# у нас появился элегантный способ работы с Задачами — операторы async/await. Эти концепции позволили писать асинхронный код так, как если бы он был простым и синхронным, это позволило даже людям, плохо понимающим внутреннюю работу потоков, писать приложения, использующие их, приложения, которые не зависают при выполнении длительных операций.
Использование async/await — тема для одной или даже нескольких статей, но я постараюсь передать суть в нескольких предложениях: async — модификатор метода, возвращающего Task или void и await — неблокирующий оператор ожидания задачи.
Еще раз: оператор await в общем случае (есть исключения) будет освобождать текущий поток выполнения дальше, а когда Задача завершит свое выполнение, и поток (на самом деле правильнее было бы сказать контекст) , но об этом позже) продолжит выполнение метода дальше.
Внутри .
NET этот механизм реализован так же, как и return return, когда написанный метод превращается в целый класс, который является конечным автоматом и может выполняться отдельными частями в зависимости от этих состояний.
Любой желающий может написать любой простой код, используя async/await, скомпилировать и просмотреть сборку с помощью JetBrains dotPeek с включенным кодом, сгенерированным компилятором.
Давайте рассмотрим варианты запуска и использования Задачи.
В приведенном ниже примере кода мы создаем новую задачу, которая не делает ничего полезного ( Thread.Sleep(10000) ), но в реальной жизни это должна быть какая-то сложная работа с интенсивным использованием процессора.
using TCO = System.Threading.Tasks.TaskCreationOptions;
public static async void VoidAsyncMethod() {
var cancellationSource = new CancellationTokenSource();
await Task.Factory.StartNew(
// Code of action will be executed on other context
() => Thread.Sleep(10000),
cancellationSource.Token,
TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
scheduler
);
// Code after await will be executed on captured context
}
Задача создается с несколькими опциями: LongRunning — это подсказка о том, что задача не будет выполнена быстро, а значит, возможно, стоит подумать о том, чтобы не брать поток из пула, а создать отдельный для этой задачи, чтобы не навредить другим.
AttachedToParent — задачи можно упорядочить в иерархии.
Если была использована эта опция, то Задача может находиться в состоянии, когда она сама завершилась и ожидает выполнения своих дочерних элементов.
PreferFairness — означает, что Задачи, отправленные на выполнение раньше, лучше было бы выполнить, чем отправленные позже.
Но это всего лишь рекомендация и результат не гарантирован.
Второй параметр, передаваемый методу, — CancellationToken. Чтобы корректно обработать отмену операции после ее запуска, исполняемый код должен быть наполнен проверками состояния CancellationToken. Если проверок нет, то метод Cancel, вызванный у объекта CancellationTokenSource, сможет остановить выполнение Задания только до его запуска.
Последний параметр — это объект планировщика типа TaskScheduler. Этот класс и его потомки предназначены для управления стратегиями распределения задач по потокам; по умолчанию Задача будет выполняться в случайном потоке из пула.
Оператор await применяется к созданному Task, а это значит, что код, написанный после него, если он есть, будет выполняться в том же контексте (часто это означает в том же потоке), что и код перед await. Метод помечен как async void, что означает, что он может использовать оператор await, но вызывающий код не сможет дождаться выполнения.
Если такая возможность необходима, то метод должен вернуть Task. Методы с пометкой async void встречаются довольно часто: как правило, это обработчики событий или другие методы, работающие по принципу «выстрелил-забыл».
Если вам нужно не только дать возможность дождаться окончания выполнения, но и вернуть результат, то вам нужно использовать Task. На Task, который вернул метод StartNew, как и на любом другом, можно вызвать метод ConfigurationAwait с параметром false, тогда выполнение после await продолжится не на захваченном контексте, а на произвольном.
Это следует делать всегда, когда контекст выполнения не важен для кода после ожидания.
Это также рекомендация MS при написании кода, который будет поставляться в виде библиотеки.
Остановимся немного подробнее на том, как можно дождаться завершения Задания.
Ниже приведен пример кода с комментариями о том, когда ожидание выполняется условно хорошо, а когда — условно плохо.
public static async void AnotherMethod() {
int result = await AsyncMethod(); // good
result = AsyncMethod().
Result; // bad AsyncMethod().
Wait(); // bad
IEnumerable<Task> tasks = new Task[] {
AsyncMethod(), OtherAsyncMethod()
};
await Task.WhenAll(tasks); // good
await Task.WhenAny(tasks); // good
Task.WaitAll(tasks.ToArray()); // bad
}
В первом примере мы ждем завершения задачи, не блокируя вызывающий поток; мы вернемся к обработке результата только тогда, когда он уже есть; до тех пор вызывающий поток предоставлен самому себе.
Во втором варианте мы блокируем вызывающий поток до тех пор, пока не будет вычислен результат работы метода.
Это плохо не только потому, что мы заняли поток, столь ценный ресурс программы, простым бездействием, но и потому, что если код метода, который мы вызываем, содержит await, а контекст синхронизации требует возврата в вызывающий поток после await, то мы получим взаимоблокировку: Вызывающий поток ожидает вычисления результата асинхронного метода, асинхронный метод тщетно пытается продолжить свое выполнение в вызывающем потоке.
Еще одним недостатком этого подхода является сложная обработка ошибок.
Дело в том, что ошибки в асинхронном коде при использовании async/await очень легко обрабатывать — они ведут себя так же, как если бы код был синхронным.
Хотя, если мы применим экзорцизм синхронного ожидания к Задаче, исходное исключение превратится в AggregateException, т. е.
чтобы обработать исключение, вам придется изучить тип InnerException и написать цепочку if самостоятельно внутри одного блока catch или использовать конструкцию catch When, вместо более привычной в мире C# цепочки блоков catch. Третий и последний примеры также отмечены как плохие по той же причине и содержат все те же проблемы.
Методы WhenAny и WhenAll чрезвычайно удобны для ожидания группы Заданий; они объединяют группу задач в одну, которая срабатывает либо при первом запуске задачи из группы, либо когда все они завершают свое выполнение.
Остановка потоков
По разным причинам может возникнуть необходимость остановить поток после его начала.Есть несколько способов сделать это.
Класс Thread имеет два метода с соответствующими именами: Аборт И Прерывать .
Первый крайне не рекомендуется использовать, так как после его вызова в любой случайный момент при обработке любой инструкции будет выброшено исключение ThreadAbortedException .
Вы же не ожидаете, что такое исключение будет выдано при увеличении любой целочисленной переменной, не так ли? И при использовании этого метода это вполне реальная ситуация.
Если вам нужно запретить CLR генерировать такое исключение в определенном участке кода, вы можете обернуть его в вызовы.
Thread.BeginCriticalRegion , Thread.EndCriticalRegion .
Любой код, написанный в блокеfinally, заключен в такие вызовы.
По этой причине в глубине кода фреймворка можно найти блоки с пустым try, но не с пустым наконец.
Microsoft настолько не одобряет этот метод, что не включила его в ядро .
net. Метод Interrupt работает более предсказуемо.
Он может прервать поток с исключением ThreadInterruptedException только в те моменты, когда поток находится в состоянии ожидания.
Он переходит в это состояние во время зависания во время ожидания WaitHandle, блокировки или после вызова Thread.Sleep. Оба описанных выше варианта плохи своей непредсказуемостью.
Решение состоит в использовании структуры Токен отмены и класс ОтменаТокенИсточник .
Суть в следующем: создается экземпляр класса CancellationTokenSource и только тот, кто им владеет, может остановить операцию, вызвав метод Отмена .
В саму операцию передается только CancellationToken. Владельцы CancellationToken не могут сами отменить операцию, а могут только проверить, была ли операция отменена.
Для этого есть логическое свойство IsCancellationRequested и метод ThrowIfCancelRequested .
Последний выдаст исключение TaskCancelledException если метод Cancel был вызван для копируемого экземпляра CancellationToken. И именно этот метод я рекомендую использовать.
Это улучшение по сравнению с предыдущими вариантами, поскольку мы получаем полный контроль над тем, в какой момент операция исключения может быть прервана.
Самый жестокий вариант остановки потока — вызвать функцию Win32 API TerminateThread. Поведение CLR после вызова этой функции может быть непредсказуемым.
В MSDN об этой функции написано следующее: «TerminateThread — опасная функция, которую следует использовать только в самых крайних случаях.
«
Преобразование устаревшего API в основанный на задачах с использованием метода FromAsync
Если вам посчастливилось работать над проектом, который стартовал после введения Заданий и перестал вызывать тихий ужас у большинства разработчиков, то вам не придется иметь дело с множеством старых API, как сторонних, так и тех, которые есть у вашей команды.в прошлом подвергался пыткам.
К счастью, команда .
NET Framework позаботилась о нас, хотя, возможно, целью было позаботиться о себе.
Как бы то ни было, в .
NET имеется ряд инструментов для безболезненного преобразования кода, написанного с использованием старых подходов асинхронного программирования, в новый.
Одним из них является метод FromAsync TaskFactory. В приведенном ниже примере кода я помещаю старые асинхронные методы класса WebRequest в задачу, используя этот метод. object state = null;
WebRequest wr = WebRequest.CreateHttp(" http://github.com ");
await Task.Factory.FromAsync(
wr.BeginGetResponse,
we.EndGetResponse
);
Это всего лишь пример и вряд ли вам придется делать это со встроенными типами, но любой старый проект просто кишит методами BeginDoSomething, возвращающими IAsyncResult, и методами EndDoSomething, которые его получают.
Преобразование устаревшего API в основанный на задачах с помощью класса TaskCompletionSource.
Еще один важный инструмент, который следует учитывать, — это класс TaskCompletionSource .По функциям, назначению и принципу работы он может чем-то напоминать метод RegisterWaitForSingleObject класса ThreadPool, о котором я писал выше.
Используя этот класс, вы можете легко и удобно обернуть старые асинхронные API в задачи.
Вы скажете, что я уже говорил о предназначенном для этих целей методе FromAsync класса TaskFactory. Здесь нам придется вспомнить всю историю развития асинхронных моделей в .
net, которую Microsoft предлагала за последние 15 лет: до Task-Based Asynchronous Pattern (TAP) существовал Asynchronous Programming Pattern (APP), который речь шла о методах Начинать DoSomething возвращается IAsyncResult и методы Конец DoSomething, который его принимает, и для наследия этих лет метод FromAsync просто идеален, но со временем его заменил асинхронный шаблон на основе событий ( EAP ), который предполагал, что событие будет вызвано после завершения асинхронной операции.
TaskCompletionSource идеально подходит для упаковки задач и устаревших API, построенных на основе модели событий.
Суть его работы заключается в следующем: объект этого класса имеет публичное свойство типа Task, состоянием которого можно управлять через методы SetResult, SetException и т.д. класса TaskCompletionSource. В местах, где к этой задаче был применен оператор await, он будет выполнен или завершится с ошибкой в зависимости от метода, примененного к TaskCompletionSource. Если все еще неясно, давайте посмотрим на этот пример кода, где какой-то старый API EAP обернут в Задача с использованием TaskCompletionSource: при срабатывании события Задача будет переведена в состояние Завершено, а метод, применивший оператор ожидания этой Задаче возобновит свое выполнение после получения объекта результат .
public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {
var completionSource = new TaskCompletionSource<Result>();
someApiObj.Done +=
result => completionSource.SetResult(result);
someApiObj.Do();
result completionSource.Task;
}
Советы и подсказки по TaskCompletionSource
Обертывание старых API — это не все, что можно сделать с помощью TaskCompletionSource. Использование этого класса открывает интересную возможность разработки различных API для задач, не занятых потоками.А поток, как мы помним, — ресурс дорогой и их количество ограничено (в основном объемом оперативной памяти).
Этого ограничения можно легко добиться, разработав, например, загружаемое веб-приложение со сложной бизнес-логикой.
Давайте рассмотрим возможности, о которых я говорю, при реализации такого трюка, как Long-Polling. Вкратце, суть трюка такова: вам нужно получить от API информацию о каких-то событиях, происходящих на его стороне, при этом API по каким-то причинам не может сообщить о событии, а может только вернуть состояние.
Примером тому являются все API, построенные поверх HTTP до времен WebSocket или когда по каким-то причинам было невозможно использовать эту технологию.
Клиент может запросить HTTP-сервер.
HTTP-сервер не может сам инициировать связь с клиентом.
Простое решение — опрос сервера с помощью таймера, но это создает дополнительную нагрузку на сервер и дополнительную задержку в среднем TimerInterval/2. Чтобы обойти это, был придуман трюк под названием Long Polling, который предполагает задержку ответа от серверу до тех пор, пока не истечет тайм-аут или не произойдет событие.
Если событие произошло, то оно обрабатывается, если нет, то запрос отправляется повторно.
while(!eventOccures && !timeoutExceeded) {
CheckTimout();
CheckEvent();
Thread.Sleep(1);
}
Но такое решение окажется ужасным, как только число клиентов, ожидающих события, увеличится, потому что.
Каждый такой клиент занимает целый поток, ожидающий события.
Да, и мы получаем дополнительную задержку в 1мс при срабатывании события, чаще всего это несущественно, но зачем делать софт хуже, чем он может быть? Если мы удалим Thread.Sleep(1), то зря мы будем на 100% нагружать одно ядро процессора в режиме ожидания, вращаясь в бесполезном цикле.
Используя TaskCompletionSource, вы можете легко переделать этот код и решить все проблемы, указанные выше: class LongPollingApi {
private Dictionary<int, TaskCompletionSource<Msg>> tasks;
public async Task<Msg> AcceptMessageAsync(int userId, int duration) {
var cs = new TaskCompletionSource<Msg>();
tasks[userId] = cs;
await Task.WhenAny(Task.Delay(duration), cs.Task);
return cs.Task.IsCompleted ? cs.Task.Result : null;
}
public void SendMessage(int userId, Msg m) {
if (tasks.TryGetValue(userId, out var completionSource))
completionSource.SetResult(m);
}
}
Этот код не готов к использованию, это всего лишь демонстрация.
Чтобы использовать его в реальных случаях, вам также необходимо как минимум обработать ситуацию, когда сообщение приходит в тот момент, когда его никто не ожидает: в этом случае метод AsseptMessageAsync должен возвращать уже завершенный Task. Если это наиболее распространенный случай, то можно подумать об использовании ValueTask. Когда мы получаем запрос на сообщение, мы создаем и помещаем TaskCompletionSource в словарь, а затем ждем, что произойдет раньше: истечет указанный интервал времени или будет получено сообщение.
ValueTask: почему и как
Операторы async/await, как и оператор return return, генерируют из метода конечный автомат, а это создание нового объекта, что почти всегда не важно, но в редких случаях может создать проблему.В этом случае может быть метод, который вызывается очень часто, речь идет о десятках и сотнях тысяч вызовов в секунду.
Если такой метод написан так, что в большинстве случаев возвращает результат в обход всех await-методов, то в .
NET предусмотрен инструмент для оптимизации этого — структура ValueTask. Чтобы было понятно, рассмотрим пример его использования: есть кэш, к которому мы заходим очень часто.
В нем есть какие-то значения и потом мы их просто возвращаем; если нет, то мы переходим к медленному вводу-выводу, чтобы получить их.
Последнее я хочу сделать асинхронно, а значит, весь метод окажется асинхронным.
Таким образом, очевидный способ написания метода следующий: public async Task<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return val;
return await RequestById(id);
}
Из-за желания немного оптимизировать и небольшого страха перед тем, что сгенерирует Roslyn при компиляции этого кода, вы можете переписать этот пример следующим образом: public Task<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return Task.FromResult(val);
return RequestById(id);
}
Действительно, оптимальным решением в данном случае будет оптимизация hot-path, а именно получение значения из словаря без лишних аллокаций и нагрузки на GC, а в тех редких случаях, когда нам все равно нужно обращаться к IO за данными , всё останется плюс/минус по-старому: public ValueTask<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return new ValueTask<string>(val);
return new ValueTask<string>(RequestById(id));
}
Давайте посмотрим на этот кусок кода подробнее: если в кеше есть значение, мы создаём структуру, иначе реальная задача будет обернута осмысленной.
Вызывающему коду не важно, по какому пути был выполнен этот код: ValueTask с точки зрения синтаксиса C# в этом случае будет вести себя так же, как обычная задача.
TaskSchedulers: управление стратегиями запуска задач
Следующий API, который я хотел бы рассмотреть, — это класс Диспетчер задач и его производные.Выше я уже упоминал, что в TPL есть возможность управлять стратегиями распределения Заданий по потокам.
Такие стратегии определены в потомках класса TaskScheduler. Практически любую стратегию, которая вам может понадобиться, можно найти в библиотеке.
Параллельные расширенияДополнительно , разработанный Microsoft, но не являющийся частью .
NET, а поставляемый в виде пакета Nuget. Кратко рассмотрим некоторые из них: CurrentThreadTaskScheduler — выполняет задачи в текущем потоке LimitedConcurrencyLevelTaskScheduler — ограничивает количество одновременно выполняемых Заданий параметром N, который принимается в конструкторе OrderedTaskScheduler — определяется как LimitedConcurrencyLevelTaskScheduler(1), поэтому задачи будут выполняться последовательно.
Планировщик задач - реализует воровство работы подход к распределению задач.
По сути, это отдельный ThreadPool. Решает проблему то, что в .
NET ThreadPool — статический класс, один для всех приложений, а это значит, что его перегрузка или неправильное использование в одной части программы может привести к побочным эффектам в другой.
Более того, понять причину подобных дефектов крайне сложно.
Что.
Может возникнуть необходимость использовать отдельные планировщики WorkStealingTaskSchedulers в тех частях программы, где использование ThreadPool может быть агрессивным и непредсказуемым.
QueuedTaskScheduler — позволяет выполнять задачи по правилам приоритетной очереди ThreadPerTaskScheduler — создает отдельный поток для каждой выполняемой на нем задачи.
Может быть полезно для задач, выполнение которых занимает непредсказуемо много времени.
Есть хорошая подробная статья о TaskSchedulers в блоге Microsoft. Для удобной отладки всего, что связано с Задачами, в Visual Studio есть окно Задачи.
В этом окне вы можете увидеть текущее состояние задачи и перейти к текущей исполняемой строке кода.
PLinq и параллельный класс
Помимо Tasks и всего сказанного о них, в .NET есть еще два интересных инструмента: PLinq (Linq2Parallel) и класс Parallel. Первый обещает параллельное выполнение всех операций Linq в нескольких потоках.
Количество потоков можно настроить с помощью метода расширения WithDegreeOfParallelism. К сожалению, чаще всего PLinq в режиме по умолчанию не имеет достаточно информации о внутренностях вашего источника данных, чтобы обеспечить значительный прирост скорости, с другой стороны, стоимость попытки очень мала: вам просто нужно перед этим вызвать метод AsParallel. цепочку методов Linq и запустить тесты производительности.
Более того, можно передать в PLinq дополнительную информацию о характере вашего источника данных с помощью механизма Partitions. Вы можете прочитать больше Здесь И Здесь .
Статический класс Parallel предоставляет методы для параллельного прохода по коллекции Foreach, выполнения цикла For и выполнения нескольких делегатов в параллельном вызове Invoke. Выполнение текущего потока будет остановлено до завершения вычислений.
Количество потоков можно настроить, передав ParallelOptions в качестве последнего аргумента.
Вы также можете указать TaskScheduler и CancellationToken, используя параметры.
выводы
Когда я начал писать эту статью на основе материалов моего доклада и информации, которую я собрал в ходе работы после него, я не ожидал, что ее будет так много.Теперь, когда текстовый редактор, в котором я набираю эту статью, укоризненно сообщит мне, что страница 15 ушла, я подведу промежуточные итоги.
Другие трюки, API, визуальные инструменты и подводные камни будут описаны в следующей статье.
Выводы: Вам необходимо знать инструменты работы с потоками, асинхронностью и параллелизмом, чтобы использовать ресурсы современных ПК.
В .
NET есть много разных инструментов для этих целей.
Не все они появились сразу, поэтому часто можно встретить устаревшие, однако есть способы конвертировать старые API без особых усилий.
Работа с потоками в .
NET представлена классами Thread и ThreadPool. Методы Thread.Abort, Thread.Interrupt, функция Win32 API TerminateThread опасны и не Теги: #Системное администрирование #C++ #.
NET #асинхронность #async/await #ASP #ASP #многопоточность #асинхронный #многопоточность #параллелизм #TPL #TPL
-
Преимущества Законного Ввода Данных Дома
19 Oct, 24 -
Егор Хомяков Продолжает Хакерство
19 Oct, 24 -
Подкаст 3.14Zdim | Выпуск 5
19 Oct, 24