c#中async、await的几个注意点

6 篇文章 0 订阅

1.先举例说明异步方法的执行顺序 

class Program
    {
        static  void Main(string[] args)
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
            Console.ReadKey();
        }
        static async void Async()
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            await Task.Run(() =>                                                      
            {
                Thread.Sleep(500);
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb");
            });
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

执行结果:

引用MSDN中的结论:

  • 标记的异步方法可以使用 await 来指定暂停点。 await 运算符通知编译器异步方法:在等待的异步过程完成后才能继续通过该点。 同时,控制异步方法返回至的它的调用方

     

 2. 如果主线程遇到await时,await标记的Task已经完成,或者没有await标记,那么主线程不会立即返回跳出异步方法,而是继续执行await后面的语句,如下例:

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
            Console.ReadKey();
        }
        static async void Async()
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            Task task = Task.Run(() =>                                                      
            {
                Thread.Sleep(500);
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb");
            });
            Thread.Sleep(1000);//等待task执行完毕
            await task;
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

执行结果:

这个例子中,task启用线程池线程开始执行内部语句,接着,主线程休息1秒钟,然后遇到await,但是此刻await标记的task在主线程休息的1秒时间内已经执行完毕,所以主线程不会跳出异步方法Async,而是继续执行后面的语句,打印出“ccc“。

可以认为,程序发现await后面跟的task状态已经完成,则不会暂停和跳出。而是继续执行下去。

下面同例:如果发现是不需要等待(非可等待)的语句,会直接执行。

private async void button_Click(object sender, EventArgs e)
        {
            Console.WriteLine("button1_Click线程ID:" + Thread.CurrentThread.ManagedThreadId);
            await CalMyMethodAsync();
        }

        private async Task CalMyMethodAsync()
        {
            Console.WriteLine("CalMyMethodAsync线程ID:" + Thread.CurrentThread.ManagedThreadId);//不需要等待的时 不会开线程
            await MyMethodAsync();
        }

        private async Task MyMethodAsync()
        {
            await MySecondAsync();
        }

        private async Task MySecondAsync()
        {
            Console.WriteLine("MySecondAsync线程ID:" + Thread.CurrentThread.ManagedThreadId);//不需要等待的时 不会开线程
            await Task.Factory.StartNew(() =>
            {
                Console.WriteLine("MySecondAsync的异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
            });
        }

输出:

 

3.await后面的语句不一定由线程池线程执行(不考虑2中await标记的task已经执行完的情况)

将上述的代码拷贝到winform程序中运行:

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
        }

        static async void Async()
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            Task task = Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb");
            });
            await task;
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

执行结果:

------------------------------------

此处先分享一个小技巧:在winform项目的属性中,将输出类型改为Console Application,然后再运行项目,这样既会弹出winform窗体,同时还会弹出控制台输出窗体

------------------------------------ 

可以看出:

在winform程序中,await后面的语句是由主线程执行的,更确切的说,是由UI线程执行的。

而在1中的控制台程序中,await后面的语句是由线程池线程执行的。通过一些测试可以推测,控制台程序中,往往是由await标记的task中启用的线程来执行,如果await标记的是多个task(如:await Task.WhenAll()),则由某一个task开启的线程执行,举例如下:

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
            Console.ReadKey();
        }
        static async void Async()
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            Task task = Task.Run(() =>                                                      
            {
                Thread.Sleep(500);
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb1");
            });
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb2");
            });
            Task task3 = Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb3");
            });
            await Task.WhenAll(task, task2, task3);//多个task
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

执行结果:

至于原因,可参看MSDN文档:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

文档指出异步方法是在当前同步上下文中执行的,Windows 窗体应用程序会创建并安装一个 WindowsFormsSynchronizationContext 作为创建 UI 控件的线程(即UI线程)的当前同步上下文,WindowsFormsSynchronizationContext 的上下文是一个单独的 UI 线程;而控制台应用程序,具有一个默认的SynchronizationContext,默认的SynchronizationContext应用于线程池线程。根据惯例,如果一个线程的当前 SynchronizationContext 为 null,那么它隐式具有一个默认 SynchronizationContext(这句话接下来会用到)

关于SynchronizationContext的相关知识;有兴趣可以参考以下文章:

https://msdn.microsoft.com/magazine/gg598924.aspx

https://www.cnblogs.com/sjyforg/p/3949029.html

上述结论,暂没有找到足够的资料来支持,但通过一些测试,可以证明“当前同步上下文”(Current SynchronizationContext)会影响await后面的语句将由什么线程来执行。Stephen Cleary的《C#并发编程经典实例》中也有提到相关论述。

 

修改1中的控制台程序,分别在主线程和task内部打印出Current SynchronizationContext

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Console.WriteLine($"主线程的当前同步上下文:{SynchronizationContext.Current}");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
            Console.ReadKey();
        }
        static async void Async()
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            await Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine($"Task子线程的当前同步上下文:{SynchronizationContext.Current}");
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb");
            });
            Console.WriteLine($"await后面语句的当前同步上下文:{SynchronizationContext.Current}");
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

执行结果:

 

可以看出,控制台应用程序输出的Current SynchronizationContext都是为空值,上面提到,“如果一个线程的当前 SynchronizationContext 为 null,那么它隐式具有一个默认 SynchronizationContext”,也就是说上述控制台程序的同步上下文为SynchronizationContext,而它应用于线程池。所以上述控制台程序中,await后面的语句由线程池线程执行。

再看下winform程序中的Current SynchronizationContext

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Console.WriteLine($"主线程的当前同步上下文:{SynchronizationContext.Current}");
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
        }

        static async void Async()
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            Task task = Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine($"task子线程的当前同步上下文:{SynchronizationContext.Current}");
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb");
            });
            await task;
            Console.WriteLine($"await后面语句的当前同步上下文:{SynchronizationContext.Current}");
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

执行结果:

测试发现,winform程序中,await后面的语句的Current SynchronizationContext是WindowsFormsSynchronizationContext ,而WindowsFormsSynchronizationContext 是一个单独的 UI 线程 ,因此上面winform程序中,await后面的语句由UI线程(主线程)执行。

 

我们可以修改上面winform程序中的Current SynchronizationContext,使得await后面的语句变成由线程池线程执行,

在await之前,加入这段代码:

SynchronizationContext.SetSynchronizationContext(null);

使当前同步上下文变为默认值:

public partial class Form1 : Form
    {
        private static SynchronizationContext synchronizationContext;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程开始");
            Async();
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' 主线程结束");
        }

        static async void Async()
        {
            SynchronizationContext.SetSynchronizationContext(null);//使当前同步上下文变为默认值
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' aaa");
            Task task = Task.Run(() =>
            {
                Thread.Sleep(500);
                synchronizationContext = SynchronizationContext.Current;
                Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' bbb");
            });
            
            await task;
            Console.WriteLine($"线程Id: '{Thread.CurrentThread.ManagedThreadId}' ccc");
        }
    }

 执行结果:

对比原先的执行结果:

 可以发现,对于await语句,原本由UI线程(线程id为1)执行,将Current SynchronizationContext改为默认值后,改为由线程池的线程(线程id为3)执行。

以上便是简单的论证。

 

 

 

 

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值