异步/等待线程转换的好奇心 [英] async/await thread transition curiosity

查看:94
本文介绍了异步/等待线程转换的好奇心的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下简单的控制台应用程序:

 class Program
{
    private static int times = 0;

    static void Main(string[] args)
    {
        Console.WriteLine("Start {0}", Thread.CurrentThread.ManagedThreadId);

        var task = DoSomething();
        task.Wait();

        Console.WriteLine("End {0}", Thread.CurrentThread.ManagedThreadId);

        Console.ReadLine();
    }

    static async Task<bool> DoSomething()
    {
        times++;

        if (times >= 3)
        {
            return true;
        }

        Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Task.Yield();
        });
        Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Task.Yield();
        });
        Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);

        bool b = await DoSomething();
        return b;
    }
}
 

与输出

Start 1
DoSomething-1 sleeping 1
DoSomething-1 sleep 3
DoSomething-1 awake 4
DoSomething-2 sleeping 4
DoSomething-2 sleep 4
DoSomething-2 awake 4
DoSomething-1 sleeping 4
DoSomething-1 sleep 3
DoSomething-1 awake 3
DoSomething-2 sleeping 3
DoSomething-2 sleep 3
DoSomething-2 awake 3
End 1

我知道控制台应用程序不提供SynchronizationContext,因此Tasks在线程池上运行.但是令我惊讶的是,当从DoSomething中的等待中恢复执行时,我们与等待中的线程处于同一线程.我以为我们要么返回到我们等待的线程,要么当我们继续执行等待方法时完全回到另一个线程.

有人知道为什么吗?我的榜样在某种程度上有缺陷吗?

解决方案

此行为归因于优化(这是实现细节).

具体来说,由await安排的继续使用TaskContinuationOptions.ExecuteSynchronously标志.这在任何地方都没有正式记录,但几个月前我确实遇到过,并且 blog帖子是最好的有关ExecuteSynchronously实际工作方式的文档.重要的一点是,如果ExecuteSynchronously的延续的任务调度程序与当前线程不兼容,则它实际上不会同步执行.

正如您所指出的,控制台应用程序没有SynchronizationContext,因此由await安排的任务继续将使用TaskScheduler.Current(在本例中为TaskScheduler.Default,即线程池任务计划程序)./p>

通过Task.Run启动另一个任务时,您正在线程池上显式执行该任务.因此,当到达其方法的结尾时,它将完成其返回的任务,从而导致继续执行(同步).由于await捕获的任务调度程序是线程池调度程序(因此与延续兼容),因此它将直接执行DoSomething的下一部分.

请注意,此处存在竞争条件.如果DoSomething的下一部分已经已经作为Task.Run返回的任务的延续,则将仅同步执行.在我的机器上,第一个Task.Run将在另一个线程上恢复DoSomething,因为在Task.Run委托完成时尚未附加延续.第二个Task.Run确实在同一线程上恢复DoSomething.

因此,我将代码修改为更具确定性;这段代码:

static Task DoSomething()
{
    return Task.Run(async () =>
    {
        Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
        });
        Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        var task = Task.Run(() =>
        {
            Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
        });
        Thread.Sleep(100);
        await task;
        Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);
    });
}

(在我的机器上)显示了竞争情况下的两种可能性:

Start 8
DoSomething-1 sleeping 9
DoSomething-1 sleep 10
DoSomething-1 awake 10
DoSomething-2 sleeping 10
DoSomething-2 sleep 11
DoSomething-2 awake 10
End 8

顺便说一句,您对Task.Yield的使用不正确;您必须await结果才能实际执行任何操作.

请注意,此行为(使用ExecuteSynchronouslyawait)是未记录的实现细节,将来可能会更改.

I have the following simple console application:

class Program
{
    private static int times = 0;

    static void Main(string[] args)
    {
        Console.WriteLine("Start {0}", Thread.CurrentThread.ManagedThreadId);

        var task = DoSomething();
        task.Wait();

        Console.WriteLine("End {0}", Thread.CurrentThread.ManagedThreadId);

        Console.ReadLine();
    }

    static async Task<bool> DoSomething()
    {
        times++;

        if (times >= 3)
        {
            return true;
        }

        Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Task.Yield();
        });
        Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Task.Yield();
        });
        Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);

        bool b = await DoSomething();
        return b;
    }
}

with the output

Start 1
DoSomething-1 sleeping 1
DoSomething-1 sleep 3
DoSomething-1 awake 4
DoSomething-2 sleeping 4
DoSomething-2 sleep 4
DoSomething-2 awake 4
DoSomething-1 sleeping 4
DoSomething-1 sleep 3
DoSomething-1 awake 3
DoSomething-2 sleeping 3
DoSomething-2 sleep 3
DoSomething-2 awake 3
End 1

I'm aware that console apps don't provide a SynchronizationContext so Tasks run on the thread pool. But what surprises me is that when resuming execution from an await in DoSomething, we are on the same thread as we are on inside the await. I had assumed that we'd either return to the thread we awaited on or be on another thread entirely when we resume execution of the awaiting method.

Does anyone know why? Is my example flawed in some way?

解决方案

This behavior is due to an optimization (which is an implementation detail).

Specifically, the continuation scheduled by await uses the TaskContinuationOptions.ExecuteSynchronously flag. This is not officially documented anywhere but I did encounter this a few months ago and wrote it up on my blog.

Stephen Toub has a blog post that is the best documentation on how ExecuteSynchronously actually works. One important point is that ExecuteSynchronously will not actually execute synchronously if the task scheduler for that continuation is not compatible with the current thread.

As you pointed out, console apps don't have a SynchronizationContext, so task continuations scheduled by await will use TaskScheduler.Current (which in this case is TaskScheduler.Default, the thread pool task scheduler).

When you start another task via Task.Run, you're explicitly executing it on the thread pool. So when it reaches the end of its method, it completes its returned task, causing the continuation to execute (synchronously). Since the task scheduler captured by await was the thread pool scheduler (and therefore compatible with the continuation), it will just directly execute the next portion of DoSomething.

Note that there is a race condition here. The next portion of DoSomething will only execute synchronously if it is already attached as a continuation to the task returned by Task.Run. On my machine, the first Task.Run will resume DoSomething on another thread because the continuation is not attached by the time the Task.Run delegate completes; the second Task.Run does resume DoSomething on the same thread.

So I modified the code to be slightly more deterministic; this code:

static Task DoSomething()
{
    return Task.Run(async () =>
    {
        Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
        });
        Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        var task = Task.Run(() =>
        {
            Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
        });
        Thread.Sleep(100);
        await task;
        Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);
    });
}

(on my machine) shows both of the possibilities from the race condition:

Start 8
DoSomething-1 sleeping 9
DoSomething-1 sleep 10
DoSomething-1 awake 10
DoSomething-2 sleeping 10
DoSomething-2 sleep 11
DoSomething-2 awake 10
End 8

BTW, your use of Task.Yield is incorrect; you have to await the result to actually do anything.

Note that this behavior (await using ExecuteSynchronously) is an undocumented implementation detail and may change in the future.

这篇关于异步/等待线程转换的好奇心的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