C# 創建線程的多種方式之 線程池和任務 基礎知識


1. 線程池

創建,釋放線程都需要消耗很多時間,所以如果有許多的用時較短的小任務需要同時完成且不需要過多的控制,則可以選擇線程池來實現,即ThreadPool類.  

對於線程所執行的任務來說,可以把線程分為兩種類型:工作者線程和I/O線程。工作者線程用來完成一些計算的任務,在任務執行的過程中,需要CPU不間斷地處理,所以,在工作者線程的執行過程中,CPU和線程的資源是充分利用的。對於I/O線程,在.Net中通過以Begin開頭的方法來完成啟動,以End開頭的方法來處理結果。

SetMaxThreads() :設置最大工作線程,雙核系統默認設置工作線程數1023,IO線程數1000。若線程池中的數目達到最大,那么最新工作將排隊等待;

SetMinThreads():  設置最小工作線程,即線程池中至少會准備多少個線程備用,一旦超過此數目,線程池便會擴充(花時間);

在實際應用中需要根據線程的並發數量合理設置最大,最小線程數,這樣才能合理優化資源。如果最小線程數過底,則線程池便會不斷創建,銷毀線程,浪費資源,如果設的過高,就會造成很多線程空閑等待。

GetAvailableThreads():獲取當前可用線程數量,即最大線程數-正在使用的線程數;

QueueUserWorkItem():調用方法,無法取消;

            Console.WriteLine("Main Start....");
            ThreadPool.SetMaxThreads(100,100);       //設置最大線程數
            ThreadPool.SetMinThreads(5, 10);       //設置最小線程數
            int workThread, IOThread;
            ThreadPool.GetMaxThreads(out workThread, out IOThread);       //獲取最大線程數
            Console.WriteLine("total is {0}, IOThread is {1}", workThread, IOThread);
            ThreadPool.GetMinThreads(out workThread, out IOThread);       //獲取最小線程數
            Console.WriteLine("total is {0}, IOThread is {1}", workThread, IOThread);
            ThreadPool.QueueUserWorkItem(Calculate, 30);          //調用方法
            ThreadPool.GetAvailableThreads(out workThread, out IOThread);     //讀取當前可用線程
            Console.WriteLine("total is {0}, IOThread is {1}", workThread, IOThread);
            ThreadPool.QueueUserWorkItem(Calculate, 30);          //調用方法
            ThreadPool.GetAvailableThreads(out workThread, out IOThread);     //讀取當前可用線程
            Console.WriteLine("total is {0}, IOThread is {1}", workThread, IOThread);
            ThreadPool.QueueUserWorkItem(Calculate, 30);          //調用方法
            ThreadPool.GetAvailableThreads(out workThread, out IOThread);     //讀取當前可用線程
            Console.WriteLine("total is {0}, IOThread is {1}", workThread, IOThread);
            Console.ReadLine();

運行結果:

Main Start....
total is 100, IOThread is 100
total is 5, IOThread is 10
total is 98, IOThread is 100
total is 97, IOThread is 100
total is 95, IOThread is 100
Sum is 435
Sum is 435
Sum is 435

 2. 任務

任務表示應完成的某個單元的工作,這個單元的工作可以在單獨的線程中完成,也可同步完成(調用RunSynchronously())。

創建啟動任務有2種方式:Task類,TaskFactory類(可以Task.Factory獲取實例),兩種方式均可接受Action, Action<Object>,Func<Object,Tresult>類型的函數

 

        static void Main(string[] args)
        {
            
            Console.WriteLine("Main Start....");
            Task<int> t1 = new Task<int>(Calculate, 20);
            t1.Start();
            while(!t1.IsCompleted){
                Console.WriteLine("Waiting Result....");
                Thread.Sleep(500);
            }
            Console.WriteLine("Result is " + t1.Result);
            Console.ReadLine();
        }

        private static int Calculate(object obj)
        {
            int sum = 0;
            int total = (int)obj;
            for (int i = 0; i < total;i++ )
            {
                sum += i;
                Thread.Sleep(100);
            }
            return sum;
        }

 

IsComplete屬性表示任務是否完成,Result屬性返回結果,與委托的異步調用類似。接着使用TaskFactory

       static void Main(string[] args)
        {
            
            Console.WriteLine("Main Start....");
            Task<int> t1 = Task<int>.Factory.StartNew(obj =>
            {
                int sum = 0;
                int total = (int)obj;
                for (int i = 0; i < total; i++)
                {
                    sum += i;
                    Thread.Sleep(100);
                }
                return sum;
            }, 20);
            while(!t1.IsCompleted){
                Console.WriteLine("Waiting result....");
                Thread.Sleep(500);
            }
            Console.WriteLine("Result is " + t1.Result);
            Console.ReadLine();
        }

 

