1. 程式人生 > >Thread、ThreadPool、Task、Parallel、Async和Await基本用法、區別以及弊端

Thread、ThreadPool、Task、Parallel、Async和Await基本用法、區別以及弊端

多執行緒的操作在程式中也是比較常見的,比如開啟一個執行緒執行一些比較耗時的操作(IO操作),而主執行緒繼續執行當前操作,不會造成主執行緒阻塞。執行緒又分為前臺執行緒和後臺執行緒,區別是:整個程式必須要執行完前臺執行緒才會退出,而後臺執行緒會在程式退出的時候結束掉。Thread預設建立的是前臺執行緒,而ThreadPool和Task預設建立的是後臺執行緒,Thread可以通過設定 IsBackground 屬性將執行緒設定為後臺執行緒。

static void Main(string[] args)
{
    Thread thread = new Thread(new ThreadStart(NoParameterMethod));
    thread.Start();
    Console.WriteLine("程式已經執行完成");
}

static void NoParameterMethod()
{
    Thread.Sleep(1000);
    Console.WriteLine("NoParameterMethod");
}
前臺執行緒

效果:

static void Main(string[] args)
{
    Thread thread = new Thread(new ThreadStart(NoParameterMethod))
    {
        IsBackground = true
    };
    thread.Start();
    Console.WriteLine("程式已經執行完成");
}

static void NoParameterMethod()
{
    Thread.Sleep(1000);
    Console.WriteLine("NoParameterMethod");
}
後臺執行緒

效果:

下面來說一下幾種開啟多執行緒的方法:

1、Thread

1.1 開啟一個執行緒,執行一個不帶引數的方法

static void Main(string[] args)
{
    Thread thread = new Thread(new ThreadStart(NoParameterMethod));
//注意Start開啟執行緒之後,當前執行緒不是說一定會立馬執行
//而是說當前執行緒已經準備好被CPU呼叫,至於CPU什麼時候呼叫是需要看情況而定 thread.Start(); Console.WriteLine("程式已經執行完成"); } static void NoParameterMethod() {
//使當前執行緒停止1s Thread.Sleep(1000); Console.WriteLine("NoParameterMethod"); }

1.2開啟一個執行緒,執行帶引數的方法

static void Main(string[] args)
{
    Thread thread = new Thread(new ParameterizedThreadStart(ParameterMethod));
    //要傳入的引數在Start的時候傳入
    thread.Start("ParameterMethod");
    Console.WriteLine("程式已經執行完成");
}
//引數型別必須為Object型別,方法只能有一個引數
//如果想傳入多個引數,可已將引數封裝進入一個類中
static void ParameterMethod(Object x) {
    Thread.Sleep(1000);
    Console.WriteLine(x);
}

2、ThreadPool

使用ThreadPool開啟一個執行緒

//無參    Thread.CurrentThread.ManagedThreadId是當前執行緒的唯一識別符號
ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)));
//有參
ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)), "引數");

ThreadPool是Thread的一個升級版,ThreadPool是從執行緒池中獲取執行緒,如果執行緒池中又空閒的元素,則直接呼叫,如果沒有才會建立,而Thread則是會一直建立新的執行緒,要知道開啟一個執行緒就算什麼事都不做也會消耗大約1m的記憶體,是非常浪費效能的,接下來我們寫一個例子來看一下二者的區別:

#region 使用Thread開啟100個執行緒
for (int i = 0; i < 100; i++)
{
    (new Thread(new ThreadStart(() => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)))).Start();
}
#endregion

執行結果:

我們可以看到每一個主執行緒表示id都是不同的,也就是說使用Thread開啟執行緒每次都是新建立一個

#region 使用ThreadPool開啟100個執行緒
for (int i = 0; i < 100; i++)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)));
}
#endregion

執行結果:

相信區別已經很明顯了,這裡我再說一下,執行緒池中一開始是沒有一個執行緒的,使用ThreadPool開啟一個執行緒之後,執行緒執行完畢,會加入到執行緒池中,後續需要再次開啟執行緒的時候檢視執行緒池中有沒有空閒的執行緒,有則呼叫,沒有則建立,如此迴圈

