.Net developer, data scientist
Среда разработки .Net и язык программирования C# предлагают несколько способов создания потоков. Всё эти способы одинаково работоспособны, то есть в результате вызова определенных инструкций в коде будет создан поток, в рамках которого будет выполняться определенная задача. С другой стороны различия между этими способами заключается в эффективности и оптимальном расходовании вычислительных ресурсов компьютера, на котором работает ваше приложение. Дело в том, что создание потока — это ресурсоемкий процесс. Для того чтобы определить как способ подходит к вашей ситуации, необходимо познакомиться с этими способами поближе.
Первый способ – это явное создание потока. Существует пространство имен System.Threading. В этом пространстве имен есть класс Thread («поток» на английском). Если мы создадим экземпляр этого класса, то есть объект этого класса, то в результате у нас будет объект потока в коде, который мы можем запустить. Однако, что конкретно будет выполняться в этом потоке? Существует 4 конструктора класса Thread (по состоянию на начало 2021 года). Один из этих конструкторов, в качестве аргумента принимает делегат с методом, который будет выполняться в рамках работы потока. О том, что такое делегат и как его использовать, уже рассказано на канале в соответствующим видео. Следует помнить, что создание потока таким образом не запускает выполнение задания в потоке автоматически. Для этой цели необходимо явно вызвать метод Start() на объекте потока.
var thread = new Thread(new ThreadStart(SampleMethod));
thread.Start();
// SampleMethod это метод с сигнатурой void SampleMethod()
Создание потока явно, используя класс Thread, имеет ряд недостатков. Если мы еще раз повторим сказанное ранее, то придём к выводу, что для каждого отдельного задания необходимо создавать новый поток. Нельзя переопределить в объекте потока задание. То есть один поток – это одно задание. Как уже было сказано ранее создание потоков стоит определенных затрат ресурсов компьютера и если мы планируем многопоточно выполнять большее количество заданий мы можем столкнуться с проблемами производительности. Итак, что делать в этой ситуации? Для такой ситуации предусмотрен следующий метод создания потоков.
Метод создания потоков, о котором я хотел бы сейчас рассказать, основан на повторном использовании уже существующих потоков. Так как потоки не надо создавать, мы получаем прирост эффективности работы нашего приложения в случае использования большого числа потоков. Этот метод заключается в использовании класса ThreadPool. Сложно перевести это название на русский язык, но я попробую – пусть это называется совокупность или фонд потоков. Если сказать в двух словах как это работает, то я скажу следующее – класс ThreadPool уже содержит в себе некоторое количество готовых к использованию потоков.
ThreadPool.QueueUserWorkItem(new WaitCallback(SampleMethod));
// SampleMethod это метод с сигнатурой void SampleMethod(Object objectState)
Данный метод имеет ряд преимуществ по сравнению с явным созданием потоков. Среди этих преимуществ можно выделить следующие:
Следующим способом создания потоков, а если быть точным, то речь идет о способе создания многопоточного кода является использование объектов класса Task, то есть заданий. Концепция задания – это концепция более высокого уровня по сравнению с потоком. Задание — это определённая работа, которая может быть выполнена позже. Задание можно создать, явно используя статический метод Task.Run который в качестве аргумента принимает делегат с методом – конкретным заданием к выполнению.
Task task = Task.Run(() => SampleMethod());
task.Wait();
О статических методах и статических классах я буду еще говорить в отдельном видео, поэтому рекомендую подписаться на канал – вас ждет много интересного. Объекты класса Task также можно создавать при помощи класса TaskFactory в котором есть метод StartNew. При использовании класса TaskFactory у нас есть возможность в конструкторе класса TaskFactory передать определенные параметры создания новых заданий, что может быть полезно если, мы хотим как-то подогнать создание новых заданий под наши нужды.
Task task = Task.Run(() => {
TaskFactory taskFactory = new TaskFactory();
taskFactory.StartNew(() => SampleMethod());
taskFactory.StartNew(() => SampleMethod());
taskFactory.StartNew(() => SampleMethod());
});
task.Wait();
Что еще важно знать о классе Task? Этот класс является обобщенным. Об обобщённых классах мы еще не говорили, но я планирую снять видео и на эту тему. Поэтому чтобы не
пропустить это событие, подписывайтесь на канал. Как там кто-то говорил? Лайк, подписка, колокольчик! Возвращаясь к нашей теме – я только скажу, что обобщённый параметр
типа класса Task обозначает тип возвращаемого значения при выполнении задания в объекте класса Task. Например, тип **Task
При создании многопоточных приложений используется концепция асинхронного программирования. Мы запускаем каки-то методы результат которых будет доступен через некоторое время. Эти методы запускаются в отдельных потоках. Результатом выполнения такого метода является объект класса Task или значение его свойства Result. Чтобы создать такой метод необходимо использование 2 ключевых слов: async и await.
Ключевое слов async указывается при декларировании метода и обозначает, что данный метод асинхронный. Результатом выполнения этого метода будет Task или **Task<Т>**. Внутри должна быть асинхронная операция, например обращение к веб услуге или к базе данных, также это могут быть задания, созданные при помощи Task.Run или TaskFactory. Перед такой операций ставится ключевое слово **await**. Например у нас есть асинхронный метод **GetSumAsync()**, вызов этого метода представленный ниже является корректным и позволяет получить результат сложения:
int sum = await GetSumAsync(1, 2); // в этой строчке значение переменной sum будет 3.
Еще один пример асинхронного кода:
static void Main(string[] args)
{
LongRunnginOperationAsync().Wait();
Console.ReadKey();
}
public static async Task LongRunnginOperationAsync()
{
await Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Incremented value: {i}");
Thread.Sleep(100);
}
});
}
Пример асинхронного кода с возвращаемым значением:
private static int _result;
static void Main(string[] args)
{
LongRunnginOperationAsync().Wait();
Console.WriteLine($"Result: {_result}");
Console.ReadKey();
}
public static async Task LongRunnginOperationAsync()
{
_result = await SumAsync();
}
public static async Task<int> SumAsync()
{
return await Task.Run(() =>
{
int result = 0;
for (int i = 0; i < 10; i++)
{
result += i;
}
return result;
});
}
Вы заметили, что я назвал асинхронный метод со словом Async. Это всего лишь конвенция названий асинхронных методов. Каждый асинхронный метод может благополучно работать и без частицы Async в названии, однако использование этой частицы повышает читаемость Вашего кода.
Объекты класса Task имеет еще несколько интересных свойств. Например, их можно соединять в цепочки. То есть завершение одного задания запускает следующее, и даже более того – вы можете управлять тем, в каком случае необходимо запускать следующее задание. У вас есть несколько вариантов – новое задание запустится если:
Это возможность достигается путем использования слова метода ContinueWith. Еще одной интересной особенностью объектов класса Task является возможность определять в рамках одного задания дочерние задания. В этом случае родительский объект класса Task будет считаться завершенным, когда всегда все дочерние задания будут заврешены.