在Task類的重載構造函數中,還有可以傳遞CancellationToken結構參數,TaskCreationOptions枚舉參數。

 CancellationToken 用來傳播有關應取消操作的通知,通過實例化創建取消標記CancellationTokenSource對象,該管理取消令牌對象中檢索其CancellationTokenSource.Token屬性獲取,下面的例子參考了MSDN,

        static void Main(string[] args)
        {
            CancellationTokenSource cs = new CancellationTokenSource();     //創建CancellationTokenSource實例
            Console.WriteLine("Main Start....");
            CancellationToken token = cs.Token;                            //獲取CancellationTokenSource.Token屬性
            List<Task<int>> tasks = new List<Task<int>>();
            int num = 5;                                           //任務數量
            for(int y=0;y<num;y++){
                tasks.Add(Task<int>.Factory.StartNew((obj) =>      //創建並啟動任務
                {
                    int sum = 0;
                    int total = (int)obj;
                    for (int i = 0; i < total; i++)
                    {
                        sum += i;
                        Thread.Sleep(100);
                        if (sum > 200)
                        {
                            if (!cs.IsCancellationRequested)    // 判斷CancellationTokenSource是否已經調用Cancel方法,調用一次Cancel后就無需調用
                            {
                                cs.Cancel(false);             //當sum>200, 調用 CancellationTokenSource.Cancel 方法以提供取消通知
                                Console.WriteLine("Task" + total / 5 + " has been cancelled when sum is " + sum);
                                break;
                            }
                        }
                    }                   
                    return sum;
                }, y*5, token));                     //將 CancellationTokenSource.Token 屬性返回的標記傳遞給每個偵聽取消的任務或線程                   
            }
            try
            {
                Task<double> task2 = Task.Factory.ContinueWhenAll<int, double>(tasks.ToArray(), (arrTask) =>         //創建一個延續任務,該任務在指定的任務完成后開始
                {
                    double total = 0;
                    foreach(Task<int> item in arrTask){
                        total += item.Result;
                    }
                    return total / num;
                }, token);                            //將 CancellationTokenSource.Token 屬性返回的標記傳遞給每個偵聽取消的任務或線程 
                Console.WriteLine("the Average value is "+task2.Result);
            }
            catch (AggregateException ae)
            {
                foreach (Exception e in ae.InnerExceptions)
                {
                    if (e is TaskCanceledException)
                        Console.WriteLine("Cancel err:"+((TaskCanceledException)e).Message);
                    else
                        Console.WriteLine("Exception: " + e.GetType().Name);
                }
            }
            finally
            {
                 cs.Dispose();                         //調用Dispose方法在使用完CancellationTokenSource對象
                 Console.WriteLine("CancellationTokenSource has been disposed...");
            }
            Console.ReadLine();
        }

當num=5時,運行結果如下:

Main Start....
the Average value is 70
CancellationTokenSource has been disposed...

此時沒有調用Cancel,若num=8,運行結果如下:

Main Start....
Task5 has been cancelled when sum is 210
Cancel err:A task was canceled.
CancellationTokenSource has been disposed...

在運行第五個任務的時候,sum>200,調用Cancel(),ContinueWhenAll() 出現 AggregateException,並在其InnerExceptions中捕捉到TaskCanceledException。其實運行第6,第7和第8個任務,sum都會大於200,所以采用IsCancellationRequested屬性判斷是否已經取消。

 TaskCreationOptions 指定可控制任務的創建和執行的可選行為的標志。枚舉各字段的含義如下:

AttachedToParent 4

指定將任務附加到任務層次結構中的某個父級。 默認情況下,子任務(即由外部任務創建的內部任務)將獨立於其父任務執行。 可以使用 AttachedToParent 選項以便將父任務和子任務同步。

請注意,如果使用 DenyChildAttach 選項配置父任務,則子任務中的 AttachedToParent 選項不起作用,並且子任務將作為分離的子任務執行。

有關詳細信息,請參閱附加和分離的子任務

DenyChildAttach 8

指定任何嘗試作為附加的子任務執行(即,使用 AttachedToParent 選項創建)的子任務都無法附加到父任務,會改成作為分離的子任務執行。 有關詳細信息,請參閱附加和分離的子任務

HideScheduler 16

防止環境計划程序被視為已創建任務的當前計划程序。 這意味着像 StartNew 或 ContinueWith 創建任務的執行操作將被視為 Default 當前計划程序。

LongRunning 2

指定任務將是長時間運行的、粗粒度的操作,涉及比細化的系統更少、更大的組件。 它會向 TaskScheduler 提示,過度訂閱可能是合理的。 可以通過過度訂閱創建比可用硬件線程數更多的線程。 它還將提示任務計划程序:該任務需要附加線程,以使任務不阻塞本地線程池隊列中其他線程或工作項的向前推動。

None 0

指定應使用默認行為。

PreferFairness 1

提示 TaskScheduler 以一種盡可能公平的方式安排任務,這意味着較早安排的任務將更可能較早運行,而較晚安排運行的任務將更可能較晚運行。

RunContinuationsAsynchronously 64

強制異步執行添加到當前任務的延續任務。