二者之間還有一個區別,就是ThreadPool可以操控執行緒的狀態,比如等待執行緒完成,或者終止超時子執行緒操作

取消子執行緒操作

CancellationTokenSource cts = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem(new WaitCallback(CanCancelMethod),cts.Token);
cts.Cancel();
Console.ReadKey();
static void CanCancelMethod(Object obj) {
    CancellationToken ct = (CancellationToken)obj;
    if (ct.IsCancellationRequested) {
        Console.WriteLine("該執行緒已取消");
    }
    //就算ct.IsCancellationRequested為真,接下來的程式碼還是會執行
    //因為該方法並沒有ruturn
    Thread.Sleep(1000);
    Console.WriteLine($"子執行緒{Thread.CurrentThread.ManagedThreadId}結束");
}

感覺這個取消子執行緒的方法和設定一個全域性變數,然後通過判斷和更改全域性變數的值,設定執行緒是否取消的效果一樣

ThreadPool的其他操作感興趣的可以自己搜尋學一下,因為終止執行緒什麼操作都是比較麻煩的,關於ThreadPool就不再多說了

3、Task

Task和ThreadPool是一樣的,都是從執行緒池中取空閒的執行緒

 使用Task開啟一個執行緒

//方法1  使用Task的Run方法
Task.Run(()=> {
    Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}已開啟");
});
//方法2   使用Task工廠類TaskFactory物件的StartNew方法
(new TaskFactory()).StartNew(() =>
{
    Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}已開啟");
});

Run和StartNew方法都是返回一個Task型別的物件,代表當前開啟的執行緒,如果方法有返回值

//如果方法有返回值
Task<int> t1 = Task.Run<int>(() => {
    return 1;
});
//通過t1.Result檢視返回的結果
Console.WriteLine(t1.Result);

取消執行緒操作的話和ThreadPool取消執行緒操作一樣

//1s後自動取消執行緒
CancellationTokenSource cts = new CancellationTokenSource(1000);
//為取消執行緒註冊回撥函式
cts.Token.Register(()=> {
    Console.WriteLine("執行緒已取消");
});

Task.Run(()=> {
    Console.WriteLine("開始執行");
    Thread.Sleep(2000);
    //判斷當前執行緒是否已被取消
    if (cts.Token.IsCancellationRequested) {
        Console.WriteLine("方法已結束");
        return;
    }
    Console.WriteLine("執行緒繼續執行");
},cts.Token);

等待所有執行緒執行完畢

//存放所有執行緒
List<Task> lst = new List<Task>();
//開啟10個執行緒
for (int i = 0;i < 10;i++) {
    lst.Add(Task.Run(()=> {
        Thread.Sleep(new Random().Next(1,3000));
        Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}");
    }));
}
//等待所有執行緒執行完畢
Task.WaitAll(lst.ToArray());
Console.WriteLine("所有執行緒執行完畢");

等待任意一個先執行緒執行完畢

//存放所有執行緒
List<Task> lst = new List<Task>();
//開啟10個執行緒
for (int i = 0;i < 10;i++) {
    lst.Add(Task.Run(()=> {
        Thread.Sleep(new Random().Next(1,3000));
        Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}");
    }));
}
//等待任意執行緒執行完畢
Task.WaitAny(lst.ToArray());
Console.WriteLine("已有現成執行完畢");

對於Thread、ThreadPool和Task,如果要用多執行緒的話,優先使用Task,如果版本不支援Task,則考慮ThreadPool

4、Parallel

Parallel迴圈開啟多執行緒,並行任務,對於多執行緒開啟任務,開啟的順序都是不確定的

Parallel.Invoke()

Action[] action = new Action[] {
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
};
Parallel.Invoke(action);

相當於

Action[] action = new Action[] {
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
};
for (int i = 0; i < action.Length; i++)
{
    Task.Run(action[i]);
}

Invoke時也可以進行一些配置,例如配置執行緒池中只能最多保持一個執行緒

