【C#Task】TaskCreationOptions 枚舉


根據 TaskCreationOptions 的不同,出現了三個分支

  • LongRunning:獨立線程,和線程池無關
  • 包含 PreferFairness時:preferLocal=false,進入全局隊列
  • 不包含 PreferFairness時:preferLocal=ture,進入本地隊列

進入全局隊列的任務能夠公平地被各個線程池中的線程領取執行,也是就是 prefer fairness 這個詞組的字面意思了。

下圖中 Task666 先進入全局隊列,隨后被 Thread1 領走。Thread3 通過 WorkStealing 機制竊取了 Thread2 中的 Task2。

 

 

在使用Task.Factory.StartNew或者Task.Factory.FromAsync方法創建任務時,一些重載方法允許提供TaskCreationOptions來向調度器提示任務的調度方案。這里簡要介紹了AttachedToParent、DenyChildAttach、HideScheduler、LongRunning、PreferFairness五種選項的具體行為。


AttachedToParent

在一個Task中創建另一個Task時,爸爸Task通常不會等待兒子Task結束。

  例如:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
         Console.WriteLine("Outer task executing.");

         var child = Task.Factory.StartNew(() => {
            Console.WriteLine("Nested task starting.");
            Thread.SpinWait(500000);
            Console.WriteLine("Nested task completing.");
         });
      });

      parent.Wait();
      Console.WriteLine("Outer has completed.");
   }
}
// The example produces output like the following:
//        Outer task executing.
//        Nested task starting.
//        Outer has completed.
//        Nested task completing.
View Code

相應地,使用TaskCreationOptions.AttachedToParent創建的兒子Task則帶有以下3個特點(這3個特點也是默認情況下創建的兒子Task不具備的):

  1. 爸爸Task會等待兒子Task結束。
  2. 爸爸Task會捕獲兒子Task的Exception。
  3. 爸爸Task的執行狀態取決於兒子Task的執行狀態。
  4. 子任務和父任務並不一定運行在同一線程上。

例如,上面的代碼使用TaskCreationOptions.AttachedToParent,則會得到以下的輸出:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
            Console.WriteLine("Parent task executing.");
            var child = Task.Factory.StartNew(() => {
                  Console.WriteLine("Attached child starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("Attached child completing.");
            }, TaskCreationOptions.AttachedToParent);
      });
      parent.Wait();
      Console.WriteLine("Parent has completed.");
   }
}
// The example displays the following output:
//       Parent task executing.
//       Attached child starting.
//       Attached child completing.
//       Parent has completed.

 

DenyChildAttach

如果你不希望一個Task的啟動的兒子們Attach到它自己身上,則可以在啟動爸爸Task時為它指定TaskCreationOptions.DenyChildAttach。當通過DenyChildAttach啟動的爸爸Task試圖指定AttachedToParent來啟動兒子Task時,AttachedToParent將會失效。


HideScheduler

當指定TaskCreationOptions.HideScheduler時,創建Task里再創建的兒子Task將使用默認的TaskScheduler,而不是當前的TaskScheduler。這相當於在創建Task時隱藏了自己當前的TaskScheduler。對於本身就是在默認的TaskScheduler里創建的Task,這個選項似乎沒什么用。

using System.Reflection;
Task tasktest = new(() => { Console.WriteLine($"test Task TaskScheduler is  {TaskScheduler.Current}"); });
Task taskParent = new(() => {
    //這邊不能使用Task.Run 因為它已經配置配置好了,用的是線程池任務調度器。
    Task  subtask=new(() =>
    {
//這邊直接使用了 父任務的任務調度器 Console.WriteLine($
"sub Task TaskScheduler is {TaskScheduler.Current}"); }); subtask.Start();//這邊使用的是當前線程,所以繼承了父任務的 任務調度器 Console.WriteLine($"main Task TaskScheduler is {TaskScheduler.Current}"); Task subtask2 = new(() => { //因為隱藏父任務的任務調度器,所以采用了默認的線程池調度器 Console.WriteLine($"sub Task2 TaskScheduler is {TaskScheduler.Current}"); },TaskCreationOptions.HideScheduler);// 隱藏父類的任務調度器 subtask2.Start(); Console.WriteLine($"main Task TaskScheduler is {TaskScheduler.Current}"); }); tasktest.Start(); taskParent.Start(new PerThreadTaskScheduler());//自定義的任務調度器 taskParent.Wait(); Console.WriteLine("all complete"); Console.Read(); /* 輸出: test Task TaskScheduler is System.Threading.Tasks.ThreadPoolTaskScheduler main Task TaskScheduler is PerThreadTaskScheduler sub Task TaskScheduler is PerThreadTaskScheduler main Task TaskScheduler is PerThreadTaskScheduler sub Task2 TaskScheduler is System.Threading.Tasks.ThreadPoolTaskScheduler all complete */

自定義的任務調度器

public class PerThreadTaskScheduler : TaskScheduler
{
    public string Name => "PerThreadTaskScheduler ";
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return null;
    }

    protected override void QueueTask(Task task)
    {
        var thread = new Thread(() =>
        {
            TryExecuteTask(task);
        });

        thread.Start();
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        throw new NotImplementedException();
    }
}
View Code

 

 

 

 


