c#中線程和異步編程

一 thread

  當我們提及多線程的時候會想到thread和threadpool,這都是異步操作,threadpool其實就是thread的集合,具有很多優勢,不過在任務多的時候全局隊列會存在競爭而消耗資源。thread默認爲前臺線程,主程序必須等線程跑完纔會關閉,而threadpool相反。
  總結:threadpool確實比thread性能優,但是兩者都沒有很好的api區控制,如果線程執行無響應就只能等待結束,從而誕生了task任務。

二 task

1 什麼是task

  .NET 4.0推出了新一代的多線程模型Task,task簡單地看就是任務,那和thread有什麼區別呢?Task的背後的實現也是使用了線程池線程,但它的性能優於ThreadPoll,因爲它使用的不是線程池的全局隊列,而是使用的本地隊列,使線程之間的資源競爭減少。同時Task提供了豐富的API來管理線程、控制。但是相對前面的兩種耗內存,Task依賴於CPU,對於多核的CPU性能遠超前兩者,單核的CPU三者的性能沒什麼差別。

2 兩種創建task的模式

  創建一個task任務有兩種模式:使用factory創建會直接執行,使用new創建不會執行,必須等到start啓動之後才執行。

class Program
{
    static void DownLoad(object str)
    {
        Console.WriteLine("DownLoad Begin ID = " + Thread.CurrentThread.ManagedThreadId + " " + str);
        Thread.Sleep(1000);
        Console.WriteLine("DownLoad End");
    }
    static void Main(string[] args)
    {
    
        //創建任務
        //Task task = new Task(DownLoad, "人民日報");
        //啓動任務
        //task.Start();
        
		//創建任務工廠
        TaskFactory taskFactory = new TaskFactory();
        //開始新的任務
        taskFactory.StartNew(DownLoad, "紐約時報");
        
        Console.WriteLine("Main");   
        Console.ReadKey();
    }
}

3 task的生命週期

var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);         
testTask.Wait();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
/*
輸出結果:
Created
task start
Running
Running
RanToCompletion
RanToCompletion
*/

  可以看出task確實是異步執行,並且wait很好地控制了task。

4 task的控制

var testTask = new Task(() =>
    {
        Console.WriteLine("task start");
        System.Threading.Thread.Sleep(2000);
    });
testTask.Start();
testTask.Wait();
var testTask = new Task(() =>
    {
       Console.WriteLine("task start");
       System.Threading.Thread.Sleep(2000);
    });
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
    {                 
        Console.WriteLine("factory task start");
     });
Task.WaitAll(testTask, factoryTeak);
Console.WriteLine("end");
var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
{                  
    Console.WriteLine("factory task start");
});
Task.WaitAny(testTask, factoryTeak);
Console.WriteLine("end");

  通過wait()對單個task進行等待,Task.waitall()對多個task進行等待,waitany()執行任意一個task就往下繼續執行。

5 連續任務

var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var resultTest = testTask.ContinueWith<string>((Task) =>
{
    Console.WriteLine("testTask end");
    return "end";
});
Console.WriteLine(resultTest.Result);

6 task的取消

  首先創建一個取消task的令牌的實例,在不啓動task直接取消:

var tokenSource = new CancellationTokenSource();//創建取消task實例
var testTask = new Task(() =>
{
    for (int i = 0; i < 6; i++)
    {
        System.Threading.Thread.Sleep(1000);
    }
},tokenSource.Token);
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(()=>
{
    Console.WriteLine("task is to cancel");
});
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
//輸出結果:
/*
Created
task is to cancel
Canceled
*/

  如果task啓動了真的取消了task?

var tokenSource = new CancellationTokenSource();//創建取消task實例
var testTask = new Task(() =>
{
    for (int i = 0; i <6; i++) {
        System.Threading.Thread.Sleep(1000);
    }               
},tokenSource.Token);
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(()=>
{
    Console.WriteLine("task is to cancel");
});
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
for (int i = 0; i < 10; i++)
{
    System.Threading.Thread.Sleep(1000);
    Console.WriteLine(testTask.Status);
}
/*
輸出結果:
Created
WaitingToRun
task is to cancel
Running
Running
Running
Running
Running
Running
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
*/

  可以看出其實並沒有取消task,此時task還在繼續跑。

