並行編程和任務(二)

来源:https://www.cnblogs.com/hulizhong/archive/2019/11/08/11812428.html

前言 上一篇我們主要介紹了並行編程相關的知識,這一節我們繼續介紹關於任務相關的知識。為了更好的控制並行操作,我們可以使用System.Threading.Tasks中的Task類。我們首先來瞭解是什麼是任務——任務表示將要完成的一個或某個工作單元,這個工作單元可以在單獨線程中運行,也可以使用同步方式 ...


前言

  上一篇我們主要介紹了並行編程相關的知識,這一節我們繼續介紹關於任務相關的知識。為了更好的控制並行操作,我們可以使用System.Threading.Tasks中的Task類。我們首先來瞭解是什麼是任務——任務表示將要完成的一個或某個工作單元,這個工作單元可以在單獨線程中運行,也可以使用同步方式啟動運行(需要等待主線程調用)。為什麼使用任務呢?——任務不僅可以獲得一個抽象層(將要完成的工作單元)、還可以對底層的線程運行進行更好更多的控制(任務的運行)。

使用線程池的任務

  我們講到使用任務可以更好更多的控制底層的線程。就涉及到——線程池,線程池提供的是一個後臺線程的池。線程池獨自管理線程、根據需求增加或減少線程數。使用完成的線程返回至線程池中。我們下麵就看看創建任務:

我們看下創建任務的幾種方式:

1、使用實例化的TaskFactory類,然後使用其StartNew方法啟動任務。

2、使用Task靜態的Factory以來訪問TaskFactory,然後調用StartNew方法啟動任務。與第一種相似,但是對工廠的創建的控制就沒那麼全面。

3、使用Task的構造函數,實例化Task對象來指定創建任務,然後通過Start()方法進行啟動任務。

4、使用Task.Run方法來立即啟動任務。

  我們看下以上方法創建的任務有何區別和相同吧,看代碼:

        private static object _lock = new object();
     public
