您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“C#多線程怎么用”,內(nèi)容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“C#多線程怎么用”這篇文章吧。
首先打開任務(wù)管理器,查看當前運行的進程:
從任務(wù)管理器里面可以看到當前所有正在運行的進程。那么究竟什么是進程呢?
進程(Process)是Windows系統(tǒng)中的一個基本概念,它包含著一個運行程序所需要的資源。一個正在運行的應(yīng)用程序在操作系統(tǒng)中被視為一個進程,進程可以包括一個或多個線程。線程是操作系統(tǒng)分配處理器時間的基本單元,在進程中可以有多個線程同時執(zhí)行代碼。進程之間是相對獨立的,一個進程無法訪問另一個進程的數(shù)據(jù)(除非利用分布式計算方式),一個進程運行的失敗也不會影響其他進程的運行,Windows系統(tǒng)就是利用進程把工作劃分為多個獨立的區(qū)域的。進程可以理解為一個程序的基本邊界。是應(yīng)用程序的一個運行例程,是應(yīng)用程序的一次動態(tài)執(zhí)行過程。
在任務(wù)管理器里面查詢當前總共運行的線程數(shù):
線程(Thread)是進程中的基本執(zhí)行單元,是操作系統(tǒng)分配CPU時間的基本單位,一個進程可以包含若干個線程,在進程入口執(zhí)行的第一個線程被視為這個進程的主線程。在.NET應(yīng)用程序中,都是以Main()方法作為入口的,當調(diào)用此方法時系統(tǒng)就會自動創(chuàng)建一個主線程。線程主要是由CPU寄存器、調(diào)用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當前所執(zhí)行線程的狀態(tài),調(diào)用棧主要用于維護線程所調(diào)用到的內(nèi)存與數(shù)據(jù),TLS主要用于存放線程的狀態(tài)信息。
多線程的優(yōu)點:可以同時完成多個任務(wù);可以使程序的響應(yīng)速度更快;可以讓占用大量處理時間的任務(wù)或當前沒有進行處理的任務(wù)定期將處理時間讓給別的任務(wù);可以隨時停止任務(wù);可以設(shè)置每個任務(wù)的優(yōu)先級以優(yōu)化程序性能。
那么可能有人會問:為什么可以多線程執(zhí)行呢?總結(jié)起來有下面兩方面的原因:
1、CPU運行速度太快,硬件處理速度跟不上,所以操作系統(tǒng)進行分時間片管理。這樣,從宏觀角度來說是多線程并發(fā)的,因為CPU速度太快,察覺不到,看起來是同一時刻執(zhí)行了不同的操作。但是從微觀角度來講,同一時刻只能有一個線程在處理。
2、目前電腦都是多核多CPU的,一個CPU在同一時刻只能運行一個線程,但是多個CPU在同一時刻就可以運行多個線程。
然而,多線程雖然有很多優(yōu)點,但是也必須認識到多線程可能存在影響系統(tǒng)性能的不利方面,才能正確使用線程。不利方面主要有如下幾點:
(1)線程也是程序,所以線程需要占用內(nèi)存,線程越多,占用內(nèi)存也越多。
(2)多線程需要協(xié)調(diào)和管理,所以需要占用CPU時間以便跟蹤線程。
(3)線程之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。
(4)線程太多會導(dǎo)致控制太復(fù)雜,最終可能造成很多程序缺陷。
當啟動一個可執(zhí)行程序時,將創(chuàng)建一個主線程。在默認的情況下,C#程序具有一個線程,此線程執(zhí)行程序中以Main方法開始和結(jié)束的代碼,Main()方法直接或間接執(zhí)行的每一個命令都有默認線程(主線程)執(zhí)行,當Main()方法返回時此線程也將終止。
一個進程可以創(chuàng)建一個或多個線程以執(zhí)行與該進程關(guān)聯(lián)的部分程序代碼。在C#中,線程是使用Thread類處理的,該類在System.Threading命名空間中。使用Thread類創(chuàng)建線程時,只需要提供線程入口,線程入口告訴程序讓這個線程做什么。通過實例化一個Thread類的對象就可以創(chuàng)建一個線程。創(chuàng)建新的Thread對象時,將創(chuàng)建新的托管線程。Thread類接收一個ThreadStart委托或ParameterizedThreadStart委托的構(gòu)造函數(shù),該委托包裝了調(diào)用Start方法時由新線程調(diào)用的方法,示例代碼如下:
Thread thread=new Thread(new ThreadStart(method));//創(chuàng)建線程 thread.Start(); //啟動線程
上面代碼實例化了一個Thread對象,并指明將要調(diào)用的方法method(),然后啟動線程。ThreadStart委托中作為參數(shù)的方法不需要參數(shù),并且沒有返回值。ParameterizedThreadStart委托一個對象作為參數(shù),利用這個參數(shù)可以很方便地向線程傳遞參數(shù),示例代碼如下:
Thread thread=new Thread(new ParameterizedThreadStart(method));//創(chuàng)建線程 thread.Start(3); //啟動線程
創(chuàng)建多線程的步驟:
1、編寫線程所要執(zhí)行的方法
2、實例化Thread類,并傳入一個指向線程所要執(zhí)行方法的委托。(這時線程已經(jīng)產(chǎn)生,但還沒有運行)
3、調(diào)用Thread實例的Start方法,標記該線程可以被CPU執(zhí)行了,但具體執(zhí)行時間由CPU決定
Thread類是是控制線程的基礎(chǔ)類,位于System.Threading命名空間下,具有4個重載的構(gòu)造函數(shù):
名稱 | 說明 |
---|---|
Thread(ParameterizedThreadStart) | 初始化 Thread 類的新實例,指定允許對象在線程啟動時傳遞給線程的委托。要執(zhí)行的方法是有參的。 |
Thread(ParameterizedThreadStart,?Int32) | 初始化 Thread 類的新實例,指定允許對象在線程啟動時傳遞給線程的委托,并指定線程的最大堆棧大小 |
Thread(ThreadStart) | 初始化 Thread 類的新實例。要執(zhí)行的方法是無參的。 |
Thread(ThreadStart,?Int32) | 初始化 Thread 類的新實例,指定線程的最大堆棧大小。 |
ThreadStart是一個無參的、返回值為void的委托。委托定義如下:
public delegate void ThreadStart()
通過ThreadStart委托創(chuàng)建并運行一個線程:
class Program { static void Main(string[] args) { //創(chuàng)建無參的線程 Thread thread1 = new Thread(new ThreadStart(Thread1)); //調(diào)用Start方法執(zhí)行線程 thread1.Start(); Console.ReadKey(); } /// <summary> /// 創(chuàng)建無參的方法 /// </summary> static void Thread1() { Console.WriteLine("這是無參的方法"); } }
運行結(jié)果
除了可以運行靜態(tài)的方法,還可以運行實例方法
class Program { static void Main(string[] args) { //創(chuàng)建ThreadTest類的一個實例 ThreadTest test=new ThreadTest(); //調(diào)用test實例的MyThread方法 Thread thread = new Thread(new ThreadStart(test.MyThread)); //啟動線程 thread.Start(); Console.ReadKey(); } } class ThreadTest { public void MyThread() { Console.WriteLine("這是一個實例方法"); } }
運行結(jié)果:
如果為了簡單,也可以通過匿名委托或Lambda表達式來為Thread的構(gòu)造方法賦值
static void Main(string[] args) { //通過匿名委托創(chuàng)建 Thread thread1 = new Thread(delegate() { Console.WriteLine("我是通過匿名委托創(chuàng)建的線程"); }); thread1.Start(); //通過Lambda表達式創(chuàng)建 Thread thread2 = new Thread(() => Console.WriteLine("我是通過Lambda表達式創(chuàng)建的委托")); thread2.Start(); Console.ReadKey(); }
運行結(jié)果:
ParameterizedThreadStart是一個有參的、返回值為void的委托,定義如下:
public delegate void ParameterizedThreadStart(Object obj)
class Program { static void Main(string[] args) { //通過ParameterizedThreadStart創(chuàng)建線程 Thread thread = new Thread(new ParameterizedThreadStart(Thread1)); //給方法傳值 thread.Start("這是一個有參數(shù)的委托"); Console.ReadKey(); } /// <summary> /// 創(chuàng)建有參的方法 /// 注意:方法里面的參數(shù)類型必須是Object類型 /// </summary> /// <param name="obj"></param> static void Thread1(object obj) { Console.WriteLine(obj); } }
注意:ParameterizedThreadStart委托的參數(shù)類型必須是Object的。如果使用的是不帶參數(shù)的委托,不能使用帶參數(shù)的Start方法運行線程,否則系統(tǒng)會拋出異常。但使用帶參數(shù)的委托,可以使用thread.Start()來運行線程,這時所傳遞的參數(shù)值為null。
屬性名稱 | 說明 |
---|---|
CurrentContext | 獲取線程正在其中執(zhí)行的當前上下文。 |
CurrentThread | 獲取當前正在運行的線程。 |
ExecutionContext | 獲取一個 ExecutionContext 對象,該對象包含有關(guān)當前線程的各種上下文的信息。 |
IsAlive | 獲取一個值,該值指示當前線程的執(zhí)行狀態(tài)。 |
IsBackground | 獲取或設(shè)置一個值,該值指示某個線程是否為后臺線程。 |
IsThreadPoolThread | 獲取一個值,該值指示線程是否屬于托管線程池。 |
ManagedThreadId | 獲取當前托管線程的唯一標識符。 |
Name | 獲取或設(shè)置線程的名稱。 |
Priority | 獲取或設(shè)置一個值,該值指示線程的調(diào)度優(yōu)先級。 |
ThreadState | 獲取一個值,該值包含當前線程的狀態(tài)。 |
ManagedThreadId是確認線程的唯一標識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。而Name是一個可變值,在默認時候,Name為一個空值 Null,開發(fā)人員可以通過程序設(shè)置線程的名稱,但這只是一個輔助功能。
當線程之間爭奪CPU時間時,CPU按照線程的優(yōu)先級給予服務(wù)。高優(yōu)先級的線程可以完全阻止低優(yōu)先級的線程執(zhí)行。.NET為線程設(shè)置了Priority屬性來定義線程執(zhí)行的優(yōu)先級別,里面包含5個選項,其中Normal是默認值。除非系統(tǒng)有特殊要求,否則不應(yīng)該隨便設(shè)置線程的優(yōu)先級別。
成員名稱 | 說明 |
---|---|
Lowest | 可以將 Thread 安排在具有任何其他優(yōu)先級的線程之后。 |
BelowNormal | 可以將 Thread 安排在具有 Normal 優(yōu)先級的線程之后,在具有 Lowest 優(yōu)先級的線程之前。 |
Normal | 默認選擇。可以將 Thread 安排在具有 AboveNormal 優(yōu)先級的線程之后,在具有 BelowNormal 優(yōu)先級的線程之前。 |
AboveNormal | 可以將 Thread 安排在具有 Highest 優(yōu)先級的線程之后,在具有 Normal 優(yōu)先級的線程之前。 |
Highest | 可以將 Thread 安排在具有任何其他優(yōu)先級的線程之前。 |
通過ThreadState可以檢測線程是處于Unstarted、Sleeping、Running 等等狀態(tài),它比 IsAlive 屬性能提供更多的特定信息。
前面說過,一個應(yīng)用程序域中可能包括多個上下文,而通過CurrentContext可以獲取線程當前的上下文。
CurrentThread是最常用的一個屬性,它是用于獲取當前運行的線程。
Thread 中包括了多個方法來控制線程的創(chuàng)建、掛起、停止、銷毀,以后來的例子中會經(jīng)常使用。
方法名稱 | 說明 |
---|---|
Abort() | 終止本線程。 |
GetDomain() | 返回當前線程正在其中運行的當前域。 |
GetDomainId() | 返回當前線程正在其中運行的當前域Id。 |
Interrupt() | 中斷處于 WaitSleepJoin 線程狀態(tài)的線程。 |
Join() | 已重載。 阻塞調(diào)用線程,直到某個線程終止時為止。 |
Resume() | 繼續(xù)運行已掛起的線程。 |
Start() | 執(zhí)行本線程。 |
Suspend() | 掛起當前線程,如果當前線程已屬于掛起狀態(tài)則此不起作用 |
Sleep() | 把正在運行的線程掛起一段時間。 |
線程示例
static void Main(string[] args) { //獲取正在運行的線程 Thread thread = Thread.CurrentThread; //設(shè)置線程的名字 thread.Name = "主線程"; //獲取當前線程的唯一標識符 int id = thread.ManagedThreadId; //獲取當前線程的狀態(tài) ThreadState state= thread.ThreadState; //獲取當前線程的優(yōu)先級 ThreadPriority priority= thread.Priority; string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" + "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name, state, priority); Console.WriteLine(strMsg); Console.ReadKey(); }
運行結(jié)果:
前臺線程:只有所有的前臺線程都結(jié)束,應(yīng)用程序才能結(jié)束。默認情況下創(chuàng)建的線程都是前臺線程。
后臺線程:只要所有的前臺線程結(jié)束,后臺線程自動結(jié)束。通過Thread.IsBackground設(shè)置后臺線程。必須在調(diào)用Start方法之前設(shè)置線程的類型,否則一旦線程運行,將無法改變其類型。
通過BeginXXX方法運行的線程都是后臺線程。
class Program { static void Main(string[] args) { //演示前臺、后臺線程 BackGroundTest background = new BackGroundTest(10); //創(chuàng)建前臺線程 Thread fThread = new Thread(new ThreadStart(background.RunLoop)); //給線程命名 fThread.Name = "前臺線程"; BackGroundTest background1 = new BackGroundTest(20); //創(chuàng)建后臺線程 Thread bThread = new Thread(new ThreadStart(background1.RunLoop)); bThread.Name = "后臺線程"; //設(shè)置為后臺線程 bThread.IsBackground = true; //啟動線程 fThread.Start(); bThread.Start(); } } class BackGroundTest { private int Count; public BackGroundTest(int count) { this.Count = count; } public void RunLoop() { //獲取當前線程的名稱 string threadName = Thread.CurrentThread.Name; for (int i = 0; i < Count; i++) { Console.WriteLine("{0}計數(shù):{1}",threadName,i.ToString()); //線程休眠500毫秒 Thread.Sleep(1000); } Console.WriteLine("{0}完成計數(shù)",threadName); } }
運行結(jié)果:前臺線程執(zhí)行完,后臺線程未執(zhí)行完,程序自動結(jié)束。
把bThread.IsBackground = true注釋
掉,運行結(jié)果:主線程執(zhí)行完畢后(Main函數(shù)),程序并未結(jié)束,而是要等所有的前臺線程結(jié)束以后才會結(jié)束。
后臺線程一般用于處理不重要的事情,應(yīng)用程序結(jié)束時,后臺線程是否執(zhí)行完成對整個應(yīng)用程序沒有影響。如果要執(zhí)行的事情很重要,需要將線程設(shè)置為前臺線程。
所謂同步:是指在某一時刻只有一個線程可以訪問變量。
如果不能確保對變量的訪問是同步的,就會產(chǎn)生錯誤。
c#為同步訪問變量提供了一個非常簡單的方式,即使用c#語言的關(guān)鍵字Lock,它可以把一段代碼定義為互斥段,互斥段在一個時刻內(nèi)只允許一個線程進入執(zhí)行,而其他線程必須等待。在c#中,關(guān)鍵字Lock定義如下:
Lock(expression) { statement_block }
expression代表你希望跟蹤的對象:
如果你想保護一個類的實例,一般地,你可以使用this;
如果你想保護一個靜態(tài)變量(如互斥代碼段在一個靜態(tài)方法內(nèi)部),一般使用類名就可以了
而statement_block就算互斥段的代碼,這段代碼在一個時刻內(nèi)只可能被一個線程執(zhí)行。
以書店賣書為例
class Program { static void Main(string[] args) { BookShop book = new BookShop(); //創(chuàng)建兩個線程同時訪問Sale方法 Thread t1 = new Thread(new ThreadStart(book.Sale)); Thread t2 = new Thread(new ThreadStart(book.Sale)); //啟動線程 t1.Start(); t2.Start(); Console.ReadKey(); } } class BookShop { //剩余圖書數(shù)量 public int num = 1; public void Sale() { int tmp = num; if (tmp > 0)//判斷是否有書,如果有就可以賣 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本圖書,還剩余{0}本", num); } else { Console.WriteLine("沒有了"); } } }
運行結(jié)果:
從運行結(jié)果可以看出,兩個線程同步訪問共享資源,沒有考慮同步的問題,結(jié)果不正確。
考慮線程同步,改進后的代碼:
class Program { static void Main(string[] args) { BookShop book = new BookShop(); //創(chuàng)建兩個線程同時訪問Sale方法 Thread t1 = new Thread(new ThreadStart(book.Sale)); Thread t2 = new Thread(new ThreadStart(book.Sale)); //啟動線程 t1.Start(); t2.Start(); Console.ReadKey(); } } class BookShop { //剩余圖書數(shù)量 public int num = 1; public void Sale() { //使用lock關(guān)鍵字解決線程同步問題 lock (this) { int tmp = num; if (tmp > 0)//判斷是否有書,如果有就可以賣 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本圖書,還剩余{0}本", num); } else { Console.WriteLine("沒有了"); } } } }
運行結(jié)果:
點擊“測試”,創(chuàng)建一個線程,從0循環(huán)到10000給文本框賦值,代碼如下:
private void btn_Test_Click(object sender, EventArgs e) { //創(chuàng)建一個線程去執(zhí)行這個方法:創(chuàng)建的線程默認是前臺線程 Thread thread = new Thread(new ThreadStart(Test)); //Start方法標記這個線程就緒了,可以隨時被執(zhí)行,具體什么時候執(zhí)行這個線程,由CPU決定 //將線程設(shè)置為后臺線程 thread.IsBackground = true; thread.Start(); } private void Test() { for (int i = 0; i < 10000; i++) { this.textBox1.Text = i.ToString(); } }
運行結(jié)果:
產(chǎn)生錯誤的原因:textBox1是由主線程創(chuàng)建的,thread線程是另外創(chuàng)建的一個線程,在.NET上執(zhí)行的是托管代碼,C#強制要求這些代碼必須是線程安全的,即不允許跨線程訪問Windows窗體的控件。
解決方案:
1、在窗體的加載事件中,將C#內(nèi)置控件(Control)類的CheckForIllegalCrossThreadCalls屬性設(shè)置為false,屏蔽掉C#編譯器對跨線程調(diào)用的檢查。
private void Form1_Load(object sender, EventArgs e) { //取消跨線程的訪問 Control.CheckForIllegalCrossThreadCalls = false; }
使用上述的方法雖然可以保證程序正常運行并實現(xiàn)應(yīng)用的功能,但是在實際的軟件開發(fā)中,做如此設(shè)置是不安全的(不符合.NET的安全規(guī)范),在產(chǎn)品軟件的開發(fā)中,此類情況是不允許的。如果要在遵守.NET安全標準的前提下,實現(xiàn)從一個線程成功地訪問另一個線程創(chuàng)建的空間,要使用C#的方法回調(diào)機制。
2、使用回調(diào)函數(shù)
回調(diào)實現(xiàn)的一般過程:
C#的方法回調(diào)機制,也是建立在委托基礎(chǔ)上的,下面給出它的典型實現(xiàn)過程。
(1)、定義、聲明回調(diào)。
//定義回調(diào) private delegate void DoSomeCallBack(Type para); //聲明回調(diào) DoSomeCallBack doSomaCallBack;
可以看出,這里定義聲明的“回調(diào)”(doSomaCallBack)其實就是一個委托。
(2)、初始化回調(diào)方法。
doSomeCallBack=new DoSomeCallBack(DoSomeMethod);
所謂“初始化回調(diào)方法”實際上就是實例化剛剛定義了的委托,這里作為參數(shù)的DoSomeMethod稱為“回調(diào)方法”,它封裝了對另一個線程中目標對象(窗體控件或其他類)的操作代碼。
(3)、觸發(fā)對象動作
Opt obj.Invoke(doSomeCallBack,arg);
其中Opt obj為目標操作對象,在此假設(shè)它是某控件,故調(diào)用其Invoke方法。Invoke方法簽名為:
object Control.Invoke(Delegate method,params object[] args);
它的第一個參數(shù)為委托類型,可見“觸發(fā)對象動作”的本質(zhì),就是把委托doSomeCallBack作為參數(shù)傳遞給控件的Invoke方法,這與委托的使用方式是一模一樣的。
最終作用于對象Opt obj的代碼是置于回調(diào)方法體DoSomeMethod()中的,如下所示:
private void DoSomeMethod(type para) { //方法體 Opt obj.someMethod(para); }
如果不用回調(diào),而是直接在程序中使用“Opt obj.someMethod(para);”,則當對象Opt obj不在本線程(跨線程訪問)時就會發(fā)生上面所示的錯誤。
從以上回調(diào)實現(xiàn)的一般過程可知:C#的回調(diào)機制,實質(zhì)上是委托的一種應(yīng)用。在C#網(wǎng)絡(luò)編程中,回調(diào)的應(yīng)用是非常普遍的,有了方法回調(diào),就可以在.NET上寫出線程安全的代碼了。
使用方法回調(diào),實現(xiàn)給文本框賦值:
namespace MultiThreadDemo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } //定義回調(diào) private delegate void setTextValueCallBack(int value); //聲明回調(diào) private setTextValueCallBack setCallBack; private void btn_Test_Click(object sender, EventArgs e) { //實例化回調(diào) setCallBack = new setTextValueCallBack(SetValue); //創(chuàng)建一個線程去執(zhí)行這個方法:創(chuàng)建的線程默認是前臺線程 Thread thread = new Thread(new ThreadStart(Test)); //Start方法標記這個線程就緒了,可以隨時被執(zhí)行,具體什么時候執(zhí)行這個線程,由CPU決定 //將線程設(shè)置為后臺線程 thread.IsBackground = true; thread.Start(); } private void Test() { for (int i = 0; i < 10000; i++) { //使用回調(diào) textBox1.Invoke(setCallBack, i); } } /// <summary> /// 定義回調(diào)使用的方法 /// </summary> /// <param name="value"></param> private void SetValue(int value) { this.textBox1.Text = value.ToString(); } } }
若想終止正在運行的線程,可以使用Abort()方法。
同步和異步是對方法執(zhí)行順序的描述。
同步:等待上一行完成計算之后,才會進入下一行。
例如:請同事吃飯,同事說很忙,然后就等著同事忙完,然后一起去吃飯。
異步:不會等待方法的完成,會直接進入下一行,是非阻塞的。
例如:請同事吃飯,同事說很忙,那同事先忙,自己去吃飯,同事忙完了他自己去吃飯。
下面通過一個例子講解同步和異步的區(qū)別
1、新建一個winform程序,上面有兩個按鈕,一個同步方法、一個異步方法,在屬性里面把輸出類型改成控制臺應(yīng)用程序,這樣可以看到輸出結(jié)果,代碼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace MyAsyncThreadDemo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 異步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnAsync_Click(object sender, EventArgs e) { Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}"); Action<string> action = this.DoSomethingLong; // 調(diào)用委托(同步調(diào)用) action.Invoke("btnAsync_Click_1"); // 異步調(diào)用委托 action.BeginInvoke("btnAsync_Click_2",null,null); Console.WriteLine($"***************btnAsync_Click End {Thread.CurrentThread.ManagedThreadId}"); } /// <summary> /// 同步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSync_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); int j = 3; int k = 5; int m = j + k; for (int i = 0; i < 5; i++) { string name = string.Format($"btnSync_Click_{i}"); this.DoSomethingLong(name); } } private void DoSomethingLong(string name) { Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1000000000; i++) { lResult += i; } Console.WriteLine($"****************DoSomethingLong {name} End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); } } }
2、啟動程序,點擊同步,結(jié)果如下:
從上面的截圖中能夠很清晰的看出:同步方法是等待上一行代碼執(zhí)行完畢之后才會執(zhí)行下一行代碼。
點擊異步,結(jié)果如下:
從上面的截圖中看出:當執(zhí)行到action.BeginInvoke("btnAsync_Click_2",null,null);這句代碼的時候,程序并沒有等待這段代碼執(zhí)行完就執(zhí)行了下面的End,沒有阻塞程序的執(zhí)行。
在剛才的測試中,如果點擊同步,這時winform界面不能拖到,界面卡住了,是因為主線程(即UI線程)在忙于計算。
點擊異步的時候,界面不會卡住,這是因為主線程已經(jīng)結(jié)束,計算任務(wù)交給子線程去做。
在仔細檢查上面兩個截圖,可以看出異步的執(zhí)行速度比同步執(zhí)行速度要快。同步方法執(zhí)行完將近16秒,異步方法執(zhí)行完將近6秒。
在看下面的一個例子,修改異步的方法,也和同步方法一樣執(zhí)行循環(huán),修改后的代碼如下:
private void btnAsync_Click(object sender, EventArgs e) { Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}"); //Action<string> action = this.DoSomethingLong; //// 調(diào)用委托(同步調(diào)用) //action.Invoke("btnAsync_Click_1"); //// 異步調(diào)用委托 //action.BeginInvoke("btnAsync_Click_2",null,null); Action<string> action = this.DoSomethingLong; for (int i = 0; i < 5; i++) { //Thread.Sleep(5); string name = string.Format($"btnAsync_Click_{i}"); action.BeginInvoke(name, null, null); } Console.WriteLine($"***************btnAsync_Click End {Thread.CurrentThread.ManagedThreadId}"); }
結(jié)果如下:
從截圖中能夠看出:同步方法執(zhí)行是有序的,異步方法執(zhí)行是無序的。異步方法無序包括啟動無序和結(jié)束無序。啟動無序是因為同一時刻向操作系統(tǒng)申請線程,操作系統(tǒng)收到申請以后,返回執(zhí)行的順序是無序的,所以啟動是無序的。結(jié)束無序是因為雖然線程執(zhí)行的是同樣的操作,但是每個線程的耗時是不同的,所以結(jié)束的時候不一定是先啟動的線程就先結(jié)束。從上面同步方法中可以清晰的看出:btnSync_Click_0執(zhí)行時間耗時不到3秒,而btnSync_Click_1執(zhí)行時間耗時超過了3秒??梢韵胂篌w育比賽中的跑步,每位運動員聽到發(fā)令槍起跑的順序不同,每位運動員花費的時間不同,最終到達終點的順序也不同。
總結(jié)一下同步方法和異步方法的區(qū)別:
1、同步方法由于主線程忙于計算,所以會卡住界面。
異步方法由于主線程執(zhí)行完了,其他計算任務(wù)交給子線程去執(zhí)行,所以不會卡住界面,用戶體驗性好。
2、同步方法由于只有一個線程在計算,所以執(zhí)行速度慢。
異步方法由多個線程并發(fā)運算,所以執(zhí)行速度快,但并不是線性增長的(資源可能不夠)。多線程也不是越多越好,只有多個獨立的任務(wù)同時運行,才能加快速度。
3、同步方法是有序的。
異步多線程是無序的:啟動無序,執(zhí)行時間不確定,所以結(jié)束也是無序的。一定不要通過等待幾毫秒的形式來控制線程啟動/執(zhí)行時間/結(jié)束。
先來看看異步多線程無序的例子:
在界面上新增一個按鈕,實現(xiàn)代碼如下:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; action.BeginInvoke("btnAsyncAdvanced_Click", null, null); // 需求:異步多線程執(zhí)行完之后再打印出下面這句 Console.WriteLine($"到這里計算已經(jīng)完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。"); Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
運行結(jié)果:
從上面的截圖中看出,最終的效果并不是我們想要的效果,而且打印輸出的還是主線程。
既然異步多線程是無序的,那我們有沒有什么辦法可以解決無序的問題呢?辦法當然是有的,那就是使用回調(diào),.NET框架已經(jīng)幫我們實現(xiàn)了回調(diào):
BeginInvoke的第二個參數(shù)就是一個回調(diào),那么AsyncCallback究竟是什么呢?F12查看AsyncCallback的定義:
發(fā)現(xiàn)AsyncCallback就是一個委托,參數(shù)類型是IAsyncResult,明白了AsyncCallback是什么以后,將上面的代碼進行如下的改造:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; // 定義一個回調(diào) AsyncCallback callback = p => { Console.WriteLine($"到這里計算已經(jīng)完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。"); }; // 回調(diào)作為參數(shù) action.BeginInvoke("btnAsyncAdvanced_Click", callback, null); Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
運行結(jié)果:
上面的截圖中可以看出,這就是我們想要的效果,而且打印是子線程輸出的,但是程序究竟是怎么實現(xiàn)的呢?我們可以進行如下的猜想:
程序執(zhí)行到BeginInvoke的時候,會申請一個基于線程池的線程,這個線程會完成委托的執(zhí)行(在這里就是執(zhí)行DoSomethingLong()方法),在委托執(zhí)行完以后,這個線程又會去執(zhí)行callback回調(diào)的委托,執(zhí)行callback委托需要一個IAsyncResult類型的參數(shù),這個IAsyncResult類型的參數(shù)是如何來的呢?鼠標右鍵放到BeginInvoke上面,查看返回值:
發(fā)現(xiàn)BeginInvoke的返回值就是IAsyncResult類型的。那么這個返回值是不是就是callback委托的參數(shù)呢?將代碼進行如下的修改:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { // 需求:異步多線程執(zhí)行完之后再打印出下面這句 Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; // 無序的 //action.BeginInvoke("btnAsyncAdvanced_Click", null, null); IAsyncResult asyncResult = null; // 定義一個回調(diào) AsyncCallback callback = p => { // 比較兩個變量是否是同一個 Console.WriteLine(object.ReferenceEquals(p,asyncResult)); Console.WriteLine($"到這里計算已經(jīng)完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。"); }; // 回調(diào)作為參數(shù) asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null); Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
結(jié)果:
這里可以看出BeginInvoke的返回值就是callback委托的參數(shù)。
現(xiàn)在我們可以使用回調(diào)解決異步多線程無序的問題了。
2、獲取委托異步調(diào)用的返回值
使用EndInvoke可以獲取委托異步調(diào)用的返回值,請看下面的例子:
private void btnAsyncReturnVlaue_Click(object sender, EventArgs e) { // 定義一個無參數(shù)、int類型返回值的委托 Func<int> func = () => { Thread.Sleep(2000); return DateTime.Now.Day; }; // 輸出委托同步調(diào)用的返回值 Console.WriteLine($"func.Invoke()={func.Invoke()}"); // 委托的異步調(diào)用 IAsyncResult asyncResult = func.BeginInvoke(p => { Console.WriteLine(p.AsyncState); },"異步調(diào)用返回值"); // 輸出委托異步調(diào)用的返回值 Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}"); }
結(jié)果:
以上是“C#多線程怎么用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。