.NET 實現並行的幾種方式(一)


好久沒有更新了,今天來一篇,算是《同步與異步》系列的開篇吧,加油,堅持下去(PS:越來越懶了)。

 

一、Thread 

利用Thread 可以直接創建和控制線程,在我的認知里它是最古老的技術了。因為out了、所以不再寫例子了。

二、ThreadPool

由於線程的創建和銷毀需要耗費大量的資源,為了提過性能、引入了線程池、即ThreadPool,ThreadPool 可隱式完成線程的創建和分配管理工作。

以下是來自MSDN的幾句備注:

  線程池根據需要提供新的工作線程或 I/O 完成線程,直到其達到每個類別的最小值。 當達到最小值時,線程池可以在該類別中創建更多線程或等待某些任務完成。 從 .NET Framework 4 開始,線程池會創建和銷毀工作線程以優化吞吐量,吞吐量定義為單位時間內完成的任務數。 線程過少時可能無法更好地利用可用資源,但線程過多時又可能會加劇資源的爭用情況。

 直接應用 ThreadPool時、有兩種應用場景:

1、將需要異步執行的方法、排入隊列,當有可用線程時執行被排入隊列的方法

            // 該應用場景下 ThreadPool 提供如下兩種形式的重載方法
            // public static bool QueueUserWorkItem(WaitCallback callBack);
            // public static bool QueueUserWorkItem(WaitCallback callBack, object state);

            // WaitCallback 為 delegate, 一個object類型的入參,沒有返回值。
            // public delegate void WaitCallback(object state);


            base.SetTip(nameof(ThreadPool.QueueUserWorkItem));

            ThreadPool.QueueUserWorkItem((state) =>
            {
                this.SetTip("等待一秒");
                Thread.Sleep(1000);
                this.SetTip("任務執行完畢");
            });

 

2、注冊等待信號對象(WaitHandle)、並在其收到信號或超時時觸發回調函數

            // 該應用場景下 ThreadPool 提供了四種形式的重載方法, 下面的重載形式在我看來是最具有直觀意義的
            // public static RegisteredWaitHandle RegisterWaitForSingleObject(
            //      WaitHandle waitObject, WaitOrTimerCallback callBack, object state, long millisecondsTimeOutInterval, bool executeOnlyOnce);

            // 其中 WaitHandle 為需要等待信號的類型 的 抽象基類,在后續隨筆中會做詳細介紹,在此不再多言。

            // 其中 WaitOrTimerCallback 為 回調函數委托
            // public delegate void WaitOrTimerCallback(object state, bool timedOut);

            // state 參數為 回調函數的傳入參數

            // millisecondsTimeOutInterval 參數為 計時器超時周期, -1 為永不超時、即一直等待。
            // 對於此參數、第一次接觸到的人可能有疑問、怎么還有周期? 
            // 因為 信號可以重復接到多次、所以當每次接到信號后、或者超時后計時器都會重新計時, 所以有了周期的含義。

            // executeOnlyOnce, True 表示只執行一次, False 會一直等到該信號對象被取消注冊,否則 只要接到信號或者超時就會觸發回調函數。

            base.SetTip(nameof(ThreadPool.RegisterWaitForSingleObject));

            ThreadPool.RegisterWaitForSingleObject(this.waitObject, (state, timeout) =>
            {
                this.SetTip("++++++等待對象收到信號++++++");

            }, null, -1, false);


            ThreadPool.QueueUserWorkItem((state) =>
            {
                this.SetTip("等待一秒");
                Thread.Sleep(1000);

                this.SetTip("等待對象發出信號");
                this.waitObject.Set();

                this.SetTip("等待5秒");
                Thread.Sleep(5000);

                this.SetTip("等待對象發出信號");
                this.waitObject.Set();

            });

 

三、Delegate.BeginInvoke

Delegate.BeginInvoke 間接的調用了線程池、從線程池中獲取一個可用線程、執行當前委托所指向的函數指針所代表的方法。

        [Tag("Delegate.BeginInvoke")]
        private void Demo3()
        {
            this.txtTip.SetTip("UI, Id:" + Thread.CurrentThread.ManagedThreadId);

            Action action = new Action(this.DelegateTest);
            action.BeginInvoke(null, null);
            action.BeginInvoke(null, null);            
            
        }

        private void DelegateTest()
        {
            int id = Thread.CurrentThread.ManagedThreadId;

            this.Dispatcher.Invoke(() =>
            {
                this.txtTip.SetTip("BeginInvoke, Id:" + id);
            });
        }


通過 Thread.CurrentThread.ManagedThreadId 、我們也可以間接的證明 BeginInvoke 確實時在不同的線程中執行的。

當然,與之對應的是一系列的 Begin...  End...  方法對, 它們都是 IAsyncResult 系列的異步編程模型中的一份子。


在 IAsyncResult  系列的異步編程模型中,傳遞參數 和 獲取返回值的Demo 見下:

        [Tag("Delegate.BeginInvoke帶有參數和返回值")]
        private void Demo4()
        {
            Func<string, string> func = new Func<string, string>(this.DelegateTest2);

            this.txtTip.SetTip(" 輸入參數 123 ");

            func.BeginInvoke(" 123 ", new AsyncCallback((System.IAsyncResult result) =>
            {
                Func<string, string> tmp = result.AsyncState as Func<string, string>;
                if (tmp != null)
                {
                    string returnResult = tmp.EndInvoke(result);

                    this.Dispatcher.Invoke(() =>
                    {
                        this.txtTip.SetTip(" 函數回調 結果 : " + returnResult);
                    });
                }
            }), func);

        }
        private string DelegateTest2(string args)
        {
            return args + " : Return args";
        }

 

四、Task  

Task是在 .NET 4.0 中新增的,是基於任務的異步模型。它是對線程池的再次封裝、使得可以更加方便的創建多種任務組合,如 順序性的延續任務、父子任務。也可以方便的粗粒度地控制任務優先級。

1)Task.NET 4.0 中提倡的方式 

Task 對外公開了構造函數、但是微軟並不建議直接使用Task構造函數去實例化對象,而是 使用 Task.Factory.StartNew();

MSDN 中的備注如下:
For performance reasons, TaskFactory's StartNew method should be the preferred mechanism for creating and scheduling computational tasks,
but for scenarios where creation and scheduling must be separated, the constructors may be used,
and the task's Start method may then be used to schedule the task for execution at a later time.
Task 類還提供了初始化任務但不計划執行任務的構造函數。 出於性能方面的考慮,TaskFactory 的 StartNew 方法應該是創建和計划計算任務的首選機制,
但是對於創建和計划必須分開的情況,可以使用構造函數,然后可以使用任務的 Start 方法計划任務在稍后執行。

            Task.Factory.StartNew(() =>
            {
                base.SetTip("Task.Factory.StartNew(一個參數)");
            }).ContinueWith((t) =>
            {
                base.SetTip(t.Id.ToString());
                base.SetTip(t.CreationOptions.ToString());
            }).ContinueWith((t) =>
            {
                base.SetTip(t.Id.ToString());
                base.SetTip(t.CreationOptions.ToString());
            });

Task.Factory.StartNew 提供了高達16個的重載函數。
其中 Task.Factory.StartNew<TTask> 是創建帶有返回值的異步任務。

以最復雜的重載為例、逐一介紹其參數
public Task<TResult> StartNew<TResult>(Func<object, TResult> function, object state, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler);

function : 回調函數、我想沒有必要做解釋吧。
state : 回調函數的傳入參數
CancellationToken : 用以取消Task (后續隨筆會做詳細介紹)
TaskCreationOptions : 指定可控制任務的創建和執行的可選行為的標志(后續隨筆會做詳細介紹)
TaskScheduler : 一個實例 TaskScheduler 類表示一個任務計划程序. 該值一般都是使用 TaskScheduler.Default

也就是說:
Task.Factory.StartNew(()=> { });

Task.Factory.StartNew(()=> { }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
兩種方式效果是一致的。

如果你想更精細化的控制任務、可以使用其他重載方法、傳遞不同參數以達到預想的目的。

 

2)Task初始化任務但並不計划執行

前文說過 Task 提供了構造函數、它提供了初始化任務,但並不去計划執行的方式。
讓我們再看一下 Task 得構造函數吧,還是以最復雜的為例:
public Task(Func<object, TResult> function, object state, CancellationToken cancellationToken, TaskCreationOptions creationOptions);
其參數和 Task.Factory.StartNew 相比、 少了TaskScheduler。在性能方面MSDN提示后者會更好。

            base.SetTip("初始化任務");
            Task task = new Task(() =>
            {
                base.SetTip("被計划的任務開始執行");

                base.SetTip("任務休眠5秒");
                Thread.Sleep(5000);

                base.SetTip("任務執行完畢");
            });

            // 為了保證能實時更新UI、看到代碼執行效果、故而將代碼異步執行
            Task.Factory.StartNew(() =>
            {
                base.SetTip("休眠兩秒");
                Thread.Sleep(2000);

                base.SetTip("將任務列入執行計划");
                task.Start();


                base.SetTip("等待Task執行完畢");
                task.Wait();// Wait方法 會等待Task執行完畢
                base.SetTip("Task執行完畢");
            });

 

另外再強調一點:Task.Start(), 只是將任務列入執行計划,至於任務什么時候去執行則取決於線程池中什么時候有可用線程。
Task.Factory.StartNew 也是一樣的。

 

好了,Task類的介紹、到此為止,后續隨筆再做詳細介紹:這個強大的Task類,還有很多值得我們去探索的東西。

本隨筆到此、暫告一段落。

附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip

 

參見更多:隨筆導讀:同步與異步


(未完待續...)

 


免責聲明!

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



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