7 task的嵌套

  在一個任務中可以啓動子任務,兩個任務異步執行。默認情況下,子任務(即由外部任務創建的內部任務)將獨立於其父任務執行。使用TaskCreationOptions.AttachedToParent顯式指定將任務附加到任務層次結構中的某個父級。

var parentTask = new Task(()=>
{
    var childTask = new Task(() =>
    {
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("childTask to start");
    });
    childTask.Start();
    Console.WriteLine("parentTask to start");
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");

  此時爲普通關聯,父task和子task沒影響

var parentTask = new Task(()=>
{
	var childTask = new Task(() =>{
	    System.Threading.Thread.Sleep(2000);
	    Console.WriteLine("childTask to start");
	}, TaskCreationOptions.AttachedToParent);
	childTask.Start();
	Console.WriteLine("parentTask to start");
} );
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");

  此時爲父task和子task關聯,wait會一直等待父子task執行完。
  如果父任務執行完了但是子任務沒有執行完,則父任務的狀態會被設置爲WaitingForChildrenToComplete,只有子任務也執行完了,父任務的狀態纔會變成RunToCompletion

8 任務執行的結果

  使用Task的泛型版本,可以返回任務的執行結果。
  下面例子中的TaskWithResult的輸入爲object類型,返回一個元組Tuple<int, int>。
  定義調用TaskWithResult的任務時,使用泛型類Task<Tuple<int, int>>,泛型的參數定義了返回類型。通過構造函數,傳遞TaskWithResult,構造函數的第二個參數定義了TaskWithResult的輸入值。
  任務完成後,通過Result屬性獲取任務的結果。

class Program
{
    static Tuple<int, int> TaskWithResult(object obj)
    {
        Tuple<int, int> div = (Tuple<int, int>)obj;
        Thread.Sleep(1000);
        return Tuple.Create<int, int>(div.Item1 + div.Item2, div.Item1 - div.Item2);
    }
    static void Main(string[] args)
    {
        var task = new Task<Tuple<int, int>>(TaskWithResult, Tuple.Create<int, int>(8, 3));            
        task.Start();
        Console.WriteLine(task.Result);
        task.Wait();
        Console.WriteLine("Result: {0} {1}", task.Result.Item1, task.Result.Item2);
        Console.ReadLine();
    }
}
/*
執行結果
(11, 5)
result:11 5
*/

9 task死鎖的問題

  我們可以設置最大等待時間,如果超過了等待時間,就不再等待,下面我們來修改代碼,設置最大等待時間爲5秒(項目中可以根據實際情況設置),如果超過5秒就輸出哪個任務出錯了。

10 對Spinlock的使用

  舉例來說Parallel.for和Parallel.foreach是線程不安全的,有可能達不到你的預期,此時就需要加鎖來解決此問題,我們可以加lock和spinlock(自旋鎖)來解決。

SpinLock slock = new SpinLock(false);
var testLock= new object();
long sum1 = 0;
long sum2 = 0;
long sum3 = 0;
Parallel.For(0, 100000, i =>
{
    sum1 += i;
});

Parallel.For(0, 100000, i =>
{
    bool lockTaken = false;
    try
    {
        slock.Enter(ref lockTaken);
        sum2 += i;
    }
    finally
    {
        if (lockTaken)
            slock.Exit(false);
    }
});
Parallel.For(0, 100000, i =>
{
    lock(testLock)
    {
        sum3 += i;
    };
});
Console.WriteLine("Num1的值爲:{0}", sum1);
Console.WriteLine("Num2的值爲:{0}", sum2);
Console.WriteLine("Num3的值爲:{0}", sum3);
/*            
輸出結果:
Num1的值爲:1660913202
Num2的值爲:4999950000
Num3的值爲:4999950000

Num1的值爲:2754493646
Num2的值爲:4999950000
Num3的值爲:4999950000

Num1的值爲:4999950000
Num2的值爲:4999950000
Num3的值爲:4999950000
*/

三 Thread & Task比較

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DemoAsync{
class Program{
    static void Main(string[] args)
    {
        Console.WriteLine("Task   With Thread  Start !");
        for (int i = 0; i <= 5; i++)
        {
            Thread t = new Thread(Dotaskfunction);
            t.Start();
        }
        Console.WriteLine("Task   With Thread End !");
        Console.WriteLine("Task   With Task   Start !");
        for (int i = 0; i <= 5; i++)
        {
            Task.Run(() => { Dotaskfunction(); });
        }
        Console.WriteLine("Task   With Task End !");
        Console.ReadLine();
    }
    public static void Dotaskfunction()
    {
        Console.WriteLine("task  has been done! ThreadID: {0},IsBackGround:{1} ", Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread .IsBackground );
    }
}
}

在這裏插入圖片描述
  可以看到Thread方法每次的Thread Id都是不同的,而Task方法的Thread Id是重複出現的。我們知道線程的創建和銷燬是一個開銷比較大的操作,Task每次執行將不會立即創建一個新線程,而是到CLR線程池查看是 否有空閒的線程,有的話就取一個線程處理這個請求,處理完請求後再把線程放回線程池,這個線程也不會立即撤銷,而是設置爲空閒狀態,可供線程池再次調度, 從而減少開銷。

5 IsBackground作用

要點:
1、當在主線程中創建了一個線程,那麼該線程的IsBackground默認是設置爲FALSE的。
2、當主線程退出的時候,IsBackground=FALSE的線程還會繼續執行下去,直到線程執行結束。
3、只有IsBackground=TRUE的線程纔會隨着主線程的退出而退出。
4、當初始化一個線程,把Thread.IsBackground=true的時候,指示該線程爲後臺線程。後臺線程將會隨着主線程的退出而退出。
5、原理:只要所有前臺線程都終止後,CLR就會對每一個活在的後臺線程調用Abort()來徹底終止應用程序。
Net的公用語言運行時(Common Language Runtime,CLR)能區分兩種不同類型的線程:前臺線程和後臺線程。這兩者的區別就是:應用程序必須運行完所有的前臺線程纔可以退出;而對於後臺線程,應用程序則可以不考慮其是否已經運行完畢而直接退出,所有的後臺線程在應用程序退出時都會自動結束。
既然前臺線程和後臺線程有這種差別,那麼我們怎麼知道該如何設置一個線程的IsBackground屬性呢?下面是一些基本的原則:對於一些在後臺運行的線程,當程序結束時這些線程沒有必要繼續運行了,那麼這些線程就應該設置爲後臺線程。比如一個程序啓動了一個進行大量運算的線程,可是隻要程序一旦結束,那個線程就失去了繼續存在的意義,那麼那個線程就該是作爲後臺線程的。而對於一些服務於用戶界面的線程往往是要設置爲前臺線程的,因爲即使程序的主線程結束了,其他的用戶界面的線程很可能要繼續存在來顯示相關的信息,所以不能立即終止它們。這裏我只是給出了一些原則,具體到實際的運用往往需要編程者的進一步仔細斟酌。
一般後臺線程用於處理時間較短的任務,如在一個Web服務器中可以利用後臺線程來處理客戶端發過來的請求信息。而前臺線程一般用於處理需要長時間等待的任務,如在Web服務器中的監聽客戶端請求的程序,或是定時對某些系統資源進行掃描的程序。

四 threadpoll和task的結構圖

  threadpool:
在這裏插入圖片描述
   task:
在這裏插入圖片描述

五 Async/Await

  本節內容來自這裏
  async/await特性是與Task緊密相關的,所以在瞭解async/await前必須充分了解Task的使用。

1 示例

在這裏插入圖片描述
在這裏插入圖片描述
  1、從 Main 方法執行到CountCharactersAsync(1, url1)方法時,該方法會立即返回,然後纔會調用它內部的方法開始下載內容。該方法返回的是一個Task<int>類型的佔位符對象,表示計劃進行的工作。這個佔位符最終會返回 int 類型的值。
  2、這樣就可以不必等CountCharactersAsync(1, url1)方法執行完成就可以繼續進行下一步操作。到執行CountCharactersAsync(2, url2)方法時,一樣返回Task<int>對象。
  3、然後,Main方法繼續執行三次ExtraOperation方法,同時兩次 CountCharactersAsync方法依然在持續工作 。
  4、t1.Resultt2.Result是指從CountCharactersAsync方法調用的Task<int>對象取結果,如果還沒有結果的話,將阻塞,直有結果返回爲止。

2 async/await 結構

  async/await 結構可分成三部分:
  (1)調用方法:該方法調用異步方法,然後在異步方法執行其任務的時候繼續執行;
  (2)異步方法:該方法異步執行工作,然後立刻返回到調用方法;
  (3)await 表達式:用於異步方法內部,指出需要異步執行的任務。一個異步方法可以包含多個 await 表達式(不存在 await 表達式的話 IDE 會發出警告)。

3 What’s 異步方法

  異步方法:在執行完成前立即返回調用方法,在調用方法繼續執行的過程中完成任務。
  語法分析:
  (1)關鍵字:方法頭使用async修飾。
  (2)要求:包含 N(N>0) 個await表達式(不存在await表達式的話 IDE 會發出警告),表示需要異步執行的任務,沒有的話,就和普通方法一樣執行了。
  (3)返回類型:只能返回 3 種類型(voidTaskTask\<T>)。TaskTask\<T>標識返回的對象會在將來完成工作,表示調用方法和異步方法可以繼續執行。
  (4)參數:數量不限,但不能使用 out 和 ref 關鍵字。
  (5)命名約定:方法後綴名應以 Async 結尾。
  (6)其它:匿名方法和 Lambda 表達式也可以作爲異步對象;async 是一個上下文關鍵字;關鍵字 async 必須在返回類型前。
  關於 async 關鍵字:
  1、在返回類型之前包含 async 關鍵字;
  2、它只是標識該方法包含一個或多個 await 表達式,即,它本身不創建異步操作;
  3、它是上下文關鍵字,即可作爲變量名。

4 返回類型

  1、Task<T>:調用方法要從調用中獲取一個 T 類型的值,異步方法的返回類型就必須是Task。調用方法從 Task 的 Result 屬性獲取的就是 T 類型的值。

internal class Calculator
{
   private static int Add(int n, int m)
   {
       return n + m;
   }

   public static async Task<int> AddAsync(int n, int m)
   {
       int val = await Task.Run(() => Add(n, m));
       return val;
   }
}
private static void Main(string[] args)
{
    Task<int> t = Calculator.AddAsync(1, 2);
    //一直在幹活
    Console.WriteLine($"result: {t.Result}");
    Console.Read();
}

  2、Task:調用方法不需要從異步方法中取返回值,但是希望檢查異步方法的狀態,那麼可以選擇可以返回 Task 類型的對象。不過,就算異步方法中包含 return 語句,也不會返回任何東西。

internal class Calculator
{
   private static int Add(int n, int m)
   {
       return n + m;
   }

   public static async Task AddAsync(int n, int m)
   {
       int val = await Task.Run(() => Add(n, m));
       Console.WriteLine($"Result: {val}");
   }
}
private static void Main(string[] args)
{
    Task t = Calculator.AddAsync(1, 2);

    //一直在幹活

   t.Wait();
   Console.WriteLine("AddAsync 方法執行完成");

   Console.Read();
}

  (3)void:調用方法執行異步方法,但又不需要做進一步的交互。

internal class Calculator
{
   private static int Add(int n, int m)
    {
        return n + m;
    }
    public static async void AddAsync(int n, int m)
    {
        int val = await Task.Run(() => Add(n, m));
        Console.WriteLine($"Result: {val}");
    }
}
private static void Main(string[] args)
{
    Calculator.AddAsync(1, 2);
    //一直在幹活
    Thread.Sleep(1000); //掛起1秒鐘
    Console.WriteLine("AddAsync 方法執行完成");
    Console.Read();
}

  
  

參考文章

  c#之task與thread區別及其使用:https://blog.csdn.net/qq_40677590/article/details/102797838
  c#中任務Task:https://blog.csdn.net/liyazhen2011/article/details/81262582
  走進異步編程的世界 - 剖析異步方法(上):https://www.cnblogs.com/liqingwen/p/5844095.html
  走進異步編程的世界 - 剖析異步方法(下):cnblogs.com/liqingwen/p/5866241.html
  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章