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)执行。
以上便是简单的论证。