Почему нет перегрузки Task.Run, которая принимает задачу? - PullRequest
0 голосов
/ 25 апреля 2020

Я постоянно повторяю:

var task = f(); // some code returning a task

Task.Run(async () => await task);

Что для меня сбивает с толку код, на который я часто спотыкаюсь.
Есть ли причина , почему класс Task делает нет перегрузки, которая принимает задачу?

ie,

public static Task Run(Task task) => Task.Run(async () => await task);

?

1 Ответ

4 голосов
/ 25 апреля 2020

Есть ли причина, по которой класс Task не имеет перегрузки Run, которая принимает задачу?

Да, потому что ее не должно быть.

Ради этого ответа Task или Task<T> представляет некоторую операцию, которая выполняет некоторую работу и возвращает значение, которое может существовать или может еще не существовать . Это абстракция для всех видов работы (например, параллельная операция, выполняемая в другом потоке, асинхронная операция ввода-вывода, выполняемая в другом месте на аппаратном уровне, представление синхронной операции или что-то еще.

Что за Task / Task<T> не означает , не представляет собой Func<> или Action<> и не представляет собой «шаблон задания», который можно использовать для запуска новой операции (воспринимайте его как представление задания, которое уже началось ).

В частности, Task.Run: действительный метод Task.Run(Func<>) / Task.Run(Action) в. NET - сокращение от , начиная с a Func<> или Action<> в пул потоков планировщика по умолчанию (т.е. одновременно, многопоточный). Вы не можете «перезапустить» Task (граф конечного автомата Task строго однонаправлен), вы можете только запустить новый Task, используя любой Для запуска оригинального Task использовался механизм. Таким образом, вы не можете произвольно перезапустить асинхронную операцию Socket, например, потому что это будет означать перемотку состояния всей вашей программы, и это и нарушая физику l aws ...

Если у вас есть объект Task<T>, то (при условии, что вы используете его правильно), любая операция, которую он представляет, будет уже запланирована или иным образом запущена - или быть уже завершенным - поэтому вы не можете «запустить» Task, передав его Task.Run, потому что он уже был запущен (это упрощение).


Пример, который вы привели (reposted) ниже) ничего полезного не делает:

public static Task Run(Task task) => Task.Run(async () => await task);

Я переписал его в более длинной форме ниже, чтобы было легче следить:

public static Task Run(Task originalTask)
{
    LambdaCapture capture = new LambdaCapture( originalTask );
    Task poolTask = Run( capture.Run ); // Remember that a Delegate includes the `this` reference unlike a raw C-style function-pointer.
    return poolTask;
}

// Oversimplified representation of what Task.Run does:
public static Task Run( Action action )
{
    ThreadPool pool = GetThreadPoolFromSomewhere();

    TaskCompletionSource tsc = new TaskCompletionSource();

    Action wrappedAction = () =>
    {
        // Run the action:
        action();

        // When it completes, inform TaskCompletionSource:
        tsc.SetResult(); // Task (not `Task<T>`) has no result value.

        // When `SetResult()` is invoked, the thread running this code will not return to here until after it runs the contination scheduled after `originalTask`.
    };

    pool.AddJob( wrappedAction ); // Adds `wrappedAction` to a queue which is dequeued by the first available thread.

    return tsc.Task; // <-- this is a new Task created by the TaskCompletionSource.
}

private class LambdaCapture
{
    private readonly Task originalTask;

    public Runnable( Task originalTask )
    {
        this.originalTask = originalTask;
    }

    public async Task Run()
    {
         await this.originalTask;
    }
}

Когда вы предлагаете Task.Run(Task) метод вызывается, он делает это:

  1. Он запланирует LambdaCapture.Run запуск на первом доступном потоке в пуле потоков.
  2. Затем он создаст и вернет отдельный новый экземпляр Task для представления операции пула потоков (то есть параллельной операции) независимо от характера originalTask.
  3. Когда пул потоков становится доступным и рабочий поток запускает LambdaCapture.Run, он будет (оверси предупреждение mplification) проверьте, завершено ли originalTask, и если да, вернет и сообщит планировщику originalTask, что оно завершено, если нет, то запланирует оставшуюся часть Runnable.Run (т.е. все код после await (который в данном случае является просто одним оператором return;) для выполнения после завершения originalTask, сделав его продолжением .
  4. Так что, когда originalTask завершает (предполагая, что делает выполнено), тогда поток, которому назначено следующее продолжение из originalTask, затем выполнит оставшуюся часть операции Task.Run и сообщит планировщику пула рабочих потоков, что это сделано, и затем (предположительно) выполнить продолжение await из любого кода awaits Task, возвращенного из Task.Run.
    • Если это сбивает с толку, это потому, что я отстой в объяснении. Task<T> в C# работает по существу так же, как Promise<T> в JavaScript / TypeScript или std::promise в C ++.

Короче говоря: нет причин для делать то, что вы предлагаете, кроме как тратить циклы ЦП в потоке пула потоков. Как говорит @Fabio, просто сделайте await task в исходном методе.

Если вы не можете await task в своем исходном методе, потому что этот метод не является async методом, тогда даже если Task.Run(Task) Существовало, что это не поможет, потому что вам все еще нужно await Task, которое возвращается Task.Run.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...