Многопоточное программирование на c#, путевые заметки
TRANSCRIPT
Многопоточное
программирование на C#,
путевые заметки
Дмитрий Литичевский
ByndyuSoft
11-я конференция .NET разработчиков
31 октября 2015
dotnetconf.ru
2
О себе Team lead в компании ByndyuSoft
Аспирант ЧелГУ, дискретная математика и
математическая кибернетика
Любитель вселенной Warhammer 40000
В МРАЧНОЙ ТЬМЕ ДАЛЁКОГО БУДУЩЕГО ЕСТЬ ТОЛЬКО ВОЙНА...
3
Прежде чем начать
Зачем нам все это?
Хотим эффективно использовать имеющиеся ресурсы.
Когда это сработает?
Обрабатывается поток ресурсоемких слабосвязанных
друг с другом задач
4
Асинхронная обработка
Одним из способов ускорить приложения являетсяасинхронный запуск длительных операций, мызапускаем ее и не ждем завершения
Возможные примеры:
1. Cетевые запросы;
2. Доступ к диску;
3. Сложные вычисления.
l
Основное различие заключается в том, в каком потокевыполняется запущенная длительная операция.
5
Что предлагает нам C#
Event-based Asynchronous Pattern (EAP) private void DownloadContentFromUrl(Uri uri){
var client = new WebClient();client.DownloadStringCompleted += OnDownloadStringCompleted;client.DownloadStringAsync(uri);
}
Интерфейс IAsyncResultprivate void LookupHostName(){
Dns.BeginGetHostAddresses("dotnetconf.ru", OnNameResolved, null);}private void OnNameResolved(IAsyncResult ar){
var addresses = Dns.EndGetHostAddresses(ar);}
Task-based Asynchronous Pattern (TAP)
6
Договор с TAP
Не допустимы ref и out параметры методов.
Метод должен возвращать значение типа Task<T>, Task илиvoid в зависимости от возвращаемого значения синхронногометода.
Исключение из-за ошибки в параметрах вызова метода можновозбуждать непосредственно (используя оператор throw),остальные исключения следует помещать в объект Task.
Единый API для работы с асинхронными операциями, чтопозволяет реализовать на уровне компилятора такие средствакак async/await.
7
Примеры использования TAP
var x = await Task.Run(() => SolveSystem(a, b));
Task t = Task.Factory.StartNew(() => SolveSystem(a, b),cts.Token,TaskCreationOptions.LongRunning,TaskScheduler.Current);
private async Task DownloadPageAsync(string uri){
using (var client = new HttpClient())using (var response = await client.GetAsync(uri))using (var content = response.Content){
var result = await content.ReadAsStringAsync();// Processing
}}
8
TAP, обработка ошибок
Методы возвращают Task илиTask<T>, ожидаем простойтаск используя await.
try{
var pageContent = await webClient.DownloadStringTaskAsync(uri);// Processing
}catch (Exception ex){
// Handling}
Исключение возбуждается там, где находится операторawait, перехватывается, все красиво и здорово.
9
TAP, обработка ошибок
Методы возвращают Task илиTask<T>, ожидаемрезультат используя методы Wait, WaitAll, WaitAny
try{
var task = webClient.DownloadStringTaskAsync(uri);task.Wait();// Processing
}catch (AggregateException ex){
foreach (var inner in ex.InnerExceptions) {/*Handling*/ }}
AggregateException возбуждается в месте вызоваWait, WaitAll, WaitAny, возникшие исключениянаходятся в свойстве InnerExceptions.
10
TAP, обработка ошибокМетоды оформлены по стандарту, ожидаем составной таск
var allTask = Task.WhenAll(tasks);try{
await allTask;}catch{
foreach (var ex in allTask.Exception.InnerExceptions){
// Handling}
}
При выполнении тасков может возникать несколько исключений, но в операторе await возбуждается одно из них. Все исключения могутбыть найдены в коллекции InnerExceptions объекта классаAggregateException, лежащего в свойстве Exceptionожидаемого таска.
11
TAP, обработка ошибокАсинхронный метод возвращает Task или void, мы не хотим или не можем
дождаться его завершения. Тогда мы можем:
1. Реализовать обработку ошибок внутри самой операции;
2. Если метод все же возвращает таск, то добавить к нему Continuation
var task = DownloadPageAsync("http://dotnetconf.ru/");task.ContinueWith(t => t.Exception.Handle(e =>
{// Handlingreturn true;
}),
TaskContinuationOptions.OnlyOnFaulted);
private async Task DownloadPageAsync(string uri){
using (var webclient = new WebClient()){
var content = await webclient.DownloadStringTaskAsync(uri);// Processing
}}
12
TAP, отмена операцииИспользуется классы CancellationToken и CancellationTokenSource
Способы прерывания:
1. По прошествии определенного времени с помощью CancelAfter
2. В ручную с помощью Cancell
using (var cts = new CancellationTokenSource()){
cts.CancelAfter(TimeSpan.FromMilliseconds(100));try{
var task = DownloadPageAsync("http://dotnetconf.ru/", cts.Token);task.Wait(cts.Token);
}catch (OperationCanceledException){
// Handling}catch (AggregateException){
// Handling}
}
13
TAP, отмена операцииОбработка сводится к периодической проверке вызываемым кодом
свойства IsCancellationRequested и вызову методаThrowIfCancellationRequested объекта CancellationToken.
Task.Run(() =>{
for (var i = 0; i < n; i++){
// Calculationstoken.ThrowIfCancellationRequested();
}
}, token)
.Wait();
Если требуется освобождение ресурсов, то мы можем периодическипроверять флаг IsCancellationRequested.
14
Типы многопоточная обработки
Организацию многопоточной обработки данных вприложениях можно разделить на два типа:
1. Задачи заранее известны, организовывать их обработкубудем с использованием библиотек TPL или PLINQ.
2. Задачи генерируются в процессе обработки, модельproducer and consumer будем реализовывать припомощи класса ThreadPool или библиотеки Rx.
Данная классификация является условной призвана помочьавтору структурировать остальную часть доклада.
15
TPL, теория
TPL содержит класс Parallel, возможности которого аналогичныбиблиоке OpenMP для С++. Для распараллеливания циклов в классеобъявлены три перегруженных метода:
1. For, организует распаралеленный цикл for, в качестве одного изаргументов передается тело цикла.
2. ForEach, организует распаралеленную обработку коллекций,реализующих интерефейс IEnumerable<T>, в качестве одного изаргументов передается тело цикла.
3. Invoke, организует параллельный вызов экземпляров Action,переданных в качестве параметров.
Ошибки, возникшие и не перехваченные в обрабатывающих потоках,помещаются в экземпляр AggregateException. Вызвавшийметоды поток блокируется до завершения обработки.
16
TPL, примеры
Parallel.ForEach(orders,
new ParallelOptions {MaxDegreeOfParallelism = threadsCount},
UpdateOrder);
Parallel.Invoke(
() => DeleteSubjects(orderId),
() => DeleteDocuments(orderId),
() => DeleteSuppliers(orderId)
);
17
TPL, нюансыИспользование экземпляра ParallelOptions позволяет более полно
контролировать выполняемые операции:
1. MaxDegreeOfParallelism, регулирует количество потоков, которые будут использованы для обработки.
2. CancellationToken, токен, используемый для отменызапускаемой операции.
3. TaskScheduler, управляет запуском потоков обработки задач.
Использование класса Partitioner позволяет управлять разбиением задачмежду потоками и их балансировкой. TPL может динамически распределятьнагрузку между потоками или же может распределить их некоторымобразом перед стартом выполнения расчетов и не менять по ходуобработки, это настраивается с помощью Partitioner.Create.
var tasks = Enumerable.Range(1, 100).ToArray();Parallel.ForEach(Partitioner.Create(tasks, true), ProcessItem);
18
PLINQ, теория
PLINQ является реализацией LINQ, распеределенно выполняющейзапросы. Для этого были добавлены классыParallelEnumerable, ParallelQuery, ParallelQuery<T>, атакже нескольких других. Не все операторы LINQ реализованы вPLINQ.
Этапы выполнения запроса:
1. Выбор модели выполнения (параллельно или последовательно),будем считать, что запрос будет выполняться параллельно.
2. Распределение задач между потоками.
3. Запуск выполнения запроса;
4. Слияние результатов работы отдельных потоков в вызывающемпотоке;
5. Итерация по результатам запроса.
Ошибки, возникшие и не перехваченные в обрабатывающих потоках,помещаются в экземпляр AggregateException.
19
PLINQ, пример
var tradeItemsForUpdate = tradeItems.AsParallel().WithDegreeOfParallelism(_threadsCount).Where(UpdateNeeded).ToArray();
var range = Enumerable.Range(0, 100000000).ToArray();var partitioner = Partitioner.Create(range, true);var query = partitioner.AsParallel().Select(Calc);
Для написания запросов с использованием PLINQ достаточно вызватьметод разширения ParallelEnumerable.AsParallel послечего запрос будет строиться с использованием методов PLINQ.
20
PLINQ, нюансы
В случае 100% уверенности в том, что распараллеливание запросапринесет прирост в скорости, можно заставить среду исполнять егопараллельно, вызвав метода WithExecutionMode, вызванного c параметром ParallelExecutionMode.ForceParallelism.
var ordersForUpdate = orders.AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism).Where(UpdateNeeded).ToArray();
Разбиение задач между потоками настраивается. Для этогоиспользуется уже описанный ранее класс Partitioner и егометод Create. С его помощью можно включить динамическуюбалансировку задач, отключенную по умолчанию для массивов и коллекций, реализующих Ilist. Если описанного недостаточно, томожно реализовать свой Partitioner согласно требованиям TPL.
21
PLINQ, нюансыВ отличии от первых двух пунктов цепи выполнения запроса, существует
гораздо больше возможностей влияния на его выполнение. Допустимоследующее:
1. Управлять числом обрабатывающих потоков с помощью методаWithDegreeOfParallelism.
2. Управлять досрочным прекращением операций с помощью методаWithCancellation, передав в качестве параметра экземплярCancellationToken.
3. Управлять учетом порядка записей исходной коллекции при генерациирезультатов с помощью методов AsOrdered/AsUnordered.
var ordersForUpdate = orders.AsParallel().AsOrdered().WithDegreeOfParallelism(threadsCount).WithCancellation(cts.Token).Where(UpdateNeeded).ToArray();
22
PLINQ, нюансыДля обработки результатов запросов из примеров необходимо
слить вместе результаты отдельных потоков и отдать ихвызвавшему. Это настраивается при помощи методаWithMergeOptions, доступны следующие варианты:
1. FullyBuffered, результаты полностью буферизуются.
2. AutoBuffered, результаты частично буферизуются.
3. NotBuffered, результаты не буферизуются.
var ordersForUpdate = orders.AsParallel().WithMergeOptions(ParallelMergeOptions.FullyBuffered).Where(UpdateNeeded);
foreach (var order in ordersForUpdate)
UpdateOrder(order);
23
PLINQ, нюансы
Если обработка результатов запроса может быть выполненапараллельно и ее порядок не важен, то можновоспользоваться методом ForAll, передав в него тело цикла, сэкономив на слиянии промежуточных результатов.
orders.AsParallel().ForAll(x =>
{if(UpdateNeeded(x))
UpdateOrder(x);});
24
Использование ThreadPool
Простейший вариант параллельной обработки — это использованиеметода QueueUserWorkItem класса ThreadPool.
while (true){
var message = GetMessage(queueName);
if (message != null)ThreadPool.QueueUserWorkItem(DispatchMessage, message);
elseThread.Sleep(30000);
}
25
Использование ThreadPool
Для ограничения числа отправляемых в ThreadPool задач, например, причтении из очереди, можно использовать Semaphore.
_semaphore.WaitOne();ThreadPool.QueueUserWorkItem(DispatchMessage, message);
private void DispatchMessage(object state){
var message = (MessageInfo)state;try{
OnMessageReceive(this, message);}finally{
_semaphore.Release();}
}
26
Использование Rx
Состоит из интерфейсов Iobservable<T> и IObserver<T>, включенных в .NET начиная с версии 4 и nuget пакета Rx-Main.
public interface IObservable<T>{
IDisposable Subscribe(IObserver<T> observer);}public interface IObserver<T>{
void OnNext(T value);void OnCompleted();void OnException(Exception error);
}
Rx предлагает другую модель обработки данных, push вместо pull, о новыхсобытиях нас будет уведомлять сама библиотека.
27
Использование Rx
Библиотека позволяет настраивать, в каких потоках будут выполняться методыObserver'а. Для этих целей служат методы SubscribeOn и ObserveOnкласса Observable.
_customerService.GetCustomers()
.SubscribeOn(Scheduler.TaskPool)
.Subscribe(Customers.Add);
Метод ObserveOn определяет какой Scheduler'е будет использоваться длявызовов методов Observer'а. Метод SubscribeOn определяет где будетпорождаться и обрабатываться уведомления для Observer'ов. Интересныследующие Scheduler'ы:
1. Scheduler.NewThread, обработка будет запущена в отдельном потоке.
2. Scheduler.ThreadPool, обработка будет запущена в ThreadPool.
3. Scheduler.TaskPool, обработка будет запущена в TaskPool.
28
Литература и примерыЛитература:1. https://msdn.microsoft.com/ru-ru/library/dd997415(v=vs.110).aspx
2. https://msdn.microsoft.com/ru-ru/library/hh524395.aspx
3. https://msdn.microsoft.com/en-us/library/jj155759.aspx
4. http://habrahabr.ru/post/168669/
5. https://msdn.microsoft.com/en-us/library/dd997425(v=vs.110).aspx
6. https://msdn.microsoft.com/en-us/library/dd997411.aspx
7. http://stackoverflow.com/questions/15773008/reactive-framework-as-message-queue-using-blockingcollection
8. http://www.introtorx.com/content/v1.0.10621.0/15_SchedulingAndThreading.html
9. Асинхронное программирование в C# 5.0, Алекс Дэвис
10. Concurrency in C# Cookbook, Stephen Cleary
11. C# in depth, Jon Skeet
Примеры: github
29
Спасибо за внимание
Литичевский Дмитрий
ByndyuSoft
vk