外部工作和粒紋
根據設計,從粒紋程式碼繁衍的任何子工作 (例如,使用 await
或 ContinueWith
或 Task.Factory.StartNew
) 會分派在與父工作相同的每個啟用 TaskScheduler 上,因此會繼承與其餘粒紋程式碼相同的單一執行緒執行模型。 這是粒紋輪流並行的單一執行緒執行背後的主要觀點。
在某些情況下,粒紋程式碼可能需要「中斷」Orleans 工作排程模型和「執行特殊動作」,例如將 Task
明確地指向不同的工作排程器或 .NET ThreadPool。 這類案例的範例是,當粒紋程式碼必須執行同步遠端封鎖呼叫時 (例如遠端 IO)。 在粒紋內容中執行封鎖呼叫將會封鎖粒紋,因此不應該進行這類操作。 相反地,粒紋程式碼可在執行緒集區執行緒上執行此封鎖程式碼片段,並聯結 (await
) 該執行完成,並在粒紋內容中繼續進行。 我們預期從 Orleans 排程器逸出是非常進階且很少需要進行的使用情節,超出「一般」使用模式。
以工作為基礎的 API
await、TaskFactory.StartNew (請參閱下列)、Task.ContinueWithTask.WhenAny、Task.WhenAll、Task.Delay 全都遵守目前的工作排程器。 這表示不傳遞不同的 TaskScheduler,透過預設的使用方式會導致在粒紋內容中的執行。
Task.Run 和 TaskFactory.FromAsync 的
endMethod
委派兩者都不會遵守目前的工作排程器。 兩者都使用TaskScheduler.Default
排程器,也就是 .NET 執行緒集區工作排程器。 因此,在Task.Run
中的程式碼和Task.Factory.FromAsync
中的endMethod
將一律會在單一執行緒執行模型以外的 .NET 執行緒集區上執行,以取得 Orleans 粒紋。 不過,在建立工作時,await Task.Run
或await Task.Factory.FromAsync
之後的任何程式碼都會在排程器 (也就是粒紋排程器) 下執行。具有
false
的 Task.ConfigureAwait 是用來逸出目前工作排程器的明確 API。 其會導致等候工作後的程式碼在 TaskScheduler.Default 排程器 (也就是 .NET 執行緒集區) 上執行,因此會中斷粒紋的單一執行緒執行。警告
您一般不應該直接在粒紋程式碼中使用
ConfigureAwait(false)
。具有簽章
async void
的方法不應與粒紋搭配使用。 其適用於圖形化使用者介面事件處理常式。async void
方法如果允許例外狀況逸出,則可能使目前的流程立即損毀,而無法處理例外狀況。 這也適用於List<T>.ForEach(async element => ...)
和任何接受 Action<T> 的其他方法,因為非同步委派會強制轉換成async void
委派。
Task.Factory.StartNew
和 async
委派
在任何 C# 程式中排程工作的一般建議是使用 Task.Run
,而不是 Task.Factory.StartNew
。 使用 Task.Factory.StartNew
的快速 Google 搜尋會建議這個情況很危險,而且應一律優先使用 Task.Run
。 但是,如果我們想要停留在粒紋的單一執行緒執行模型以取得粒紋,則我們需要加以使用,那我們該如何正確地執行? 使用 Task.Factory.StartNew()
時,危險在於其原本不支援非同步委派。 這表示這可能是錯誤:var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)
。 notIntendedTask
不是在 SomeDelegateAsync
執行時完成的工作。 相反地,應一律取消包裝傳回的工作:var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()
。
多個工作和工作排程器範例
以下是範例程式碼,示範如何使用 TaskScheduler.Current
、Task.Run
和特殊的自訂排程器,以從 Orleans 粒紋內容逸出,以及如何回到其中。
public async Task MyGrainMethod()
{
// Grab the grain's task scheduler
var orleansTS = TaskScheduler.Current;
await Task.Delay(10_000);
// Current task scheduler did not change, the code after await is still running
// in the same task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
Task t1 = Task.Run(() =>
{
// This code runs on the thread pool scheduler, not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
});
await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in Orleans task scheduler context, we are now back
// to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
// Example of using Task.Factory.StartNew with a custom scheduler to escape from
// the Orleans scheduler
Task t2 = Task.Factory.StartNew(() =>
{
// This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
// the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
},
CancellationToken.None,
TaskCreationOptions.None,
scheduler: MyCustomSchedulerThatIWroteMyself);
await t2;
// We are back to Orleans task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
從執行緒集區上執行的程式碼發出粒紋呼叫的範例
另一個情節是一段粒紋程式碼,需要「中斷」粒紋的工作排程模型,並在執行緒集區 (或其他非粒紋內容) 上執行,但仍需要呼叫另一個粒紋。 可以從非粒紋內容進行粒紋呼叫,而不需要額外的規範。
下列程式碼示範如何從一段在粒紋內執行,但不是在粒紋內容中執行的程式碼中執行粒紋呼叫。
public async Task MyGrainMethod()
{
// Grab the Orleans task scheduler
var orleansTS = TaskScheduler.Current;
var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
Task<int> t1 = Task.Run(async () =>
{
// This code runs on the thread pool scheduler,
// not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
int res = await fooGrain.MakeGrainCall();
// This code continues on the thread pool scheduler,
// not on the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
return res;
});
int result = await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in the Orleans task scheduler context,
// we are now back to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
使用程式庫
程式碼正在使用的某些外部程式庫可能會在內部使用 ConfigureAwait(false)
。 實作一般用途程式庫時,最好在 .NET 中使用 ConfigureAwait(false)
。 這不是 Orleans 中的問題。 只要叫用程式庫方法之粒紋中的程式碼正在等候使用一般 await
的程式庫呼叫,則粒紋程式碼是正確的。 結果會完全符合需求 – 程式庫程式碼會在預設排程器上執行接續 (TaskScheduler.Default
傳回的值,這不保證接續會在 ThreadPool 執行緒上執行,因為接續通常會內嵌在先前的執行緒中),而粒紋程式碼則會在粒紋排程器上執行。
另一個常見問題是是否需要使用 Task.Run
執行程式庫呼叫,也就是是否需要明確地將程式庫程式碼卸載至 ThreadPool
(讓粒紋程式碼進行 Task.Run(() => myLibrary.FooAsync())
)。 答案是不可能。 除了進行封鎖同步呼叫的程式庫程式碼之外,不需要將任何程式碼卸載至 ThreadPool
。 通常,任何妥善撰寫且正確的 .NET 非同步程式庫 (傳回 Task
並以 Async
尾碼命名的方法) 不會進行封鎖呼叫。 因此,除非您懷疑非同步程式庫有錯,或刻意使用同步封鎖程式庫,否則不需要將任何項目卸載至 ThreadPool
。
死結
由於粒紋會以單一執行緒的方式執行,因此可能以需要多執行緒解除封鎖的方式,藉由同步封鎖將粒紋死結。 這表示如果在叫用該方法或屬性時尚未完成提供的工作,呼叫下列任何方法和屬性的程式碼可能會使粒紋死結:
Task.Wait()
Task.Result
Task.WaitAny(...)
Task.WaitAll(...)
task.GetAwaiter().GetResult()
在任何高度並行服務中,都應該避免這些方法,因為它們會藉由封鎖可能執行有用工作的執行緒,並要求 .NET ThreadPool
插入其他執行緒使其能夠完成,使 .NET ThreadPool
資源不足,因而導致效能不佳和不穩定。 如上所述,執行粒紋程式碼時,這些方法可能會造成粒紋死結,因此也應該在粒紋程式碼中避免使用。
如果有一些無法避免的同步與非同步處理工作,最好將該工作移至不同的排程器。 例如,這樣做的最簡單方式是使用 await Task.Run(() => task.Wait())
。 請注意,強烈建議您避免同步與非同步處理工作,因為如上所述,這會導致應用程式的可擴縮性和效能受到影響。
在 Orleans 中使用工作的摘要
嘗試執行什麼動作? | 如何執行此動作 |
---|---|
在 .NET 執行緒集區執行緒上執行背景工作。 不允許任何粒紋程式碼或粒紋呼叫。 | Task.Run |
使用 Orleans 輪流並行保證,從粒紋程式碼執行非同步背景工作 (請參閱上文)。 | |
使用 Orleans 輪流並行保證,從粒紋程式碼執行同步背景工作。 | Task.Factory.StartNew(WorkerSync) |
執行工作項目的逾時 | Task.Delay + Task.WhenAny |
呼叫非同步程式庫方法 | await 程式庫呼叫 |
使用 async /await |
一般 .NET 工作非同步程式設計模型。 建議與支援 |
ConfigureAwait(false) |
請勿在粒紋程式碼內使用。 只允許在程式庫內。 |