請注意,RunContinuationsAsynchronously 成員在以 .NET Framework 4.6 開頭的 TaskCreationOptions 枚舉中可用。

 
連續的任務:可以指定在某個任務完成后,開始運行另外一個特定任務。ContinueWith()創建連續特定任務,可以傳遞需等待完成的任務和TaskContinuationOptions枚舉參數。改枚舉與TaskCreationOptions類似,除了上述字段還增加了一些字段:
NotOnCanceled 262144

指定不應在延續任務前面的任務已取消的情況下安排延續任務。 如果前面任務完成的 Status 屬性是 Canceled,則前面的任務會取消。 此選項對多任務延續無效。

NotOnFaulted 131072

指定不應在延續任務前面的任務引發了未處理異常的情況下安排延續任務。 如果前面任務完成的 Status 屬性是 Faulted,則前面的任務會引發未處理的異常。 此選項對多任務延續無效。

NotOnRanToCompletion 65536

指定不應在延續任務前面的任務已完成運行的情況下安排延續任務。 如果前面任務完成的 Status 屬性是 RanToCompletion,則前面的任務會運行直至完成。 此選項對多任務延續無效。

OnlyOnCanceled 196608

指定只應在延續前面的任務已取消的情況下安排延續任務。 如果前面任務完成的 Status 屬性是 Canceled,則前面的任務會取消。 此選項對多任務延續無效。

OnlyOnFaulted 327680

指定只有在延續任務前面的任務引發了未處理異常的情況下才應安排延續任務。 如果前面任務完成的 Status 屬性是 Faulted,則前面的任務會引發未處理的異常。

OnlyOnFaulted 選項可保證前面任務中的 Exception 屬性不是 null你可以使用該屬性來捕獲異常,並確定導致任務出錯的異常。 如果你不訪問 Exception 屬性,則不會處理異常。 此外,如果嘗試訪問已取消或出錯的任務的 Result 屬性,則會引發一個新異常。

此選項對多任務延續無效。

OnlyOnRanToCompletion 393216

指定只應在延續任務前面的任務已完成運行的情況下才安排延續任務。 如果前面任務完成的 Status 屬性是 RanToCompletion,則前面的任務會運行直至完成。 此選項對多任務延續無效。

 寫個下例子:

        static void Main(string[] args)
        {
            Console.WriteLine("Main Start....");
            Task<int> t1 = new Task<int>(Calculate, 10);
            Task<int> t2 = t1.ContinueWith<int>(t =>
            {
                Console.WriteLine("t1 finish, t2 start....");
                return t.Result;
            }, TaskContinuationOptions.NotOnFaulted);     //在沒有拋出異常的情況下
            Task t3 = t1.ContinueWith(t =>
            {
                Console.WriteLine("t1 err, t3 start....");
            }, TaskContinuationOptions.OnlyOnFaulted);   //在拋出異常的情況下
            t1.Start();
            Console.WriteLine("t2's result is "+t2.Result);
            Console.ReadLine();
        }

        private static int Calculate(object obj)
        {
            int sum = 0;
            int total = (int)obj;
            for (int i = 0; i < total; i++)
            {
                sum += i;
                Thread.Sleep(100);
            }
            if (sum > 50)
            {
                throw new Exception();
            }
            return sum;
        }

運行結果:

Main Start....
t1 finish, t2 start....
t2's result is 45

但如果t1 = new Task<int>(Calculate, 15),讓t1運行拋出異常,同時要注釋Console.WriteLine("t2's result is "+t2.Result); 運行后,Studio會彈出異常框,點擊Continue, 結果如下:

Main Start....
t1 err, t3 start....

t1 運行后,運行t3 ,而不t2。

任務層次結構:當從一個任務里啟動另一個任務時,就構成了父/子層次結構。

 

        static void Main(string[] args)
        {
            Console.WriteLine("Main Start....");
            Task parentTask = new Task(() =>
            {
                Console.WriteLine("parent task start....");
                Task childTask = new Task(() =>
                {
                    Console.WriteLine("child task start....");
                    Thread.Sleep(5000);
                    Console.WriteLine("child task end....");
                },TaskCreationOptions.AttachedToParent);     //父任務和子任務同步
                childTask.Start();
                Console.WriteLine("parent task end....");
            },TaskCreationOptions.None);
            parentTask.Start();
            Thread.Sleep(1000);
            Console.WriteLine(parentTask.Status);      //輸出父任務的狀態
            Console.ReadLine();
        }

 

運行結果:

Main Start....
parent task start....
parent task end....
child task start....
WaitingForChildrenToComplete
child task end....

從運行結果可以看出,父任務雖運行到最后,但其狀態仍然是WaitingForChildrenToComplete,這是因為父任務中創建子任務時,傳遞了TaskCreationOptions.AttachedToParent參數,默認父任務與子任務是相互獨立的。如果創建父任務時傳遞了TaskCreationOptions.DenyChildAttach參數,則父任務中創建的子任務均為獨立的,TaskCreationOptions.AttachedToParent參數無效。修改上面例子,父任務創建時傳遞TaskCreationOptions.DenyChildAttach,運行結果:

Main Start....
parent task start....
parent task end....
child task start....
RanToCompletion
child task end....

 


免責聲明!

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



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