LongRunning

C#啟動的Task都會通過TaskScheduler來安排執行。根據官方文檔的描述:

The default scheduler for the Task Parallel Library and PLINQ uses the .NET Framework thread pool, which is represented by the ThreadPool class, to queue and execute work. The thread pool uses the information that is provided by the Task type to efficiently support the fine-grained parallelism (short-lived units of work) that parallel tasks and queries often represent.

而默認的TaskScheduler采用的是.NET線程池ThreadPool,它主要面向的是細粒度的小任務,其執行時間通常在毫秒級。線程池中的線程數與處理器的內核數有關,如果線程池中沒有空閑的線程,那么后續的Task將會被阻塞。因此,如果事先知道一個Task的執行需要較長的時間,就需要使用TaskCreationOptions.LongRunning枚舉指明。使用TaskCreationOptions.LongRunning創建的任務將會脫離線程池啟動一個單獨的線程來執行。


PreferFairness

任務並行庫實現良好性能的方法之一是通過"工作竊取"。.NET 4 線程池支持工作竊取,以便通過任務並行庫及其默認計划程序進行訪問。這表現為線程池中的每個線程都有自己的工作隊列;當該線程創建任務時,默認情況下,這些任務將排隊到線程的本地隊列中,而不是排隊到對 ThreadPool.QueueUserWorkItem 的調用通常面向的全局隊列中。當線程搜索要執行的工作時,它會從其本地隊列開始,該操作由於改進了緩存局部性,最小化了爭用等,從而實現了一些額外的效率。但是,這種邏輯也會影響公平性。

典型的線程池將具有單個隊列,用於維護要執行的所有工作。當池中的線程准備好處理另一個工作項時,它們將從隊列的頭部取消排隊工作,當新工作到達池中執行時,它將排隊到隊列的尾部。這為工作項之間提供了一定程度的公平性,因為首先到達的工作項更有可能被選中並首先開始執行。

偷工作擾亂了這種公平。池外部的線程可能正在排隊工作,但如果池中的線程也在生成工作,則池生成的工作將優先於其他工作項,具體取決於池中線程(這些線程首先開始使用其本地隊列搜索工作, 僅繼續進入全局隊列,然后繼續到其他線程的隊列(如果本地沒有工作可用)。這種行為通常是預期的,甚至是期望的,因為如果正在執行的工作項正在生成更多工作,則生成的工作通常被視為正在處理的整體操作的一部分,因此它比其他不相關的工作更可取是有道理的。例如,想象一個快速排序操作,其中每個遞歸排序調用都可能導致幾個進一步的遞歸調用;這些調用(在並行實現中可能是單個任務)是全系列排序操作的一部分。

不過,在某些情況下,這種默認行為是不合適的,其中應該在池中的線程生成的特定工作項和其他線程生成的工作項之間保持公平性。對於長鏈的延續,通常就是這種情況,其中生成的工作不被視為當前工作的一部分,而是當前工作的后續工作。在這些情況下,您可能希望以公平的方式將后續工作與系統中的其他工作放在一起。這就是TaskCreationOptions.PreferFairness可以證明有用的地方。

將 Task 調度到默認調度程序時,調度程序將查看任務從中排隊的當前線程是否是具有自己的本地隊列的 ThreadPool 線程。如果不是,則工作項將排隊到全局隊列。如果是,計划程序還將檢查任務的 TaskCreationOptions 值是否包含"首選公平性"標志,默認情況下該標志未打開。如果設置了該標志,即使線程確實有自己的本地隊列,調度程序仍將 Task 排隊到全局隊列,而不是本地隊列。通過這種方式,該任務將與全局排隊的所有其他工作項一起被公平地考慮。

剛才描述的是默認計划程序中優先公平標志的當前實現。實現當然可以更改,但不會更改的是標志的目的:通過指定 PreferFairness,您可以告訴系統不應僅僅因為此任務來自本地隊列而對其進行優先級排序。您是在告訴系統,您希望系統盡最大努力確保此任務以先到先得的方式進行優先級排序。

另一件需要注意的事情是,Task本身對這面旗幟一無所知。它只是一個標志,設置為任務上的一個選項。調度程序決定了它想要如何處理這個特定的選項,就像TaskCreationOptions.LongRunning一樣。默認調度程序按上述方式處理它,但另一個調度程序(例如您編寫的調度程序)可以根據需要使用此標志,包括忽略它。因此,命名"首選"而不是像"保證"這樣更嚴格的東西。

 

轉載自:TaskCreationOptions.PreferFairness - .NET Parallel Programming (microsoft.com)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM