共用方式為


外部工作和粒紋

根據設計,從粒紋程式碼繁衍的任何子工作 (例如,使用 awaitContinueWithTask.Factory.StartNew) 會分派在與父工作相同的每個啟用 TaskScheduler 上,因此會繼承與其餘粒紋程式碼相同的單一執行緒執行模型。 這是粒紋輪流並行的單一執行緒執行背後的主要觀點。

在某些情況下,粒紋程式碼可能需要「中斷」Orleans 工作排程模型和「執行特殊動作」,例如將 Task 明確地指向不同的工作排程器或 .NET ThreadPool。 這類案例的範例是,當粒紋程式碼必須執行同步遠端封鎖呼叫時 (例如遠端 IO)。 在粒紋內容中執行封鎖呼叫將會封鎖粒紋,因此不應該進行這類操作。 相反地,粒紋程式碼可在執行緒集區執行緒上執行此封鎖程式碼片段,並聯結 (await) 該執行完成,並在粒紋內容中繼續進行。 我們預期從 Orleans 排程器逸出是非常進階且很少需要進行的使用情節,超出「一般」使用模式。

以工作為基礎的 API

  1. awaitTaskFactory.StartNew (請參閱下列)、Task.ContinueWithTask.WhenAnyTask.WhenAllTask.Delay 全都遵守目前的工作排程器。 這表示不傳遞不同的 TaskScheduler,透過預設的使用方式會導致在粒紋內容中的執行。

  2. Task.RunTaskFactory.FromAsyncendMethod 委派兩者都不會遵守目前的工作排程器。 兩者都使用 TaskScheduler.Default 排程器,也就是 .NET 執行緒集區工作排程器。 因此,在 Task.Run 中的程式碼和 Task.Factory.FromAsync 中的 endMethod 將一律會在單一執行緒執行模型以外的 .NET 執行緒集區上執行,以取得 Orleans 粒紋。 不過,在建立工作時,await Task.Runawait Task.Factory.FromAsync 之後的任何程式碼都會在排程器 (也就是粒紋排程器) 下執行。

  3. 具有 falseTask.ConfigureAwait 是用來逸出目前工作排程器的明確 API。 其會導致等候工作後的程式碼在 TaskScheduler.Default 排程器 (也就是 .NET 執行緒集區) 上執行,因此會中斷粒紋的單一執行緒執行。

    警告

    您一般不應該直接在粒紋程式碼中使用 ConfigureAwait(false)

  4. 具有簽章 async void 的方法不應與粒紋搭配使用。 其適用於圖形化使用者介面事件處理常式。 async void 方法如果允許例外狀況逸出,則可能使目前的流程立即損毀,而無法處理例外狀況。 這也適用於 List<T>.ForEach(async element => ...) 和任何接受 Action<T> 的其他方法,因為非同步委派會強制轉換成 async void 委派。

Task.Factory.StartNewasync 委派

在任何 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.CurrentTask.Run 和特殊的自訂排程器,以從 Orleans 粒紋內容逸出,以及如何回到其中。

public async Task MyGrainMethod()
{
    // Grab the grain's task scheduler
    var orleansTS = TaskScheduler.Current;
    await TaskDelay(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 輪流並行保證,從粒紋程式碼執行非同步背景工作 (請參閱上文)。 Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap)
使用 Orleans 輪流並行保證,從粒紋程式碼執行同步背景工作。 Task.Factory.StartNew(WorkerSync)
執行工作項目的逾時 Task.Delay + Task.WhenAny
呼叫非同步程式庫方法 await 程式庫呼叫
使用 async/await 一般 .NET 工作非同步程式設計模型。 建議與支援
ConfigureAwait(false) 請勿在粒紋程式碼內使用。 只允許在程式庫內。