Статьи

Обзор параллельной библиотеки задач (TPL)

Вступление

Помните те времена, когда нам нужно было создавать отдельный поток для выполнения длительных операций без блокировки выполнения приложения до завершения выполнения операции? Что ж, время радоваться; Эти времена давно прошли. Начиная с версии 4.5, Microsoft.NET Framework предоставляет новую библиотеку, которая представляет концепцию «задач» . Эта библиотека известна как библиотека параллельных задач ; или TPL.

Задачи против потоков

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

User user = null;
 
// Create background thread that will get the user from the repository.
Thread findUserThread = new Thread(() =>
{
    user = DataContext.Users.FindByName("luis.aguilar");
});
 
// Start background thread execution.
findUserThread.Start();
 
Console.WriteLine("Loading user..");
 
// Block current thread until background thread finishes assigning a
// value to the "user" variable.
findUserThread.Join();
 
// At this point the "user" variable contains the user instance loaded
// from the repository.
Console.WriteLine("User loaded. Name is " + user.Name);

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

Библиотека параллельных задач вводит понятие «задачи» . Задачи — это в основном операции, которые должны выполняться асинхронно, точно так же, как мы только что использовали «нотацию потоков». Это означает, что мы больше не говорим с точки зрения потоков , а вместо этого задач ; что позволяет нам выполнять асинхронные операции путем написания очень небольшого количества кода (что также намного проще для понимания и чтения). Теперь все изменилось навсегда, вот так:

Console.WriteLine("Loading user..");
 
// Create and start the task that will get the user from the repository.
var findUserTask = Task.Factory.StartNew(() => DataContext.Users.FindByName("luis.aguilar"));
 
// The task Result property hold the result of the async operation. If
// the task has not finished, it will block the current thread until it does.
// Pretty much like the Thread.Join() method.
var user = findUserTask.Result;
 
Console.WriteLine("User loaded. Name is " + user.Name);

Намного лучше, а? Конечно, это. Теперь мы имеем строго напечатанный результат асинхронной операции. Очень похоже на использование асинхронных делегатов, но без всего стандартного кода, необходимого для создания делегатов; которое возможно благодаря силе выражений # лямбда — C и встроенный делегатов ( Func, Action, Predicateи т.д.)

Задачи имеют свойство под названием Result. Это свойство содержит значение, возвращаемое лямбда-выражением, которое мы передали StartNew()методу. Что происходит, когда мы пытаемся получить доступ к этому свойству, когда задача еще выполняется? Что ж, выполнение вызывающего метода прекращается, пока задача не завершится. Это поведение аналогично Thread.Join()(строка 16 первого примера кода).

Задачи Продолжения

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

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

Console.WriteLine("Loading user..");
 
// Create tasks to be executed in fluent manner.
Task.Factory
    .StartNew<User>(() => DataContext.Users.FindByName("luis.aguilar")) // First task.
    .ContinueWith(previousTask =>
    {
        // This will execute after the first task finishes. First task's result
        // is passed as the first argument of this lambda expression.
        var user = previousTask.Result;
 
        Console.WriteLine("User loaded. Name is " + user.Name);
    });
 
// Tasks will start running asynchronously. You can do more things here...

Как бы многословно это ни звучало, вы можете прочитать предыдущий код, например «Запустить новую задачу, чтобы найти пользователя по имени, и продолжить, напечатав имя пользователя на консоли» . Важно отметить, что первым параметром ContinueWith()метода является ранее выполненная задача, которая позволяет нам получить доступ к его возвращаемому значению через его Resultсвойство.

Async And Await

Библиотека параллельных задач так много значит для Microsoft.NET Framework, что во все ее языковые спецификации были добавлены новые ключевые слова для решения асинхронных задач. Эти новые ключевые слова asyncи await.

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

Вот как это работает:

// 1. Awaiting For Tasks With Result:
async void LoadAndPrintUserNameAsync()
{
    // Create, start and wait for the task to finish; then assign the result to a local variable.
    var user = await Task.Factory.StartNew<User>(() => DataContext.Users.FindByName("luis.aguilar"));
 
    // At this point we can use the loaded user.
    Console.WriteLine("User loaded. Name is " + user.Name);
}
 
// 2. Awaiting For Task With No Result:
async void PrintRandomMessage()
{
    // Create, start and wait for the task to finish.
    await Task.Factory.StartNew(() => Console.WriteLine("Not doing anything really."));
}
 
// 3. Usage:
void RunTasks()
{
    // Load user and print its name.
    LoadAndPrintUserNameAsync();
 
    // Do something else.
    PrintRandomMessage();
}

Как видите, асинхронные методы теперь помечены аккуратным asyncмодификатором. Как я упоминал ранее, это означает, что они будут работать асинхронно; лучше сказать: в отдельной ветке. Важно уточнить, что асинхронные методы могут содержать внутри себя несколько дочерних задач, которые будут выполняться в любом порядке, но пометка метода как асинхронного означает, что при его традиционном вызове среда выполнения неявно переносит содержимое этого метода в объект задачи.

Например, написать это:

var loadAndPrintUserNameTask = LoadAndPrintUserAsync();

… эквивалентно написанию этого:

var loadAndPrintUserNameTask = new Task(LoadAndPrintUserAsync);

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

Теперь мы также можем создавать  ожидаемые  методы. Этот особый вид методов можно вызвать с помощью awaitключевого слова.

async Task LoadUserAsync()
{
    // Create, start and wait for the task to finish; then assign the result to a local variable.
    var user = await Task.Factory.StartNew<User>(() => DataContext.Users.FindByName("luis.aguilar"));
 
    // Return the loaded user. The runtime converts this to a Task<User> automagically.
    return user;
}

Все  ожидаемые методы указывают задачу в качестве типа ее возврата. Теперь есть вещи, которые нам нужно подробно обсудить здесь. Подпись этого метода указывает, что он имеет возвращаемое значение типа,  Task<User> но вместо этого он фактически возвращает загруженный пользовательский экземпляр (строка 7). Что это? Ну, этот метод может возвращать два типа значений в зависимости от сценария вызова.

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

Task loadUserTask = LoadUserAsync();
 
// The previous code is equivalent to:
Task loadUserTask = new Task<User>(() => LoadUserAsync().Result);

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

User user = await LoadUserAsync();
 
// The previous code is equivalent to:
User user = LoadUserAsync().Result;

Увидеть? Лично я впервые вижу метод, который может возвращать два типа значения в зависимости от того, как оно вызывается. Хотя это довольно интересно, такая вещь существует. Кстати, важно помнить, что любой метод, который в любой момент ожидает асинхронного метода с использованием awaitключевого слова, должен быть помечен как async.

Вывод

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

Это все, ребята. Оставайтесь в курсе!;)

Дальнейшее чтение