1、基本概念express
一、進程編程
首先打開任務管理器,查看當前運行的進程:c#
從任務管理器裏面能夠看到當前全部正在運行的進程。那麼究竟什麼是進程呢?安全
進程(Process)是Windows系統中的一個基本概念,它包含着一個運行程序所須要的資源。一個正在運行的應用程序在操做系統中被視爲一個進程,進程能夠包括一個或多個線程。線程是操做系統分配處理器時間的基本單元,在進程中能夠有多個線程同時執行代碼。進程之間是相對獨立的,一個進程沒法訪問另外一個進程的數據(除非利用分佈式計算方式),一個進程運行的失敗也不會影響其餘進程的運行,Windows系統就是利用進程把工做劃分爲多個獨立的區域的。進程能夠理解爲一個程序的基本邊界。是應用程序的一個運行例程,是應用程序的一次動態執行過程。網絡
2、線程多線程
在任務管理器裏面查詢當前總共運行的線程數:併發
線程(Thread)是進程中的基本執行單元,是操做系統分配CPU時間的基本單位,一個進程能夠包含若干個線程,在進程入口執行的第一個線程被視爲這個進程的主線程。在.NET應用程序中,都是以Main()方法做爲入口的,當調用此方法時系統就會自動建立一個主線程。線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當前所執行線程的狀態,調用棧主要用於維護線程所調用到的內存與數據,TLS主要用於存放線程的狀態信息。框架
2、多線程異步
多線程的優勢:能夠同時完成多個任務;可使程序的響應速度更快;可讓佔用大量處理時間的任務或當前沒有進行處理的任務按期將處理時間讓給別的任務;能夠隨時中止任務;能夠設置每一個任務的優先級以優化程序性能。async
那麼可能有人會問:爲何能夠多線程執行呢?總結起來有下面兩方面的緣由:
一、CPU運行速度太快,硬件處理速度跟不上,因此操做系統進行分時間片管理。這樣,從宏觀角度來講是多線程併發的,由於CPU速度太快,察覺不到,看起來是同一時刻執行了不一樣的操做。可是從微觀角度來說,同一時刻只能有一個線程在處理。
二、目前電腦都是多核多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); //啓動線程
建立多線程的步驟:
一、編寫線程所要執行的方法
二、實例化Thread類,並傳入一個指向線程所要執行方法的委託。(這時線程已經產生,但尚未運行)
三、調用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 //建立無參的線程 6 Thread thread1 = new Thread(new ThreadStart(Thread1)); 7 //調用Start方法執行線程 8 thread1.Start(); 9 10 Console.ReadKey(); 11 } 12 13 /// <summary> 14 /// 建立無參的方法 15 /// </summary> 16 static void Thread1() 17 { 18 Console.WriteLine("這是無參的方法"); 19 } 20 }
運行結果
除了能夠運行靜態的方法,還能夠運行實例方法
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窗體的控件。
解決方案:
一、在窗體的加載事件中,將C#內置控件(Control)類的CheckForIllegalCrossThreadCalls屬性設置爲false,屏蔽掉C#編譯器對跨線程調用的檢查。
private void Form1_Load(object sender, EventArgs e)
{
//取消跨線程的訪問
Control.CheckForIllegalCrossThreadCalls = false;
}
使用上述的方法雖然能夠保證程序正常運行並實現應用的功能,可是在實際的軟件開發中,作如此設置是不安全的(不符合.NET的安全規範),在產品軟件的開發中,此類狀況是不容許的。若是要在遵照.NET安全標準的前提下,實現從一個線程成功地訪問另外一個線程建立的空間,要使用C#的方法回調機制。
二、使用回調函數
回調實現的通常過程:
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()方法。
3、同步和異步
同步和異步是對方法執行順序的描述。
同步:等待上一行完成計算以後,纔會進入下一行。
例如:請同事吃飯,同事說很忙,而後就等着同事忙完,而後一塊兒去吃飯。
異步:不會等待方法的完成,會直接進入下一行,是非阻塞的。
例如:請同事吃飯,同事說很忙,那同事先忙,本身去吃飯,同事忙完了他本身去吃飯。
下面經過一個例子講解同步和異步的區別
一、新建一個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 }
二、啓動程序,點擊同步,結果以下:
從上面的截圖中可以很清晰的看出:同步方法是等待上一行代碼執行完畢以後纔會執行下一行代碼。
點擊異步,結果以下:
從上面的截圖中看出:當執行到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秒。能夠想象體育比賽中的跑步,每位運動員聽到發令槍起跑的順序不一樣,每位運動員花費的時間不一樣,最終到達終點的順序也不一樣。
總結一下同步方法和異步方法的區別:
一、同步方法因爲主線程忙於計算,因此會卡住界面。
異步方法因爲主線程執行完了,其餘計算任務交給子線程去執行,因此不會卡住界面,用戶體驗性好。
二、同步方法因爲只有一個線程在計算,因此執行速度慢。
異步方法由多個線程併發運算,因此執行速度快,但並非線性增加的(資源可能不夠)。多線程也不是越多越好,只有多個獨立的任務同時運行,才能加快速度。
三、同步方法是有序的。
異步多線程是無序的:啓動無序,執行時間不肯定,因此結束也是無序的。必定不要經過等待幾毫秒的形式來控制線程啓動/執行時間/結束。
4、回調
先來看看異步多線程無序的例子:
在界面上新增一個按鈕,實現代碼以下:
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委託的參數。
如今咱們可使用回調解決異步多線程無序的問題了。
二、獲取委託異步調用的返回值
使用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 }
結果: