await Task 和 await Func<Task>() 的区别

Difference between await Task and await Func<Task>()

我有一个程序需要 运行 并行执行多个异步任务。 Task1()Task2() 两个工具的主要区别:

Task1() 保存类型 Func<Task>FTask 并稍后等待 Task.WhenAny(FTask()),

Task2() 保存由 Task.Run() 生成的 Task 并稍后等待 Task.WhenAny(Task).

我认为它们应该是等价的,但是 Task1() 不能正常工作,但是 Task2() 可以。

这个程序的流程是: 如果空闲队列中有可用的任务上下文,则将其出列并使用异步任务对其进行设置;异步任务完成后将上下文放回空闲队列。

问题是 Task1() 保存 Func<Task> 不起作用,似乎异步任务有多个实例 运行ning.

完整代码为:

static async Task Task1()
{
    int qd = 4;
    var taskPool = Enumerable.Range(0, qd).Select(n => new TaskCtx()).ToArray();
    Queue<TaskCtx> qFree = new Queue<TaskCtx>(taskPool);
    Random r = new Random();

    long segs = 7, submit = 0;
    int token = 0;
    int ticket = 0;

    Console.WriteLine($"-------- Task1 start --------");
    
    while (submit < segs)
    {
        if (qFree.Count > 0)
        {
            var x = qFree.Dequeue();
            x.Token = token++;
            x.Delay1 = r.Next(10, 20);
            x.Delay2 = r.Next(30, 50);

            x.FTask = async () =>
            {
                Console.WriteLine($"[{x.Token}] start");
                await Task.Delay(x.Delay1);

                Console.WriteLine($"[{x.Token}] wait ticket: {ticket}");
                while (ticket != x.Token)
                {
                    await Task.Yield(); // yield and wait other task increases the ticket
                }
                
                Console.WriteLine($"[{x.Token}] acquire ticket: {ticket}");
                await Task.Delay(x.Delay2);
                Console.WriteLine($"[{x.Token}] release ticket: {ticket}");
                ticket++;
                
                qFree.Enqueue(x);
            };

            Console.WriteLine($"[{x.Token}] submit");
            submit++;
        }
        else await Task.WhenAny(taskPool.Select(x => x.FTask()));
    }

    await Task.WhenAll(taskPool.Select(x => x.FTask()));
    Console.WriteLine($"-------- Task1 finished --------");
}

static async Task Task2()
{
    int qd = 4;
    var taskPool = Enumerable.Range(0, qd).Select(n => new TaskCtx()).ToArray();
    Queue<TaskCtx> qFree = new Queue<TaskCtx>(taskPool);
    Random r = new Random();

    long segs = 7, submit = 0;
    int token = 0;
    int ticket = 0;

    Console.WriteLine($"-------- Task2 start --------");
    
    while (submit < segs)
    {
        if (qFree.Count > 0)
        {
            var x = qFree.Dequeue();
            x.Token = token++;
            x.Delay1 = r.Next(10, 20);
            x.Delay2 = r.Next(30, 50);

            x.Task = Task.Run(async () =>
            {
                Console.WriteLine($"[{x.Token}] start");
                await Task.Delay(x.Delay1);

                Console.WriteLine($"[{x.Token}] wait ticket: {ticket}");
                while (ticket != x.Token)
                {
                    await Task.Yield(); // yield and wait other task increases the ticket
                }
                
                Console.WriteLine($"[{x.Token}] acquire ticket: {ticket}");
                await Task.Delay(x.Delay2);
                Console.WriteLine($"[{x.Token}] release ticket: {ticket}");
                ticket++;
                
                qFree.Enqueue(x);
            });

            Console.WriteLine($"[{x.Token}] submit");
            submit++;
        }
        else await Task.WhenAny(taskPool.Select(x => x.Task));
    }

    await Task.WhenAll(taskPool.Select(x => x.Task));
    Console.WriteLine($"-------- Task2 finished --------");
}

public class TaskCtx
{
    public int Token;
    public int Delay1;
    public int Delay2;
    public Func<Task> FTask;
    public Task Task;

