1. 程式人生 > 實用技巧 >C# 併發程式設計 (非同步程式設計與多執行緒)

C# 併發程式設計 (非同步程式設計與多執行緒)

併發:同時做多件事情

多執行緒:併發的一種形式,它採用多個執行緒來執行程式。

並行處理:把正在執行的大量的任務分割成小塊,分配給多個同時執行的執行緒。並行處理是多執行緒的一種,而多執行緒是併發的一種。

非同步程式設計:併發的一種形式,它採用 future 模式或回撥(callback)機制,以避免產生不必要的 執行緒,非同步程式設計的核心理念是非同步操作:啟動了的操作將會在一段時間後完成。這個操作 正在執行時,不會阻塞原來的執行緒。啟動了這個操作的執行緒,可以繼續執行其他任務。當 操作完成時,會通知它的 future,或者呼叫回撥函式,以便讓程式知道操作已經結束,

async 和 await:這讓非同步程式設計變得幾乎和同步(非併發)程式設計一樣容易。await的作用:啟動一個將會被執行的Task(該Task將會在新執行緒中執行),並立即返回,所以await所在的函式不會被阻塞,當Task完成後,繼續執行await後面的程式碼。

通常情況下,一個併發程式要使用多種技術。大多數程式至少使用了多執行緒(通過執行緒 池)和非同步程式設計

非同步程式設計有兩大好處。第一個好處是對於面向終端使用者的 GUI 程式:非同步程式設計提高了響應 能力。我們都遇到過在執行時會臨時鎖定介面的程式,非同步程式設計可以使程式在執行任務時 仍能響應使用者的輸入。第二個好處是對於伺服器端應用:非同步程式設計實現了可擴充套件性。服務 器應用可以利用執行緒池滿足其可擴充套件性,使用非同步程式設計後,可擴充套件性通常可以提高一個數 量級。
現代的非同步 .NET 程式使用兩個關鍵字:async 和 await。async 關鍵字加在方法宣告上, 它的主要目的是使方法內的 await 關鍵字生效(為了保持向後相容,同時引入了這兩個關 鍵字)。如果 async 方法有返回值,應返回 Task<T>;如果沒有返回值,應返回 Task。這些 task 型別相當於 future,用來在非同步方法結束時通知主程式。

1.Async/Await

async 方法在開始時以同步方式執行。在 async 方法內部,await 關鍵字 對它的引數執行一個非同步等待。它首先檢查操作是否已經完成,如果完成了,就繼續執行 (同步方式)。否則,它會暫停 async 方法,並返回,留下一個未完成的 task。一段時間後, 操作完成,async 方法就恢復執行。 不會阻塞UI執行緒 ,await方法完成自動的通知他 然後執行剩餘的程式碼,非同步是為了程式本身不卡 呼叫處還是非同步的 呼叫處下面的程式碼還是會先執行 await實際上只是把主執行緒釋放了,使用了其他執行緒代替他繼續 非同步不是為了讓請求更快,而是為了可以處理更多請求

非同步有一個核心,是Task。而Task有一個方法,就是Wait,寫法是Task.Wait()。所以,很多人把這個Wait和await混為一談,這是錯的。

這個問題來自於Task。C#裡,Task不是專為非同步準備的,它表達的是一個執行緒,是工作線上程池裡的一個執行緒。非同步是執行緒的一種應用,多執行緒也是執行緒的一種應用。Wait,以及Status、IsCanceled、IsCompleted、IsFaulted等等,是給多執行緒準備的方法,跟非同步沒有半毛錢關係。當然你非要在非同步中使用多執行緒的Wait或其它,從程式碼編譯層面不會出錯,但程式會。

尤其,Task.Wait()是一個同步方法,用於多執行緒中阻塞等待。

用Task.Wait()來實現同步方法中呼叫非同步方法,這個用法本身就是錯誤的。 非同步不是多執行緒,而且在多執行緒中,多個Task.Wait()使用也會死鎖,也有解決和避免死鎖的一整套方式。

Task.Wait()是一個同步方法,用於多執行緒中阻塞等待,不是實現同步方法中呼叫非同步方法的實現方式。

在非同步中,await表達的意思是:當前執行緒/方法中,await引導的方法出結果前,跳出當前執行緒/方法,從呼叫當前執行緒/方法的位置,去執行其它可能執行的執行緒/方法,並在引導的方法出結果後,把執行點拉回到當前位置繼續執行;直到遇到下一個await,或執行緒/方法完成返回,跳回去剛才外部最後執行的位置繼續執行。

2.非同步鎖

