C# 多線程詳細講解
一、基本概念
1、程序
首先打開任務管理器,檢視目前運作的程序:

從任務管理器裡面可以看到目前所有正在運作的程序。那麼究竟什麼是程序呢?
程序(Process)是Windows系統中的一個基本概念,它包含着一個運作程式所需要的資源。一個正在運作的應用程式在作業系統中被視為一個程序,程序可以包括一個或多個線程。
線程是作業系統配置設定處理器時間的基本單元,在程序中可以有多個線程同時執行代碼。程序之間是相對獨立的,一個程序無法通路另一個程序的資料(除非利用分布式計算方式),
一個程序運作的失敗也不會影響其他程序的運作,Windows系統就是利用程序把工作劃分為多個獨立的區域的。程序可以了解為一個程式的基本邊界。是應用程式的一個運作例程,
是應用程式的一次動态執行過程。
二、線程
在任務管理器裡面查詢目前總共運作的線程數:
線程(Thread)是程序中的基本執行單元,是作業系統配置設定CPU時間的基本機關,一個程序可以包含若幹個線程,在程序入口執行的第一個線程被視為這個程序的主線程。
在.NET應用程式中,都是以Main()方法作為入口的,當調用此方法時系統就會自動建立一個主線程。線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)
組成的。CPU寄存器主要記錄目前所執行線程的狀态,調用棧主要用于維護線程所調用到的記憶體與資料,TLS主要用于存放線程的狀态資訊。
二、多線程
多線程的優點:可以同時完成多個任務;可以使程式的響應速度更快;可以讓占用大量處理時間的任務或目前沒有進行處理的任務定期将處理時間讓給别的任務;可以随時停止任務;
可以設定每個任務的優先級以優化程式性能。那麼可能有人會問:為什麼可以多線程執行呢?總結起來有下面兩方面的原因:
1、CPU運作速度太快,硬體處理速度跟不上,是以作業系統進行分時間片管理。這樣,從宏觀角度來說是多線程并發的,因為CPU速度太快,察覺不到,看起來是同一時刻執行了不同的操作。
但是從微觀角度來講,同一時刻隻能有一個線程在處理。
2、目前電腦都是多核多CPU的,一個CPU在同一時刻隻能運作一個線程,但是多個CPU在同一時刻就可以運作多個線程。
然而,多線程雖然有很多優點,但是也必須認識到多線程可能存在影響系統性能的不利方面,才能正确使用線程。不利方面主要有如下幾點:
(1)線程也是程式,是以線程需要占用記憶體,線程越多,占用記憶體也越多。
(2)多線程需要協調和管理,是以需要占用CPU時間以便跟蹤線程。
(3)線程之間對共享資源的通路會互相影響,必須解決争用共享資源的問題。
(4)線程太多會導緻控制太複雜,最終可能造成很多程式缺陷。
當啟動一個可執行程式時,将建立一個主線程。在預設的情況下,C#程式具有一個線程,此線程執行程式中以Main方法開始和結束的代碼,Main()方法直接或間接執行的每一個指令都有
預設線程(主線程)執行,當Main()方法傳回時此線程也将終止。一個程序可以建立一個或多個線程以執行與該程序關聯的部分程式代碼。在C#中,線程是使用Thread類處理的,
該類在System.Threading命名空間中。使用Thread類建立線程時,隻需要提供線程入口,線程入口告訴程式讓這個線程做什麼。通過執行個體化一個Thread類的對象就可以建立一個線程。
建立新的Thread對象時,将建立新的托管線程。Thread類接收一個ThreadStart委托或ParameterizedThreadStart委托的構造函數,該委托包裝了調用Start方法時由新線程調用的方法,示例代碼如下:
Thread thread=new Thread(new ThreadStart(method));//建立線程
thread.Start(); //啟動線程
上面代碼執行個體化了一個Thread對象,并指明将要調用的方法method(),然後啟動線程。ThreadStart委托中作為參數的方法不需要參數,并且沒有傳回值。ParameterizedThreadStart委托一個對象作為參數,
利用這個參數可以很友善地向線程傳遞參數,示例代碼如下:
Thread thread=new Thread(new ParameterizedThreadStart(method));//建立線程
thread.Start(3); //啟動線程
建立多線程的步驟:
1、編寫線程所要執行的方法
2、執行個體化Thread類,并傳入一個指向線程所要執行方法的委托。(這時線程已經産生,但還沒有運作)
3、調用Thread執行個體的Start方法,标記該線程可以被CPU執行了,但具體執行時間由CPU決定
2.1 System.Threading.Thread類
Thread類是是控制線程的基礎類,位于System.Threading命名空間下,具有4個重載的構造函數:
名稱 | 說明 |
Thread(ParameterizedThreadStart) | 初始化 Thread 類的新執行個體,指定允許對象線上程啟動時傳遞給線程的委托。要執行的方法是有參的。 |
Thread(ParameterizedThreadStart, Int32) | 初始化 Thread 類的新執行個體,指定允許對象線上程啟動時傳遞給線程的委托,并指定線程的最大堆棧大小 |
Thread(ThreadStart) | 初始化 Thread 類的新執行個體。要執行的方法是無參的。 |
Thread(ThreadStart, Int32) | 初始化 Thread 類的新執行個體,指定線程的最大堆棧大小。 |
ThreadStart是一個無參的、傳回值為void的委托。委托定義如下:
public delegate void ThreadStart()
通過ThreadStart委托建立并運作一個線程:
運作結果
除了可以運作靜态的方法,還可以運作執行個體方法
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 //建立ThreadTest類的一個執行個體
6 ThreadTest test=new ThreadTest();
7 //調用test執行個體的MyThread方法
8 Thread thread = new Thread(new ThreadStart(test.MyThread));
9 //啟動線程
10 thread.Start();
11 Console.ReadKey();
12 }
13 }
14
15 class ThreadTest
16 {
17 public void MyThread()
18 {
19 Console.WriteLine("這是一個執行個體方法");
20 }
21 }
運作結果:
如果為了簡單,也可以通過匿名委托或Lambda表達式來為Thread的構造方法指派
1 static void Main(string[] args)
2 {
3 //通過匿名委托建立
4 Thread thread1 = new Thread(delegate() { Console.WriteLine("我是通過匿名委托建立的線程"); });
5 thread1.Start();
6 //通過Lambda表達式建立
7 Thread thread2 = new Thread(() => Console.WriteLine("我是通過Lambda表達式建立的委托"));
8 thread2.Start();
9 Console.ReadKey();
10 }
運作結果:
ParameterizedThreadStart是一個有參的、傳回值為void的委托,定義如下:
public delegate void ParameterizedThreadStart(Object obj)
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 //通過ParameterizedThreadStart建立線程
6 Thread thread = new Thread(new ParameterizedThreadStart(Thread1));
7 //給方法傳值
8 thread.Start("這是一個有參數的委托");
9 Console.ReadKey();
10 }
11
12 /// <summary>
13 /// 建立有參的方法
14 /// 注意:方法裡面的參數類型必須是Object類型
15 /// </summary>
16 /// <param name="obj"></param>
17 static void Thread1(object obj)
18 {
19 Console.WriteLine(obj);
20 }
21 }
注意:ParameterizedThreadStart委托的參數類型必須是Object的。如果使用的是不帶參數的委托,不能使用帶參數的Start方法運作線程,否則系統會抛出異常。但使用帶參數的委托,可以使用thread.Start()來運作線程,這時所傳遞的參數值為null。
2.2 線程的常用屬性
屬性名稱 | 說明 |
---|---|
CurrentContext | 擷取線程正在其中執行的目前上下文。 |
CurrentThread | 擷取目前正在運作的線程。 |
ExecutionContext | 擷取一個 ExecutionContext 對象,該對象包含有關目前線程的各種上下文的資訊。 |
IsAlive | 擷取一個值,該值訓示目前線程的執行狀态。 |
IsBackground | 擷取或設定一個值,該值訓示某個線程是否為背景線程。 |
IsThreadPoolThread | 擷取一個值,該值訓示線程是否屬于托管線程池。 |
ManagedThreadId | 擷取目前托管線程的唯一辨別符。 |
Name | 擷取或設定線程的名稱。 |
Priority | 擷取或設定一個值,該值訓示線程的排程優先級。 |
ThreadState | 擷取一個值,該值包含目前線程的狀态。 |
2.2.1 線程的辨別符
ManagedThreadId是确認線程的唯一辨別符,程式在大部分情況下都是通過Thread.ManagedThreadId來辨識線程的。而Name是一個可變值,在預設時候,Name為一個空值 Null,
開發人員可以通過程式設定線程的名稱,但這隻是一個輔助功能。
2.2.2 線程的優先級别
當線程之間争奪CPU時間時,CPU按照線程的優先級給予服務。高優先級的線程可以完全阻止低優先級的線程執行。.NET為線程設定了Priority屬性來定義線程執行的優先級别,
裡面包含5個選項,其中Normal是預設值。除非系統有特殊要求,否則不應該随便設定線程的優先級别。
成員名稱 | 說明 |
---|---|
Lowest | 可以将 Thread 安排在具有任何其他優先級的線程之後。 |
BelowNormal | 可以将 Thread 安排在具有 Normal 優先級的線程之後,在具有 Lowest 優先級的線程之前。 |
Normal | 預設選擇。可以将 Thread 安排在具有 AboveNormal 優先級的線程之後,在具有 BelowNormal 優先級的線程之前。 |
AboveNormal | 可以将 Thread 安排在具有 Highest 優先級的線程之後,在具有 Normal 優先級的線程之前。 |
Highest | 可以将 Thread 安排在具有任何其他優先級的線程之前。 |
2.2.3 線程的狀态
通過ThreadState可以檢測線程是處于Unstarted、Sleeping、Running 等等狀态,它比 IsAlive 屬性能提供更多的特定資訊。
前面說過,一個應用程式域中可能包括多個上下文,而通過CurrentContext可以擷取線程目前的上下文。
CurrentThread是最常用的一個屬性,它是用于擷取目前運作的線程。
2.2.4 System.Threading.Thread的方法
Thread 中包括了多個方法來控制線程的建立、挂起、停止、銷毀,以後來的例子中會經常使用。
方法名稱 | 說明 |
---|---|
Abort() | 終止本線程。 |
GetDomain() | 傳回目前線程正在其中運作的目前域。 |
GetDomainId() | 傳回目前線程正在其中運作的目前域Id。 |
Interrupt() | 中斷處于 WaitSleepJoin 線程狀态的線程。 |
Join() | 已重載。 阻塞調用線程,直到某個線程終止時為止。 |
Resume() | 繼續運作已挂起的線程。 |
Start() | 執行本線程。 |
Suspend() | 挂起目前線程,如果目前線程已屬于挂起狀态則此不起作用 |
Sleep() | 把正在運作的線程挂起一段時間。 |
線程示例
1 static void Main(string[] args)
2 {
3 //擷取正在運作的線程
4 Thread thread = Thread.CurrentThread;
5 //設定線程的名字
6 thread.Name = "主線程";
7 //擷取目前線程的唯一辨別符
8 int id = thread.ManagedThreadId;
9 //擷取目前線程的狀态
10 ThreadState state= thread.ThreadState;
11 //擷取目前線程的優先級
12 ThreadPriority priority= thread.Priority;
13 string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
14 "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
15 state, priority);
16
17 Console.WriteLine(strMsg);
18
19 Console.ReadKey();
20 }
運作結果:
2.3 前台線程和背景線程
前台線程:隻有所有的前台線程都結束,應用程式才能結束。預設情況下建立的線程
都是前台線程
背景線程:隻要所有的前台線程結束,背景線程自動結束。通過Thread.IsBackground設定背景線程。必須在調用Start方法之前設定線程的類型,否則一旦線程運作,将無法改變其類型。
通過BeginXXX方法運作的線程都是背景線程。
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 //示範前台、背景線程
6 BackGroundTest background = new BackGroundTest(10);
7 //建立前台線程
8 Thread fThread = new Thread(new ThreadStart(background.RunLoop));
9 //給線程命名
10 fThread.Name = "前台線程";
11
12
13 BackGroundTest background1 = new BackGroundTest(20);
14 //建立背景線程
15 Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
16 bThread.Name = "背景線程";
17 //設定為背景線程
18 bThread.IsBackground = true;
19
20 //啟動線程
21 fThread.Start();
22 bThread.Start();
23 }
24 }
25
26 class BackGroundTest
27 {
28 private int Count;
29 public BackGroundTest(int count)
30 {
31 this.Count = count;
32 }
33 public void RunLoop()
34 {
35 //擷取目前線程的名稱
36 string threadName = Thread.CurrentThread.Name;
37 for (int i = 0; i < Count; i++)
38 {
39 Console.WriteLine("{0}計數:{1}",threadName,i.ToString());
40 //線程休眠500毫秒
41 Thread.Sleep(1000);
42 }
43 Console.WriteLine("{0}完成計數",threadName);
44
45 }
46 }
運作結果:前台線程執行完,背景線程未執行完,程式自動結束。
把bThread.IsBackground = true注釋掉,運作結果:主線程執行完畢後(Main函數),程式并未結束,而是要等所有的前台線程結束以後才會結束。
背景線程一般用于處理不重要的事情,應用程式結束時,背景線程是否執行完成對整個應用程式沒有影響。如果要執行的事情很重要,需要将線程設定為前台線程。
2.4 線程同步
所謂同步:是指在某一時刻隻有一個線程可以通路變量。
如果不能確定對變量的通路是同步的,就會産生錯誤。
c#為同步通路變量提供了一個非常簡單的方式,即使用c#語言的關鍵字Lock,它可以把一段代碼定義為互斥段,互斥段在一個時刻内隻允許一個線程進入執行,
而其他線程必須等待。在c#中,關鍵字Lock定義如下:
Lock(expression)
{
statement_block
}
expression代表你希望跟蹤的對象:
如果你想保護一個類的執行個體,一般地,你可以使用this;
如果你想保護一個靜态變量(如互斥代碼段在一個靜态方法内部),一般使用類名就可以了
而statement_block就算互斥段的代碼,這段代碼在一個時刻内隻可能被一個線程執行。
以書店賣書為例
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 BookShop book = new BookShop();
6 //建立兩個線程同時通路Sale方法
7 Thread t1 = new Thread(new ThreadStart(book.Sale));
8 Thread t2 = new Thread(new ThreadStart(book.Sale));
9 //啟動線程
10 t1.Start();
11 t2.Start();
12 Console.ReadKey();
13 }
14 }
15
16
17
18 class BookShop
19 {
20 //剩餘圖書數量
21 public int num = 1;
22 public void Sale()
23 {
24 int tmp = num;
25 if (tmp > 0)//判斷是否有書,如果有就可以賣
26 {
27 Thread.Sleep(1000);
28 num -= 1;
29 Console.WriteLine("售出一本圖書,還剩餘{0}本", num);
30 }
31 else
32 {
33 Console.WriteLine("沒有了");
34 }
35 }
36 }
運作結果:
從運作結果可以看出,兩個線程同步通路共享資源,沒有考慮同步的問題,結果不正确。
考慮線程同步,改進後的代碼:
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 BookShop book = new BookShop();
6 //建立兩個線程同時通路Sale方法
7 Thread t1 = new Thread(new ThreadStart(book.Sale));
8 Thread t2 = new Thread(new ThreadStart(book.Sale));
9 //啟動線程
10 t1.Start();
11 t2.Start();
12 Console.ReadKey();
13 }
14 }
15
16
17
18 class BookShop
19 {
20 //剩餘圖書數量
21 public int num = 1;
22 public void Sale()
23 {
24 //使用lock關鍵字解決線程同步問題
25 lock (this)
26 {
27 int tmp = num;
28 if (tmp > 0)//判斷是否有書,如果有就可以賣
29 {
30 Thread.Sleep(1000);
31 num -= 1;
32 Console.WriteLine("售出一本圖書,還剩餘{0}本", num);
33 }
34 else
35 {
36 Console.WriteLine("沒有了");
37 }
38 }
39 }
40 }
運作結果:
2.5 跨線程通路
點選“測試”,建立一個線程,從0循環到10000給文本框指派,代碼如下:
1 private void btn_Test_Click(object sender, EventArgs e)
2 {
3 //建立一個線程去執行這個方法:建立的線程預設是前台線程
4 Thread thread = new Thread(new ThreadStart(Test));
5 //Start方法标記這個線程就緒了,可以随時被執行,具體什麼時候執行這個線程,由CPU決定
6 //将線程設定為背景線程
7 thread.IsBackground = true;
8 thread.Start();
9 }
10
11 private void Test()
12 {
13 for (int i = 0; i < 10000; i++)
14 {
15 this.textBox1.Text = i.ToString();
16 }
17 }
運作結果:
産生錯誤的原因:textBox1是由主線程建立的,thread線程是另外建立的一個線程,在.NET上執行的是托管代碼,C#強制要求這些代碼必須是線程安全的,即不允許跨線程通路Windows窗體的控件。
解決方案:
1、在窗體的加載事件中,将C#内置控件(Control)類的CheckForIllegalCrossThreadCalls屬性設定為false,屏蔽掉C#編譯器對跨線程調用的檢查。
private void Form1_Load(object sender, EventArgs e)
{
//取消跨線程的通路
Control.CheckForIllegalCrossThreadCalls = false;
}
使用上述的方法雖然可以保證程式正常運作并實作應用的功能,但是在實際的軟體開發中,做如此設定是不安全的(不符合.NET的安全規範),在産品軟體的開發中,此類情況是不允許的。
如果要在遵守.NET安全标準的前提下,實作從一個線程成功地通路另一個線程建立的空間,要使用C#的方法回調機制。
2、使用回調函數
回調實作的一般過程:
C#的方法回調機制,也是建立在委托基礎上的,下面給出它的典型實作過程。
(1)、定義、聲明回調。
1 //定義回調
2 private delegate void DoSomeCallBack(Type para);
3 //聲明回調
4 DoSomeCallBack doSomaCallBack;
可以看出,這裡定義聲明的“回調”(doSomaCallBack)其實就是一個委托。
(2)、初始化回調方法。
doSomeCallBack=new DoSomeCallBack(DoSomeMethod);
所謂“初始化回調方法”實際上就是執行個體化剛剛定義了的委托,這裡作為參數的DoSomeMethod稱為“回調方法”,它封裝了對另一個線程中目标對象(窗體控件或其他類)的操作代碼。
(3)、觸發對象動作
Opt obj.Invoke(doSomeCallBack,arg);
其中Opt obj為目标操作對象,在此假設它是某控件,故調用其Invoke方法。Invoke方法簽名為:
object Control.Invoke(Delegate method,params object[] args);
它的第一個參數為委托類型,可見“觸發對象動作”的本質,就是把委托doSomeCallBack作為參數傳遞給控件的Invoke方法,這與委托的使用方式是一模一樣的。
最終作用于對象Opt obj的代碼是置于回調方法體DoSomeMethod()中的,如下所示:
private void DoSomeMethod(type para)
{
//方法體
Opt obj.someMethod(para);
}
如果不用回調,而是直接在程式中使用“Opt obj.someMethod(para);”,則當對象Opt obj不在本線程(跨線程通路)時就會發生上面所示的錯誤。
從以上回調實作的一般過程可知:C#的回調機制,實質上是委托的一種應用。在C#網絡程式設計中,回調的應用是非常普遍的,有了方法回調,就可以在.NET上寫出線程安全的代碼了。
使用方法回調,實作給文本框指派:
1 namespace MultiThreadDemo
2 {
3 public partial class Form1 : Form
4 {
5 public Form1()
6 {
7 InitializeComponent();
8 }
9
10 //定義回調
11 private delegate void setTextValueCallBack(int value);
12 //聲明回調
13 private setTextValueCallBack setCallBack;
14
15 private void btn_Test_Click(object sender, EventArgs e)
16 {
17 //執行個體化回調
18 setCallBack = new setTextValueCallBack(SetValue);
19 //建立一個線程去執行這個方法:建立的線程預設是前台線程
20 Thread thread = new Thread(new ThreadStart(Test));
21 //Start方法标記這個線程就緒了,可以随時被執行,具體什麼時候執行這個線程,由CPU決定
22 //将線程設定為背景線程
23 thread.IsBackground = true;
24 thread.Start();
25 }
26
27 private void Test()
28 {
29 for (int i = 0; i < 10000; i++)
30 {
31 //使用回調
32 textBox1.Invoke(setCallBack, i);
33 }
34 }
35
36 /// <summary>
37 /// 定義回調使用的方法
38 /// </summary>
39 /// <param name="value"></param>
40 private void SetValue(int value)
41 {
42 this.textBox1.Text = value.ToString();
43 }
44 }
45 }
2.6 終止線程
若想終止正在運作的線程,可以使用Abort()方法。
三、同步和異步
同步和異步是對方法執行順序的描述。
同步:等待上一行完成計算之後,才會進入下一行。
例如:請同僚吃飯,同僚說很忙,然後就等着同僚忙完,然後一起去吃飯。
異步:不會等待方法的完成,會直接進入下一行,是非阻塞的。
例如:請同僚吃飯,同僚說很忙,那同僚先忙,自己去吃飯,同僚忙完了他自己去吃飯。
下面通過一個例子講解同步和異步的差別
1、建立一個winform程式,上面有兩個按鈕,一個同步方法、一個異步方法,在屬性裡面把輸出類型改成控制台應用程式,這樣可以看到輸出結果,代碼如下:
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using System.Windows.Forms;
11
12 namespace MyAsyncThreadDemo
13 {
14 public partial class Form1 : Form
15 {
16 public Form1()
17 {
18 InitializeComponent();
19 }
20
21 /// <summary>
22 /// 異步方法
23 /// </summary>
24 /// <param name="sender"></param>
25 /// <param name="e"></param>
26 private void btnAsync_Click(object sender, EventArgs e)
27 {
28 Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
29 Action<string> action = this.DoSomethingLong;
30 // 調用委托(同步調用)
31 action.Invoke("btnAsync_Click_1");
32 // 異步調用委托
33 action.BeginInvoke("btnAsync_Click_2",null,null);
34 Console.WriteLine($"***************btnAsync_Click End {Thread.CurrentThread.ManagedThreadId}");
35 }
36
37 /// <summary>
38 /// 同步方法
39 /// </summary>
40 /// <param name="sender"></param>
41 /// <param name="e"></param>
42 private void btnSync_Click(object sender, EventArgs e)
43 {
44 Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
45 int j = 3;
46 int k = 5;
47 int m = j + k;
48 for (int i = 0; i < 5; i++)
49 {
50 string name = string.Format($"btnSync_Click_{i}");
51 this.DoSomethingLong(name);
52 }
53 }
54
55
56 private void DoSomethingLong(string name)
57 {
58 Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
59 long lResult = 0;
60 for (int i = 0; i < 1000000000; i++)
61 {
62 lResult += i;
63 }
64 Console.WriteLine($"****************DoSomethingLong {name} End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
65 }
66 }
67 }
2、啟動程式,點選同步,結果如下:
從上面的截圖中能夠很清晰的看出:同步方法是等待上一行代碼執行完畢之後才會執行下一行代碼。
點選異步,結果如下:
從上面的截圖中看出:當執行到action.BeginInvoke("btnAsync_Click_2",null,null);這句代碼的時候,程式并沒有等待這段代碼執行完就執行了下面的End,沒有阻塞程式的執行。
在剛才的測試中,如果點選同步,這時winform界面不能拖到,界面卡住了,是因為主線程(即UI線程)在忙于計算。
點選異步的時候,界面不會卡住,這是因為主線程已經結束,計算任務交給子線程去做。
在仔細檢查上面兩個截圖,可以看出異步的執行速度比同步執行速度要快。同步方法執行完将近16秒,異步方法執行完将近6秒。
在看下面的一個例子,修改異步的方法,也和同步方法一樣執行循環,修改後的代碼如下:
1 private void btnAsync_Click(object sender, EventArgs e)
2 {
3 Console.WriteLine($"***************btnAsync_Click Start
{Thread.CurrentThread.ManagedThreadId}");
4 //Action<string> action = this.DoSomethingLong;
5 調用委托(同步調用)
6 //action.Invoke("btnAsync_Click_1");
7 異步調用委托
8 //action.BeginInvoke("btnAsync_Click_2",null,null);
9 Action<string> action = this.DoSomethingLong;
10 for (int i = 0; i < 5; i++)
11 {
12 //Thread.Sleep(5);
13 string name = string.Format($"btnAsync_Click_{i}");
14 action.BeginInvoke(name, null, null);
15 }
16 Console.WriteLine($"***************btnAsync_Click End
{Thread.CurrentThread.ManagedThreadId}");
17 }
結果如下:
從截圖中能夠看出:同步方法執行是有序的,異步方法執行是無序的。異步方法無序包括啟動無序和結束無序。啟動無序是因為同一時刻向作業系統申請線程,作業系統收到申請以後,
傳回執行的順序是無序的,是以啟動是無序的。結束無序是因為雖然線程執行的是同樣的操作,但是每個線程的耗時是不同的,是以結束的時候不一定是先啟動的線程就先結束。
從上面同步方法中可以清晰的看出:btnSync_Click_0執行時間耗時不到3秒,而btnSync_Click_1執行時間耗時超過了3秒。可以想象體育比賽中的跑步,每位運動員聽到發令槍起跑的順序不同,
每位運動員花費的時間不同,最終到達終點的順序也不同。
總結一下同步方法和異步方法的差別:
1、同步方法由于主線程忙于計算,是以會卡住界面。
異步方法由于主線程執行完了,其他計算任務交給子線程去執行,是以不會卡住界面,使用者體驗性好。
2、同步方法由于隻有一個線程在計算,是以執行速度慢。
異步方法由多個線程并發運算,是以執行速度快,但并不是線性增長的(資源可能不夠)。多線程也不是越多越好,隻有多個獨立的任務同時運作,才能加快速度。
3、同步方法是有序的。
異步多線程是無序的:啟動無序,執行時間不确定,是以結束也是無序的。一定不要通過等待幾毫秒的形式來控制線程啟動/執行時間/結束。
四、回調
先來看看異步多線程無序的例子:
在界面上新增一個按鈕,實作代碼如下:
1 private void btnAsyncAdvanced_Click(object sender, EventArgs e)
2 {
3 Console.WriteLine($"****************btnAsyncAdvanced_Click Start
{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-
dd HH:mm:ss.fff")}***************");
4 Action<string> action = this.DoSomethingLong;
5 action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
6 // 需求:異步多線程執行完之後再列印出下面這句
7 Console.WriteLine($"到這裡計算已經完成了。
{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
8 Console.WriteLine($"****************btnAsyncAdvanced_Click End
{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-
dd HH:mm:ss.fff")}***************");
9 }
運作結果:
從上面的截圖中看出,最終的效果并不是我們想要的效果,而且列印輸出的還是主線程。
既然異步多線程是無序的,那我們有沒有什麼辦法可以解決無序的問題呢?辦法當然是有的,那就是使用回調,.NET架構已經幫我們實作了回調:
BeginInvoke的第二個參數就是一個回調,那麼AsyncCallback究竟是什麼呢?F12檢視AsyncCallback的定義:
發現AsyncCallback就是一個委托,參數類型是IAsyncResult,明白了AsyncCallback是什麼以後,将上面的代碼進行如下的改造:
1 private void btnAsyncAdvanced_Click(object sender, EventArgs e)
2 {
3 Console.WriteLine($"****************btnAsyncAdvanced_Click Start
{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-
MM-dd HH:mm:ss.fff")}***************");
4 Action<string> action = this.DoSomethingLong;
5 // 定義一個回調
6 AsyncCallback callback = p =>
7 {
8 Console.WriteLine($"到這裡計算已經完成了。
{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
9 };
10 // 回調作為參數
11 action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);
12 Console.WriteLine($"****************btnAsyncAdvanced_Click End
{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-
MM-dd HH:mm:ss.fff")}***************");
13 }
運作結果:
上面的截圖中可以看出,這就是我們想要的效果,而且列印是子線程輸出的,但是程式究竟是怎麼實作的呢?我們可以進行如下的猜想:
程式執行到BeginInvoke的時候,會申請一個基于線程池的線程,這個線程會完成委托的執行(在這裡就是執行DoSomethingLong()方法),在委托執行完以後,
這個線程又會去執行callback回調的委托,執行callback委托需要一個IAsyncResult類型的參數,這個IAsyncResult類型的參數是如何來的呢?滑鼠右鍵放到BeginInvoke上面,檢視傳回值:
發現BeginInvoke的傳回值就是IAsyncResult類型的。那麼這個傳回值是不是就是callback委托的參數呢?将代碼進行如下的修改:
1 private void btnAsyncAdvanced_Click(object sender, EventArgs e)
2 {
3 // 需求:異步多線程執行完之後再列印出下面這句
4 Console.WriteLine($"****************btnAsyncAdvanced_Click Start
{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-
MM-dd HH:mm:ss.fff")}***************");
5 Action<string> action = this.DoSomethingLong;
6 // 無序的
7 //action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
8
9 IAsyncResult asyncResult = null;
10 // 定義一個回調
11 AsyncCallback callback = p =>
12 {
13 // 比較兩個變量是否是同一個
14 Console.WriteLine(object.ReferenceEquals(p,asyncResult));
15 Console.WriteLine($"到這裡計算已經完成了。
{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
16 };
17 // 回調作為參數
18 asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);
19 Console.WriteLine($"****************btnAsyncAdvanced_Click End
{Thread.CurrentThread.ManagedThreadId.ToString("00")}
{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
20 }
結果:
這裡可以看出BeginInvoke的傳回值就是callback委托的參數。
現在我們可以使用回調解決異步多線程無序的問題了。
2、擷取委托異步調用的傳回值
使用EndInvoke可以擷取委托異步調用的傳回值,請看下面的例子:
1 private void btnAsyncReturnVlaue_Click(object sender, EventArgs e)
2 {
3 // 定義一個無參數、int類型傳回值的委托
4 Func<int> func = () =>
5 {
6 Thread.Sleep(2000);
7 return DateTime.Now.Day;
8 };
9 // 輸出委托同步調用的傳回值
10 Console.WriteLine($"func.Invoke()={func.Invoke()}");
11 // 委托的異步調用
12 IAsyncResult asyncResult = func.BeginInvoke(p =>
13 {
14 Console.WriteLine(p.AsyncState);
15 },"異步調用傳回值");
16 // 輸出委托異步調用的傳回值
17 Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}");
18 }
結果:
轉發:https://www.cnblogs.com/w6w6/p/10648921.html