起因
最近有個小夥伴提出了一個問題,就是在使用.net core的BackgroundService的時候,對應的ExecuteAsync方法裡面寫如下程式碼,會使程式一直卡在當前方法,不會繼續執行,程式碼如下:
public class BGService : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true)
{
Thread.Sleep(1000);
}
}
}
其實這個問題我們還是對Task和非同步執行過程理解不夠深入導致的,所以本篇文章筆者就以這個問題來對Task和非同步方法執行過程來做原始碼的探究。
PS:本文只貼出重要的程式碼和註釋,不是其全部的程式碼,讀者多關注下注釋。
解析
Thread.Sleep和Task.Delay的區別
- Thread.Sleep分析
它會掛起當前執行執行緒指定時間(呼叫了系統核心的方法),而這時候當前執行緒是不能做任何其他的事情,只能等待指定時間後再執行。最終執行的程式碼如下圖:
private static void SleepInternal(int millisecondsTimeout)
{
//這是Windows平臺,不同平臺呼叫的方法不一樣
Interop.Kernel32.Sleep((uint)millisecondsTimeout);
}
- Task.Delay分析
它的執行實際上是交給了TimerQueueTimer,也就是定時器佇列(每個程式裡,所有的timer執行都在一個TimerQueueTimer佇列集合裡面),在指定時間後回撥方法,由ThreadPool中的執行緒執行。實際執行程式碼如下圖:
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken)
{
if (millisecondsDelay < -1)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay, ExceptionResource.Task_Delay_InvalidMillisecondsDelay);
}
//開始執行Delay方法
return Delay((uint)millisecondsDelay, cancellationToken);
}
private static Task Delay(uint millisecondsDelay, CancellationToken cancellationToken) =>
cancellationToken.IsCancellationRequested ? FromCanceled(cancellationToken) :
millisecondsDelay == 0 ? CompletedTask :
//它繼承自DelayPromise,只不過加了CancellationTiken
cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, cancellationToken) :
//最終執行這個
new DelayPromise(millisecondsDelay);
internal DelayPromise(uint millisecondsDelay)
{
if (millisecondsDelay != Timeout.UnsignedInfinite)
{
//把任務放到定時佇列裡
_timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
//如果已經完成了,就把這個銷燬掉
if (IsCompleted)
{
_timer.Close();
}
}
}
總結來說:
1.Thread.Sleep會讓當前執行執行緒掛起一段時間,而在掛起的過程中,不能去幹其他的事情,影響執行緒池對執行緒的排程,間接影響系統的併發性。
2.Task.Delay由建立定時佇列訊息,在指定時間之後由執行緒池去處理Callback,而在這指定時間內是由系統去排程的(這裡可能我理解不對),而當前執行執行緒可以繼續幹其他事情。
多執行緒和非同步
Task任務預設情況下是通過執行緒池中的空閒執行緒去執行,除非設定LongRunning才會單獨開啟一個Thread去執行。一般來說多執行緒只是非同步程式設計實現的一種方式,
- 多執行緒
並行的處理一些任務,尤其是多核CPU,充分利用CPU的效能,增加任務的處理效率,如Paraller並行庫等。 - 非同步
IO密集型操作:如Web應用在進行資料庫操作,檔案操作或者呼叫外部介面,發生磁碟IO或者網路IO時,如果非非同步操作,會使當前執行執行緒一直保持等待事件的完成,而不做其他的處理,導致資源被浪費。如果是非同步操作,當前執行執行緒在出發IO操作後,執行緒不需要等待事件的完成再去操作,而可以由執行緒池排程執行其他的請求,那麼當事件完成後,由作業系統硬體去通知,然後再有執行緒池去排程執行緒去執行。所以我們可以發現在執行非同步方法時,await前和await後不一定是相同一個執行緒去執行,可能會切換執行緒(可以對比前後的執行緒Id)。
CPU密集型操作:如進行大量的計算任務,需要CPU一直排程,我們在WinForm或者WPF中可能會有很深的體會。假如我們執行一個很複雜的計算任務,如果是同步的話,使用者得一直等待計算完成,UI才會展示,如果是非同步的話,使用者不用等待計算完成,UI直接就正常顯示和操作,而這部分計算由執行緒池提供的執行緒獨立其執行,而不影響當前執行執行緒的操作。
Async和Await
一般來說我們使用Await和Async是一起使用的,但是它存在其傳播性,它本身實際上是個語法糖,算是隱性的呼叫ContinueWith方法,在執行完成後繼續執行其他任務,接下我們來解析下他是怎麼執行的。我們看下如下程式碼:
public async Task AA() {
await Task.Delay(1000);
Console.WriteLine("執行到我了");
}
實際上上面的程式碼在編譯之後,會形成一個狀態機(只有標識是async的才會被編譯成狀態機的形式),具體程式碼如下(含註釋),
public class C
{
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <AA>d__0 : IAsyncStateMachine //所有的非同步方法都繼承自它
{
//初始值是-1
public int <>1__state;
//非同步任務方法構造器
public AsyncTaskMethodBuilder <>t__builder;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
//在有標識await的地方,會呼叫對應Task的GetAwaiter()方法,但是它還是會以當前執行執行緒去呼叫Task.Delay。
awaiter = Task.Delay(1000).GetAwaiter();
//當await是未完成狀態
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
//重點是這個方法,裡面實際上是執行了ContinueWith,而在Task執行完成之後,又呼叫其MoveNext方法(這時候可能是不同的執行緒去執行的)。
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
//在獲取到值之後,繼續執行await後面的程式碼
Console.WriteLine("執行到我了");
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
}
//AA整個非同步方法被編譯成這樣
[AsyncStateMachine(typeof(<AA>d__0))]
public Task AA()
{
//構建狀態機
<AA>d__0 stateMachine = default(<AA>d__0);
//建立非同步任務方法構造器
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
//執行Start方法
stateMachine.<>t__builder.Start(ref stateMachine);
//返回當前Task
return stateMachine.<>t__builder.Task;
}
}
我們來看AA非同步方法,被編譯成一個完全不同的方法,在
AA方法中stateMachine.<>t__builder.Start(ref stateMachine);我們看一下到底執行了什麼,如下:
public struct AsyncTaskMethodBuilder<TResult>
{
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine =>
AsyncMethodBuilderCore.Start(ref stateMachine);
}
internal static class AsyncMethodBuilderCore
{
[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null) // TStateMachines are generally non-nullable value types, so this check will be elided
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
Thread currentThread = Thread.CurrentThread;
//當前執行緒的執行上下文
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
//當前執行緒的同步上下文
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;
try
{
//這裡當前執行執行緒開始執行狀態機的MoveNext方法
stateMachine.MoveNext();
}
finally
{
//此處省略,主要是防止上下文改變,設定上下文。
}
}
}
在MoveNext方法裡面,我們繼續看,如果當前Task的狀態是未完成的話,那麼會執行一個叫做AwaitUnsafeOnCompleted的方法,我們看如下程式碼:
public struct AsyncTaskMethodBuilder<TResult>
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
internal static void AwaitUnsafeOnCompleted<TAwaiter>(
ref TAwaiter awaiter, IAsyncStateMachineBox box)
where TAwaiter : ICriticalNotifyCompletion
{
//一般來說當前await是TaskAwaiter繼承自ITaskAwaiter,所以會計入這個判斷
if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
{
ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter);
//這個box,裡面包含MoveNext方法。
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true);
}
//省略部分程式碼。。。
}
}
public readonly struct TaskAwaiter : ICriticalNotifyCompletion, ITaskAwaiter
{
internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
{
Debug.Assert(stateMachineBox != null);
//這裡省略了if判斷
else
{
//執行當前TaskContinuationForAwait,也就類似ContinuWith,當前的task的ContinuWith就是執行MoveNext方法
task.UnsafeSetContinuationForAwait(stateMachineBox, continueOnCapturedContext);
}
}
}
總結來說:
1.帶有Async的非同步方法會在編譯之後生成狀態機。
2.當前執行執行緒會一直執行,把對應的MoveNext放到task的Continuation裡面,也就是當作task完成的延續任務(回撥事件)。
3.當前執行緒不是在執行非同步任務的時候切換執行緒,而是一直執行方法內部,直到內部方法執行完成,所以我們在編寫自定義的Task方法時,應該保證該方法能夠進行立即的返回Task,不要執行過多的其他事情。
4.當發生執行緒切換時(也可能不切換),其實是看執行緒池的排程,讓哪個執行緒去執行對應的Callback(MoveNext方法),所以我們有時候在除錯時可以發現在await前和await之後其實可能不是一個執行緒id。
5.其實我們想一下WinForm和WPF的應用使用非同步編寫,其實當前執行執行緒已經返回了Task(非同步方法編譯後,是直接返回Task),也就是說執行完了,所以沒有造成阻塞,而後來UI上的還能顯示對應的元素,是因為任務排程完成,由其他執行緒去執行了這個操作,而這個執行緒保持了執行上下文和同步上下文。
結果
1.從上述解析可以看出,當在BackgroundService中直接在While迴圈裡面寫Thread.Sleep,當前執行執行緒會一直執行這段程式碼,也就是卡到這個while了,具體到編譯後的程式碼就是卡到stateMachine.<>t__builder.Start(ref stateMachine),然後不會再繼續往下執行了。
2.當我們使用async和await之後,並將Thread.Sleep替換為Task.Delay之後,當前方法就被編譯成狀態機,在當前執行緒執行到awaiter = Task.Delay(1000).GetAwaiter()之後,把當前MoveNext新增到這個Task的Continution,然後直接返回了Task,這樣並不會阻塞當前執行緒繼續往下執行,而後面的事情交給執行緒池空閒執行緒去執行。
3.如果我們不使用async和await的話,那麼我們可以啟動一個Task.Run(建議將TaskCreationOptions設定為LongRunning),這樣的話該方法直接返回了Task,也不會阻塞當前執行緒繼續往下執行。
4.對於Thread.Sleep在非同步程式設計中不建議使用,建議使用Task.Delay,這樣執行緒能夠被更有效的利用起來。
以上就是筆者的看法,因為篇幅問題,沒有貼太多的程式碼,有興趣的小夥伴可以去看看原始碼就瞭解了,總結的可能會有一些理解錯誤的地方,還請評論指正。