從 .NET Framework 4.5 開始,任何使用 async/await 進行修飾的方法,都會被認為是一個非同步方法;實際上,這些非同步方法都是基於佇列的執行緒任務,從你開始使用 Task 去執行一段程式碼的時候,實際上就相當於開啟了一個執行緒,預設情況下,這個執行緒數由執行緒池 ThreadPool 進行管理的。

執行緒安全的訪問方式可以通過lock來進行唯一執行緒限定,但如果使用await等待Task完成,則Task中不允許使用lock。

因此採用另外一種方式完成

呼叫

3.ConcurrentQueue 佇列

鎖的引入,帶來了一定的開銷和效能的損耗,並降低了程式的擴充套件性,而且還會有死鎖的發生(雖說概率不大,但也不能不防啊),因此:使用LOCK進行併發程式設計顯然不太適用。

還好,微軟一直在更新自己的東西:

.NET Framework 4提供了新的執行緒安全和擴充套件的併發集合,它們能夠解決潛在的死鎖問題和競爭條件問題,因此在很多複雜的情形下它們能夠使得並行程式碼更容易編寫,這些集合儘可能減少使用鎖的次數,從而使得在大部分情形下能夠優化為最佳效能,不會產生不必要的同步開銷。

ConcurrentQueue 佇列

ConcurrentQueue是完全無鎖的,能夠支援併發的新增元素,先進先出。下面貼程式碼,詳解見註釋:

class Program
    {
        private static object o = new object();
        /*定義 Queue*/
        private static Queue<Product> _Products { get; set; }
        private static ConcurrentQueue<Product> _ConcurrenProducts { get; set; }
        /*  coder:天才臥龍  
         *  程式碼中 建立三個併發執行緒 來操作_Products 和 _ConcurrenProducts 集合,每次新增 10000 條資料 檢視 一般佇列Queue 和 多執行緒安全下的佇列ConcurrentQueue 執行情況
         */
        static void Main(string[] args)
        {
            Thread.Sleep(1000);
            _Products = new Queue<Product>();
            Stopwatch swTask = new Stopwatch();//用於統計時間消耗的
            swTask.Start();

            /*建立任務 t1  t1 執行 資料集合新增操作*/
            Task t1 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*建立任務 t2  t2 執行 資料集合新增操作*/
            Task t2 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });
            /*建立任務 t3  t3 執行 資料集合新增操作*/
            Task t3 = Task.Factory.StartNew(() =>
            {
                AddProducts();
            });

            Task.WaitAll(t1, t2, t3);
            swTask.Stop();
            Console.WriteLine("List<Product> 當前資料量為:" + _Products.Count);
            Console.WriteLine("List<Product> 執行時間為:" + swTask.ElapsedMilliseconds);

            Thread.Sleep(1000);
            _ConcurrenProducts = new ConcurrentQueue<Product>();
            Stopwatch swTask1 = new Stopwatch();
            swTask1.Start();

            /*建立任務 tk1  tk1 執行 資料集合新增操作*/
            Task tk1 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });
            /*建立任務 tk2  tk2 執行 資料集合新增操作*/
            Task tk2 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });
            /*建立任務 tk3  tk3 執行 資料集合新增操作*/
            Task tk3 = Task.Factory.StartNew(() =>
            {
                AddConcurrenProducts();
            });

            Task.WaitAll(tk1, tk2, tk3);
            swTask1.Stop();
            Console.WriteLine("ConcurrentQueue<Product> 當前資料量為:" + _ConcurrenProducts.Count);
            Console.WriteLine("ConcurrentQueue<Product> 執行時間為:" + swTask1.ElapsedMilliseconds);
            Console.ReadLine();
        }

        /*執行集合資料新增操作*/

        /*執行集合資料新增操作*/
        static void AddProducts()
        {
            Parallel.For(0, 30000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                lock (o)
                {
                    _Products.Enqueue(product);
                }
            });

        }
        /*執行集合資料新增操作*/
        static void AddConcurrenProducts()
        {
            Parallel.For(0, 30000, (i) =>
            {
                Product product = new Product();
                product.Name = "name" + i;
                product.Category = "Category" + i;
                product.SellPrice = i;
                _ConcurrenProducts.Enqueue(product);
            });

        }
    }

    class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public int SellPrice { get; set; }
    }

從執行時間上來看,使用 ConcurrentQueue 相比 LOCK 明顯快了很多!

1.BlockingCollection與經典的阻塞佇列資料結構類似,能夠適用於多個任務新增和刪除資料,提供阻塞和限界能力。

2.ConcurrentBag提供物件的執行緒安全的無序集合

3.ConcurrentDictionary提供可有多個執行緒同時訪問的鍵值對的執行緒安全集合

4.ConcurrentQueue提供執行緒安全的先進先出集合

5.ConcurrentStack提供執行緒安全的後進先出集合