Action[] action = new Action[] {
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),
};
Parallel.Invoke(new ParallelOptions()
{
    MaxDegreeOfParallelism = 1
}, action);

執行結果:

Parallel.For()

//將迭代的結果儲存起來
ParallelLoopResult plr =  Parallel.For(1, 10, (i) =>
{
    Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine(plr.IsCompleted);

相當於

for (int i = 1; i < 10; i++)
{
    Task.Run(() =>
    {
        Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}");
    });
}

相對於迴圈Task.Run()更加簡潔

Parallel.ForEach()

方法和foreach類似,不過是採用的是非同步方式遍歷,要想被Parallel.ForEach()必須實現IEnumerable介面

Parallel.ForEach<String>(new List<String>() {
    "a","b","c","d","e","f","g","h","i"
}, (str) =>
{
    Console.WriteLine(str);
});

執行結果:

停止迴圈的方法

//將迭代的結果儲存起來
ParallelLoopResult plr =  Parallel.For(1, 10, (i,state) =>
{
    Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}");
    if (i==4) {
        //結束
        state.Break();
    }
});
Console.WriteLine(plr.IsCompleted);

5、Async、Await

async和await關鍵字用來實現非同步程式設計,async用來修飾方法,await用來呼叫方法,await關鍵字必須出現在有async的方法中,await呼叫的方法可以不用async關鍵字修飾,但是返回值型別必須為Task<T>型別,下面來說一下用法:

static void Main(string[] args)
{ Demo1(); Console.ReadKey(); } static async void Demo1()
{ await Demo2(); } static async Task<int> Demo2()
{ return 1; }

await開啟非同步和Task開啟非同步還是有區別的

例如下面兩個例子

我們先用Task開啟非同步程式設計

static void Main(string[] args)
{
    Console.WriteLine("主執行緒開始");
    TaskDemo1();
    Console.WriteLine("主執行緒結束");
    Console.ReadKey();
}

static void TaskDemo1() {
    Console.WriteLine("非同步開始");
    Task.Run<int>(() =>
    {
        return TaskDemo2();
    });
    Console.WriteLine("非同步結束");
}

static int TaskDemo2()
{
    Console.WriteLine("子執行緒開始");
    Thread.Sleep(1000);
    Console.WriteLine("子執行緒結束");
    return 1;
}

我們這是可以大膽的猜測一下顯示的順尋

大致應該是:主執行緒開始==》非同步開始==》(子執行緒開始|非同步結束)=》(子執行緒開始|主執行緒結束)==》(子執行緒開始)=》子執行緒結束

執行結果:

果然和我們猜想的差不多,大致順序沒有變,接下來我們用async和await關鍵字開啟非同步

static void Main(string[] args)
{
    Console.WriteLine("主執行緒開始");
    AsyncDemo1();
    Console.WriteLine("主執行緒結束");
    Console.ReadKey();
}
static async void AsyncDemo1()
{
    Console.WriteLine("非同步開始");
    await AsyncDemo2();
    Console.WriteLine("非同步結束");
}

static async Task<int> AsyncDemo2()
{
    Console.WriteLine("子執行緒開始");
    //當前子執行緒暫停1s
    await Task.Delay(1000);
    Console.WriteLine("子執行緒結束");
    return 0;
}

 按理說順序也會是:主執行緒開始==》非同步開始==》(子執行緒開始|非同步結束)=》(子執行緒開始|主執行緒結束)==》(子執行緒開始)=》子執行緒結束

但事實是:

Task和async&await關鍵字的區別就此處

首先說一下梳理一下Task的執行過程(畫圖畫的很粗糙,重點是流程)

然後我們再來看一下async和await的執行過程

現在問題已經很清晰了,就是當主執行緒執行到await AsyncDemo2()時,會像是碰到了return語句一樣,退出當前方法(AsyncDemo1),將當前方法(AsyncDemo1)的後續執行語句交給子執行緒來執行,子執行緒會在執行完AsyncDemo2方法之後,返回過來執行AsyncDemo1方法。

這一點就是await與Task非同步程式設計的不同點

&n