【问题标题】:Difference between await Task and await Func<Task>()await Task 和 await Func<Task>() 的区别
【发布时间】:2022-02-08 10:58:24
【问题描述】:

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

Task1() 保存 FTask 类型的 Func&lt;Task&gt; 和稍后等待 Task.WhenAny(FTask())

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

我认为它们应该是等效的,但是 Task1() 不能正常工作,但是 Task2() 确实有效。

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

问题是 Task1() 保存 Func&lt;Task&gt; 不起作用,似乎异步任务有多个实例在运行。

完整代码为:

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);

【问题讨论】:

  • 伊万你的例子远非最小。如果您想深入了解物化 Task 和异步委托之间的区别,您可以通过使示例尽可能少来了解更多信息。一个五行十行的程序可能足以证明这些差异。
  • 谢谢@TheodorZoulias,我有一个最小的,但他们没有这个问题。请参阅 UPDATE2。
  • "下面的代码没有这个问题" -- 原来的复杂例子有什么问题,而更新的最小例子没有?跨度>
  • 嗨@TheodorZoulias,我更新了描述。该程序的流程是:如果空闲队列中有可用的任务上下文,则将其出列并使用异步任务对其进行设置;当异步任务完成时,将上下文放回空闲队列。问题是保存 Func 的 Task1() 不起作用,似乎异步任务有多个实例在运行。

标签: c# asynchronous async-await task-parallel-library


【解决方案1】:

正如其他人所指出的,TaskFunc&lt;Task&gt; 之间的主要区别在于,第一个代表一个操作(已经开始),第二个代表一个开始操作的委托。这与任何TFunc&lt;T&gt; 的逻辑相同。

那么,“为什么这些运行不止一次”的问题?通过查看您的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会再次启动taskPool中的所有任务。

【讨论】:

【解决方案2】:

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

【讨论】:

  • Task2() 的输出显示该函数也是惰性的,但是Task3() 使用x.Task = RunTask3Async(x, ticket, qFree);,并且在“提交”打印之前立即执行异步函数。
【解决方案3】:

这个问题本质上归结为调用之间的区别

public Task MyMethod();

打电话

public void MyMethod()

使用Task.Run。本质区别在于第二个示例将显式在后台线程上运行。第一个示例将在当前线程上运行,直到第一次等待。在这一点上,它可能会继续,或者可能会在以后安排“继续”。此延续将在与调用线程相同的同步上下文中运行,这意味着在 UI 程序中,如果从 UI 线程调用,它将在 UI 线程上运行。

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

【讨论】:

  • 但是Task1()的输出显示,好像启动了不止一个task [1]的实例,功能流程不对。
  • @ivan_wl 你的代码对我来说看起来不是线程安全的。您在没有任何形式的同步的情况下递增和读取值,这可能会导致多线程程序中出现意外行为。我强烈建议在尝试执行任何多线程操作之前学习线程安全编程。因为编写编译器未检测到的错误非常容易,并且可能很难重现。
  • 嗨@JonasH 我知道线程安全,我不认为这是线程同步或竞争条件问题。
【解决方案4】:

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

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

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()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-03-04
    • 2015-02-12
    • 2019-10-08
    • 2013-09-04
    • 2013-06-04
    • 1970-01-01
    • 2014-06-25
    相关资源
    最近更新 更多