    public TaskCtx()
    {
        FTask = async () => await Task.CompletedTask;
        Task = Task.CompletedTask;
    }
}

Task1()的输出:

-------- Task1 start --------
[0] submit
[1] submit
[2] submit
[3] submit
[0] start
[1] start
[2] start
[3] start
[0] wait ticket: 0
[0] acquire ticket: 0
[2] wait ticket: 0
[1] wait ticket: 0
[3] wait ticket: 0
[0] release ticket: 0
[1] acquire ticket: 1   <-- [1] is running and got the ticket
[4] submit
[4] start
[1] start               <-- another [1] starts ???
[2] start
[3] start
[2] wait ticket: 1
[1] wait ticket: 1
[1] acquire ticket: 1
[4] wait ticket: 1
[3] wait ticket: 1
[1] release ticket: 1
[2] acquire ticket: 2
[2] acquire ticket: 2
[1] release ticket: 2
[3] acquire ticket: 3
[3] acquire ticket: 3
[5] submit
[6] submit
[4] start
[6] start
[2] start
[3] start
[2] wait ticket: 3
[4] wait ticket: 3
[6] wait ticket: 3
[3] wait ticket: 3
[3] acquire ticket: 3
[2] release ticket: 3
[4] acquire ticket: 4
[2] release ticket: 3
[4] acquire ticket: 4
[3] release ticket: 5
[6] acquire ticket: 6
[3] release ticket: 5
[3] release ticket: 7
[4] release ticket: 8
[4] release ticket: 8
[6] release ticket: 10

Task2()的输出:

-------- Task2 start --------
[0] submit
[1] submit
[2] submit
[3] submit
[0] start
[1] start
[2] start
[3] start
[2] wait ticket: 0
[1] wait ticket: 0
[3] wait ticket: 0
[0] wait ticket: 0
[0] acquire ticket: 0
[0] release ticket: 0
[1] acquire ticket: 1
[4] submit
[4] start
[4] wait ticket: 1
[1] release ticket: 1
[2] acquire ticket: 2
[5] submit
[5] start
[5] wait ticket: 2
[2] release ticket: 2
[3] acquire ticket: 3
[6] start
[6] submit
[6] wait ticket: 3
[3] release ticket: 3
[4] acquire ticket: 4
[4] release ticket: 4
[5] acquire ticket: 5
[5] release ticket: 5
[6] acquire ticket: 6
[6] release ticket: 6
-------- Task2 finished --------

更新: 我做了另一个实现,使用普通的异步函数,而不是匿名函数:

static async Task Task3()
{
    int qd = 4;
    var taskPool = Enumerable.Range(0, qd).Select(n => new TaskCtx()).ToArray();
    Queue<TaskCtx> qFree = new Queue<TaskCtx>(taskPool);
    Random r = new Random();

    long segs = 7, submit = 0;
    int token = 0;
    Ticket ticket = new Ticket(0);

    Console.WriteLine($"-------- Task3 start --------");
    
    while (submit < segs)
    {
        if (qFree.Count > 0)
        {
            var x = qFree.Dequeue();
            x.Token = token++;
            x.Delay1 = r.Next(10, 20);
            x.Delay2 = r.Next(30, 50);

            x.Task = RunTask3Async(x, ticket, qFree);

            Console.WriteLine($"[{x.Token}] submit");
            submit++;
        }
        else await Task.WhenAny(taskPool.Select(x => x.Task));
    }

    await Task.WhenAll(taskPool.Select(x => x.Task));
    Console.WriteLine($"-------- Task3 finished --------");
}

static async Task RunTask3Async(TaskCtx x, Ticket ticket, Queue<TaskCtx> qFree)
{
    Console.WriteLine($"[{x.Token}] start");
    await Task.Delay(x.Delay1);

    Console.WriteLine($"[{x.Token}] wait ticket: {ticket}");
    while (ticket.ticket != x.Token)
    {
        await Task.Yield(); // yield and wait other task increases the ticket
    }
    
    Console.WriteLine($"[{x.Token}] acquire ticket: {ticket}");
    await Task.Delay(x.Delay2);
    Console.WriteLine($"[{x.Token}] release ticket: {ticket}");
    ticket.ticket++;
    
    qFree.Enqueue(x);
}