static void TaskMethond(object item) { lock (_lock) { Console.WriteLine(item?.ToString()); Console.WriteLine($"任務Id:{Task.CurrentId?.ToString() ?? "沒有任務運行"}\t 線程Id:{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"是否是線程池中的線程:{Thread.CurrentThread.IsThreadPoolThread}"); Console.WriteLine($"是否是後臺線程:{Thread.CurrentThread.IsBackground}"); Console.WriteLine(); } } #region 任務創建 public static void TaskCreateRun() { var taskFactory = new TaskFactory(); var task1 = taskFactory.StartNew(TaskMethond, "使用實例化TaskFactory"); var task2 = Task.Factory.StartNew(TaskMethond, "使用Task靜態調用Factory"); var task3 = new Task(TaskMethond, "使用Task構造函數實例化"); task3.Start(); var task4 = Task.Run(() => TaskMethond("使用Task.Run")); } #endregion

  我們看代碼運行的結果,發現不管使用的那種方法創建任務,都是使用過的線程池中的線程。

使用單獨線程的任務

  任務當然也不一定就是使用線程池中的線程運行的,也是可以使用其他線程的。如果任務的將長時間運行的話,我們儘可能的考慮使用單獨線程運行(TaskCreationOptions.LongRunning),這個情況下線程就不由線程池管理。我們看下不使用線程池中線程運行,使用單獨線程運行的任務。

        #region 單獨線程運行任務
        public static void OnlyThreadRun()
        {
            var task1 = new Task(TaskMethond, "單獨線程運行任務", TaskCreationOptions.LongRunning);
            task1.Start();
        }
        #endregion

  我們看其運行結果,依舊是後臺線程,但是不是使用的線程池中的線程。

使用同步任務

同時任務也可以同步運行,以相同的線程作為調用線程,下麵我們看下使用Task類中的RunSynchronoushly方法實現同步運行。

 

        #region 同步任務

        public static void TaskRunSynchronoushly()
        {
            TaskMethond("主線程調用");
            var task1 = new Task(TaskMethond, "任務同步調用");
            task1.RunSynchronously();
        }
        #endregion

 

 

我們看運行結果,發現首先調用TaskMethond方法時候沒有任務並且使用的線程1,再我們創建Task實例運行TaskMethond方法的時候,任務id是1,但是線程我們依然使用的是主線程1。 

連續任務

在任務中,我們可以指定在某個任務完成後,應該馬上開始另外一個任務。好比一個任務完成之後應該繼續其處理。但是失敗後我們應該進行一些處理工作。

我們可以使用ContinueWith()方法來定義使用連續任務,表示某任務之後應該開始其他任務,我們也可以指定任務成功後開始某個任務或者失敗後開啟某個任務(TaskContinuationOptions)。

        #region 連續任務
        public static void TaskOne()
        {
            Console.WriteLine($"任務{Task.CurrentId},方法名:{System.Reflection.MethodBase.GetCurrentMethod().Name }啟動");
            Task.Delay(1000).Wait();
            Console.WriteLine($"任務{Task.CurrentId},方法名:{System.Reflection.MethodBase.GetCurrentMethod().Name }結束");
        }
        public static void TaskTwo(Task task)
        {
            Console.WriteLine($"任務{task.Id}以及結束了");
            Console.WriteLine($"現在開始的 任務是任務{Task.CurrentId},方法名稱:{System.Reflection.MethodBase.GetCurrentMethod().Name  }  ");
            Console.WriteLine($"任務處理");
            Task.Delay(1000).Wait();
        }
        public static void TaskThree(Task task)
        {
            Console.WriteLine($"任務{task.Id}以及結束了");
            Console.WriteLine($"現在開始的 任務是任務{Task.CurrentId}.方法名稱:{System.Reflection.MethodBase.GetCurrentMethod().Name } ");
            Console.WriteLine($"任務處理");
            Task.Delay(1000).Wait();
        }
        public static void ContinueTask()
        {
            Task task1 = new Task(TaskOne);
            Task task2 = task1.ContinueWith(TaskTwo, TaskContinuationOptions.OnlyOnRanToCompletion);//已完成情況下繼續任務
            Task task3 = task1.ContinueWith(TaskThree, TaskContinuationOptions.OnlyOnFaulted);//出現未處理異常情況下繼續任務
            task1.Start();
        }
        #endregion

我們看代碼中寫的是先開始運行TaskOne(),然後當任務完成後運行TaskTwo(Task task) ,如果任務失敗的話機會運行TaskThree(Task task)。我們看運行結果中是運行了TaskOne()然後成功後運行了TaskTwo(Task task),避開了TaskThree(Task task)的運行,所以我們是可以通過ContinueWith來進行連續任務和TaskContinuationOptions進行控制任務運行的。

任務層次—父子層次結構

這裡我們利用任務的連續性,我就就可以實現在一個任務結束後立即開啟另一個任務,任務也可以構成一個層次結構。就比如一個任務中啟動了一個任務,這樣的情況就形成了父子層次的結構。下麵我們看的案例就是這麼一個案例。

 

        #region 任務的層次結構——父子層次結構
        public static void ChildTask()
        {
            Console.WriteLine("當前運行的子任務,開啟");
            Task.Delay(5000).Wait();
            Console.WriteLine("子任務運行結束");
        }

        public static void ParentTask()
        {
            Console.WriteLine("父級任務開啟");
            var child = new Task(ChildTask);
            child.Start();
            Task.Delay(1000).Wait();
            Console.WriteLine("父級任務結束");
        }

        public static void ParentAndChildTask()
        {
            var parent = new Task(ParentTask);
            parent.Start();
            Task.Delay(2000).Wait();
            Console.WriteLine($"父級任務的狀態 :{parent.Status}");
            Task.Delay(4000).Wait();
            Console.WriteLine($"父級任務的狀態 :{parent.Status}");
        }
        #endregion

 

 

等待任務

  在前面問介紹的.Net非同步編程中我們講到了WhenAll,用於處理多個非同步方法。在這裡我們繼續擴展點,WhenAll()和WaitAll(),都是等待傳遞給他們的任務完成。但是WaitAll()方法阻塞調用任務,知道所有任務完成為止,而WhenAll()返回了一個任務,從而可以使用async關鍵在等待結果。不會阻塞任務。與之相對應的也還有WaitAny()和WhenAn()。等待任務還有我們一直都用到了的Task.Delay()方法,指定這個方法放回的任務前要等待的毫秒數。

  下麵我們看這個ValueTask等待類型(結構),相對於Task類來說,ValueTask沒有堆中對象的開銷。在一般情況下,Task類型的開銷可以被忽略掉,但是在一些特殊情況下,例如方法被調用千次萬次來看。這種情況ValueTask就變得很適用了。我們看下麵這個案例,使用ValueTask時,在五秒內的情況下直接從它的構造函數返回值。如果時間不在五秒內的話就使用真正獲取數據的方法。然後我們與使用Task的方法進行對比。這裡我們採取十萬條數據的測試對比。

        #region 等待任務
        private static DateTime _time;
        private static List<string> _data;
        public static async ValueTask<List<string>> GetStringDicAsync()
        {
            if (_time >= DateTime.Now.AddSeconds(-5))
            {
                return await new ValueTask<List<string>>(_data);
            }

            else
            {
                (_data, _time) = await GetData();
                return _data; 
            }
        }
        public static Task<(List<string> data, DateTime time)> GetData() => 
            Task.FromResult(
                (Enumerable.Range(0, 10).Select(x => $"itemString{x}").ToList(), DateTime.Now));

        public static async Task<List<string>> GetStringList()
        {
            (_data, _time) = await GetData();
            return _data;
        }
        #endregion
        static async Task  Main(string[] args)
        { 
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.WriteLine("ValueTask開始");
            for (int i = 0; i < 100000; i++)
            {
               var itemList= await GetStringDicAsync(); 
            }
            Console.WriteLine("ValueTask結束");
            Console.WriteLine($"ValueTask耗時:{stopwatch.ElapsedMilliseconds}");

            Console.WriteLine();
            Console.WriteLine();

            stopwatch.Restart();
            Console.WriteLine("Task開始");
            for (int i = 0; i < 100000; i++)
            {
                var itemList = await GetStringList(); 
            }
            Console.WriteLine("Task結束");
            Console.WriteLine($"Task耗時:{stopwatch.ElapsedMilliseconds}");
            Console.ReadLine();
        }

  我們看其運行結果,使用Task和ValueTask的運行結果耗時相差是巨大的。所以在一些特殊情況下使用ValueTask或許會更加的適用。

總結

  今天我們介紹了關於任務相關的一些知識概念。我們結合上一篇文章我們來梳理一些任務、線程、多線程、非同步、同步、併發、並行任務之間的聯繫與關係吧。

  首先我們看我們這章節學習的任務、任務是一個將要完成的工作單元,那麼由誰完成呢?由線程來運行這個任務。那麼關於多線程呢?多線程應該可以說是一個設計概念,用來實現線程切換的。多線程就可以運行多個任務,但是在併發中。在同一時間內只能有一個程式運行。只不過線程間切換速度極快,讓它看起來似乎是在同一時間運行了多個程式。其實在微觀上講,併發在任意時間點只有一個程式在運行,只不過是線程切換速度快。那麼怎麼達到切換速度極快呢?這就需要非同步了。線上程運行的時候不需要等到它完成結果再去繼續其他的線程任務。也就是可等待的。實現A運行起來不等待其結果,然後切換到B繼續運行。這樣切換速度就極快了。我們再仔細看多線程切換線程似乎成了實現非同步的一種方法手段了。有非同步就有同步,同步來說就不需要使用到多線程了,沒必要。反正等到上一個任務運行完成。就繼續使用上一個線程繼續運行。這裡都是講的併發中的情況。那麼並行呢?並行可以說不管在微觀還是巨集觀上都是可以實現一個時間運行多個程式的。併發是多個程式運行在一個處理機上,但是並行任務是運行在多個處理機上。例如實現四個任務並行,那麼我們至少需要四個邏輯處理內核的配合才能到達。

 項目源碼地址


  世界上那些最容易的事情中,拖延時間最不費力。堅韌是成功的一大要素,只要在門上敲得夠久夠大聲,終會把人喚醒的。

     歡迎大家掃描下方二維碼,和我一起學習更多的知識

您的分享是我們最大的動力!

更多相關文章
  • 上一篇自動化測試,全面且詳細的介紹了從零開始到發佈版本的步驟,這是傳統的方式,本次為大家帶來的是如何在5分鐘內使用上docker進行CI/CD,畢竟現在的容器化如火如荼,本示例是基於CentOS-7系統,在示例中, jenkins 和部署 .NET Core 應用程式,都使用 docker 來完成。 ...
  • 一.前言 從這個簡單程式的輸出結果,你想到了什麼?是不是與你心中想的結果不一致?是不是覺得輸出的結果應該為:i is 1,o is 8,o2 is 8 二.程式執行前 圖 2 我們都知道,每一個方法在執行前,操作系統會給方法內每個變數分配記憶體空間。從圖2中就可以看出,在執行前各變數(i,o,o2)已 ...
  • 在一般的前後端分離的web系統開發中,在服務端除了對用戶數據的緩存之外,往往在某些介面上,還涉及到對用戶許可權的限制,有的介面只能讓具有特定許可權的人員才可以訪問。 這樣以來就可以加強系統的安全性,在前面章節中簡單講了 MemoryCache與redis緩存的使用 ,方便將用戶數據緩存到伺服器上在需要的 ...
  • Gtksharp編譯時提示下載gtk文件問題 1、昨天晚上新建gtksharp項目之後,安裝gtksharp之後,編譯時無法成功,提示無法下載gtk 3.24.zip 2、記得前幾天,另一個項目可以生成的。就打開老項目、運行、編譯成功。那會不會是今天寫的代碼導致的,註釋之後編譯還是失敗。沒辦法重新新 ...
  • 前言: 在使用百度編輯器UEditor的時候,如何將圖片保存到伺服器,我剛開始以為是要自己寫上傳文件的方法,後來發現只需要配置一下即可,如果你也正在使用百度富文本編輯器UEditor的話,這篇文章將非常適合你 我使用的C# 1、首先從官網下載插件,一般都是UTF-8格式的,我直接下載了asp版本的: ...
一周排行
  • static void Main(string[] args) { string url = "https://go.microsoft.com/fwlink/?linkid=2108895&amp;clcid=0x409"; DownloadBigFile(new Uri(url), "ssms.... ...
  • 1. 使用ASP.NET Core 3.x 構建 RESTful API - 1.準備工作 什麼是REST 什麼是REST REST一詞最早是在2000年,由Roy Fielding在他的博士論文《Architectural Styles and the Design of Network-base ...
  • wpf 兩個自定義控制項 一個是IP控制項,一個滑動條。先看下效果圖 IPControl 1、實際工作中有時需要設置IP信息,就想著做一個ip控制項。效果沒有window自帶的好,需要通過tab切換。但也能滿足使用。廢話不多說直接上代碼 IPControl.xaml IPControl.xaml.cs 2 ...
  • 近期和幾位做嵌入式開發的朋友閑聊過程中,一位朋友抱怨到:這C#太難用了,我想在N個窗體(或者是N個用戶組件之間)傳遞值都搞不定,非得要定義一個全局變數來存儲,然後用定時器來刷新值,太Low了。我急切的回答道:這很簡單,不就是委托的事嘛。那你來一個示例啊:朋友道。此為這篇博客的起因,所以此篇博客對於有 ...
  • 在面對對象編程中,類的三大特性分別為封裝,繼承,多態。其中多態的具體實現,依賴於三個方法,也就是虛方法,抽象類和介面。 多態的具體作用是什麼呢?或者說多態的存在有什麼意義呢?多態的存在有效的降低了程式的耦合度,在使用的時候,不僅可以表現大家都有的共性,還能在必要的時候突出一些特殊的的個性。 那麼如何 ...
  • [toc] 前言 在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善, 倉庫地址: "April.Util_github" , "April.Util_ ...
  • 【ASP.NET Core學習】在ASP.NET Core 種使用Entity Framework Core介紹,包括如何添加Entity Framwork Core,創建模型和遷移到資料庫,查詢數據,保存數據,使用事務,處理併發衝突 ...
  • 環境:MacOS 10.13 MAMAP Prophp 7.0.33 + xdebugVisual Studio Code前言我所理解的 POP Chain:利用魔術方法並巧妙構造特殊屬性調用一系列函數或類方法以執行某種敏感操作的調用堆棧反序列化常用魔法函數前言我所理解的 POP Chain:利用魔 ...
  • 當創建隊列jobs、監聽器或訂閱伺服器以推送到隊列中時,您可能會開始認為,一旦分派,隊列工作器決定如何處理您的邏輯就完全由您自己決定了。 嗯……並不是說你不能從作業內部與隊列工作器交互,但是通常情況下,哪怕你做了,也是沒必要的。 這個神奇的騷操作的出現是因為“InteractsWithQueue”這 ...
  • 在說正題之前先解釋一下交換機模式是個籠統的稱呼,它不是一個單獨的模式(包括了訂閱模式,路由模式和主題模式),交換機模式是一個比較常用的模式,主要是為了實現數據的同步。 首先,說一下訂閱模式,就和字面上的意思差不多主要就是一個生產者,多個消費者,同一個消息被多個消費者獲取,先看一下官網的圖示 整體執行 ...
x