public class Ticket
{
    public int ticket;

    public Ticket(int t)
    {
        ticket = t;
    }

    public override string ToString()
    {
        return $"{ticket}";
    }
}

Task3() 也有效,输出为:

-------- Task3 start --------
[0] start
[0] submit
[1] start
[1] submit
[2] start
[2] submit
[3] start
[3] submit
[0] wait ticket: 0
[3] wait ticket: 0
[0] acquire ticket: 0
[2] wait ticket: 0
[1] wait ticket: 0
[0] release ticket: 0
[1] acquire ticket: 1
[4] start
[4] submit
[4] wait ticket: 1
[1] release ticket: 1
[2] acquire ticket: 2
[5] start
[5] submit
[5] wait ticket: 2
[2] release ticket: 2
[3] acquire ticket: 3
[6] start
[6] submit
[6] wait ticket: 3
[3] release ticket: 3
[4] acquire ticket: 4
[4] release ticket: 4
[5] acquire ticket: 5
[5] release ticket: 5
[6] acquire ticket: 6
[6] release ticket: 6
-------- Task3 finished --------

更新2: 我尽量减少示例,但下面的代码没有这个问题。

var FTasks = Enumerable.Range(0, 8).Select(i => async () =>
{
    Console.WriteLine($"[{i}] start");
    await Task.Delay(i);
    Console.WriteLine($"[{i}] end");
}).ToArray();

await Task.WhenAll(FTasks.Select(x => x()));

Console.WriteLine("----------------");

var Tasks = Enumerable.Range(0, 8).Select(i => Task.Run(async () =>
{
    Console.WriteLine($"[{i}] start");
    await Task.Delay(i);
    Console.WriteLine($"[{i}] end");
})).ToArray();

await Task.WhenAll(Tasks);

这个问题本质上归结为调用

的区别
public Task MyMethod();

正在打电话

public void MyMethod()

使用Task.Run。本质区别在于第二个示例将在后台线程上显式 运行 。第一个示例将在当前线程上 运行 直到第一个等待。此时它可能会继续,也可能会在稍后安排 'continuation'。此延续将 运行 在与调用线程相同的同步上下文中,这意味着在 UI 程序中,如果从 [=] 调用,它将 运行 在 UI 线程上23=]线程。

对于像您这样的控制台程序,实际差别不大,因为第一个操作是 Task.Delay,而由于您没有 UI,它将 运行 线程池线程上的延续。

Task.Run(...) 存储在任务中和将其分配给 Func 之间的主要区别在于执行的时间。 Task.Run()直接开始工作而函数是惰性的,即调用函数时开始工作。

正如其他人所指出的,TaskFunc<Task> 之间的主要区别在于第一个代表一个操作(已经开始),第二个代表一个委托开始一个手术。这与任何 TFunc<T>.

的逻辑相同

所以,“为什么这些运行不止一次?”的问题通过查看您的 Task1:

来回答
        else await Task.WhenAny(taskPool.Select(x => x.FTask()));
    }

    await Task.WhenAll(taskPool.Select(x => x.FTask()));

每次您的代码调用 FTask() 时,它都会启动一个 new 异步操作。所以 每次 调用 Task.WhenAny 将启动 taskPool 中的所有任务,最后调用 Task.WhenAll 将启动 [=] 中的所有任务18=] 再次。

这个问题就像一样,每次等待异步委托时,都会创建一个新的异步任务实例并从头开始。

这是一个说明此问题的最小示例:

int i = 0;
var ftask = async () =>
{
    Console.WriteLine($"[{i++}] Func<Task>");
    await Task.Yield();
};

await ftask();
await ftask();

//--------------------

int j = 0;
var task = Task.Run(async () =>
{
    Console.WriteLine($"[{j++}] Task.Run()");
    await Task.Yield();
});

await task;
await task;

输出:

[0] Func<Task>
[1] Func<Task>
[0] Task.Run()