1. 概念介紹
1.1 線程
線程是操作系統能夠進行運算調度的最小單位,包含在進程之中,是進程中的實際運作單位。一條線程指的時進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。.NET 中System.Thread下可以創建線程。
1.2 主線程
每個windows進程都包含一個用做程序入口點的主線程。進程入口點(main方法)中創建的第一個線程稱為主線程,調用main方法時,主線程被創建。
1.3 前台線程
默認情況下,Thread.Start()方法創建的線程都是前台線程,屬性isBackground=true/false能夠設置線程的線程是否為后台線程。前台線程能阻止應用程序的終結,只有所有的前台線程執行完畢,CLR(Common Language Runtime,公共語言運行庫)才能關閉應用程序。前台線程屬於工作者線程。
1.4 后台線程
后台線程通過isBackground設置,它不會影響應用程序的終結,當所有前台線程執行完畢后,后台線程無論是否執行完畢,都會被終結。一般后台線程用來做無關緊要的任務(如郵箱天氣更新等),后台線程也屬於工作者線程。
2.多線程實現
2.1 創建線程
在VS2019中,建立一個控制台應用程序,測試多線程服務。首先開啟2個線程workThread、printThread,分別實現數字計數、打印字母。代碼實現如下:
class Program { static void Main(string[] args) { //新建兩個線程,單獨運行 Thread workThread=new Thread(NumberCount); Thread printThread=new Thread(printNumber); workThread.Start(); printThread.Start(); Console.WriteLine("Hello World!"); } public static void NumberCount() { for (int i = 0; i < 10; i++) { Console.WriteLine("the number is {0}",i); } } public static void printNumber() { for (char i = 'A'; i < 'J'; i++) { Console.WriteLine("print character {0}", i); } } }
運行結果如下:
根據上述運行結果可以看出,主線程workThread和其他線程printThread運行時相互獨立,互不干擾。
2.2 線程基本屬性了解
static void Main(string[] args) { Thread th = Thread.CurrentThread;//訪問當前正在運行的線程 bool aliveRes=th.IsAlive;//當前線程的執行狀態 Console.WriteLine("IsAlive= {0}", aliveRes); th.IsBackground =false;//線程是否為后台線程 Console.WriteLine("IsBackground= {0}", th.IsBackground); bool isPool= th.IsThreadPoolThread;//當前線程是否屬於托管線程池 Console.WriteLine("isPool= {0}", isPool); int sysbol = th.ManagedThreadId;//獲取當前托管線程的唯一標識 Console.WriteLine("ManagedThreadId= {0}", sysbol); ThreadPriority pry=th.Priority;//設置線程調度優先級 Console.WriteLine("pry= {0}", pry); ThreadState state=th.ThreadState;//獲取當前線程狀態值 Console.WriteLine("state= {0}", state); th.Name = "main thread"; Console.WriteLine("this is {0}",th.Name); Console.ReadKey(); Console.WriteLine("Hello World!"); }
2.3 暫停線程
暫停線程通過調用sleep()方法實現,使得線程暫停但不占用計算機資源,實現代碼如下:
static void NumberCountCouldDelay() { for (int i = 0; i < 10; i++) { Console.WriteLine("the number is {0}", i); Thread.Sleep(TimeSpan.FromSeconds(1)); } } public static void printNumber() { for (char i = 'A'; i < 'J'; i++) { Console.WriteLine("print character {0}", i); Thread.Sleep(TimeSpan.FromSeconds(1)); } }
運行結果如下:
2.4 線程池
線程池是一種多線程處理形式,將任務添加到隊列,然后再創建線程后自動啟動這些任務。通過線程池創建的任務屬於后台任務,每個線程使用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。如果某個線程在托管代碼中空閑(如正在等待某個事件),則線程池將插入另一個輔助線程來使所有的處理器保持繁忙。
實現代碼及運行結果如下:
static void Main(string[] args) { Console.WriteLine("this is main thread: ThreadId={0}", Thread.CurrentThread.ManagedThreadId); ThreadPool.QueueUserWorkItem(printNumber); ThreadPool.QueueUserWorkItem(Go); Console.Read(); } public static void printNumber(object data) { for (char i = 'A'; i < 'D'; i++) { Console.WriteLine("print character {0}", i); Console.WriteLine("the print process threadId is {0}", Thread.CurrentThread.ManagedThreadId); } } public static void Go(object data) { Console.Write("this is another thread:ThreadId={0}",Thread.CurrentThread.ManagedThreadId); }
2.5 中止線程
線程中止采用abort方法,實現如下:
static void Main(string[] args) { ThreadStart childref = new ThreadStart(CallToChildThread); Console.WriteLine("In Main: Creating the child thread"); Thread childThread = new Thread(childref);//創建線程,擴展的Thread類 childThread.Start();//調用start()方法開始子線程的執行 //停止主線程一段時間 Thread.Sleep(2000); //現在中止子線程 Console.WriteLine("In Main: Abort the child thread"); childThread.Abort(); Console.WriteLine("Hello World!"); } public static void CallToChildThread() { try { //調用abort()方法銷毀線程 Console.WriteLine("Child thread start"); for (int counter=0; counter<=10;counter++) { Thread.Sleep(500); Console.WriteLine(counter); } Console.WriteLine("child thread abort"); } catch (ThreadAbortException e) { Console.WriteLine(e); throw; } finally { Console.WriteLine("Couldn't catch the Thread Exception"); } }
運行程序,出現如下錯誤:
經查找,發現.NET CORE平台不支持線程中止,在調用abort方法時會拋出ThreadAbortException異常。
2.5 跨線程訪問
新建一個winform窗體應用程序,實現點擊按鈕為textbox賦值,代碼如下:
private void Button1_Click(object sender, EventArgs e) { Thread thread=new Thread(test); thread.IsBackground = true; thread.Start(); Console.ReadLine(); } private void test() { for (int i = 0; i < 10; i++) { this.textBox1.Text = i.ToString(); } }
然而,運行時出現以下錯誤,內容顯示“線程間操作無效:從不是創建控件textBox1的線程訪問它”。是因為控件textBox1是由主線程創建的,thread作為另外一個線程,在.NET上執行的是托管代碼,c#強制要求代碼線程安全,不允許跨線程訪問。
上述問題解決辦法如下:(參考https://docs.microsoft.com/en-us/dotnet/framework/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls)
利用委托實現回調機制,回調過程如下:
(1)定義並聲明委托;
(2)初始化回調方法;
(3)定義回調使用的方法
public partial class UserControl1: UserControl { private delegate void SetTextboxCallBack(int value);//定義委托 private SetTextboxCallBack setCallBack; /// <summary> ///定義回調使用的方法 /// </summary> /// <param name="value"></param> private void SetText(int value) { textBox1.Text = value.ToString(); } public UserControl1() { InitializeComponent(); } private void Button1_Click(object sender, EventArgs e) { //初始化回調函數 setCallBack=new SetTextboxCallBack(SetText); //創建一個線程去執行這個回調函數要操作的方法 Thread thread = new Thread(test); thread.IsBackground = true; thread.Start(); Console.ReadLine(); } public void test() { for (int i = 0; i < 10; i++) { //控件上執行回調方法,觸發操作 textBox1.Invoke(setCallBack,i); } } }
運行結果如下:
2.5 多線程使用委托
線程的創建通過new Thread來實現,c#中該構造函數的實現有以下4種:
- public Thread(ThreadStart start){}
- public Thread(ParameterizedThreadStart start){}
- public Thread(ThreadStart start, int maxStackSize){}
- public Thread(ParameterizedThreadStart start, int maxStackSize){}
其中,參數ThreadStart定義為:
public delegate void ThreadStart();//無參數無返回值的委托
參數ParameterizedThreadStart 定義為:
public delegate void ParameterizedThreadStart(object obj);//有參數無返回值的委托
因此,對無返回值的委托實現如下。
2.5.1 無參數無返回值的委托
對於無參數無返回值的委托,是最簡單原始的使用方法。Thread thread= new Thread(new ThreadStart(()=>參數),其中參數為ThreadStart類型的委托。此類多線程代碼實現如下:
class Program { public delegate void ThreadStart();//新建一個無參數、無返回值的委托 static void Main(string[] args) { Thread thread=new Thread(new System.Threading.ThreadStart(NumberCount)); thread.IsBackground = true; thread.Start(); for (char i = 'A'; i < 'D'; i++) { Console.WriteLine("print character {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId); } Console.WriteLine("Hello World!"); } public static void NumberCount() { for (int i = 0; i < 3; i++) { Console.WriteLine("the number is {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId); } } }
2.5.2 有參數無返回值的委托
對於有參數無返回值的委托,實現代碼如下:
class Program { public delegate void ThreadStart(int i);//新建一個無參數、無返回值的委托 static void Main(string[] args) { Thread thread=new Thread(new ParameterizedThreadStart(NumberCount)); thread.IsBackground = true; thread.Start(3); for (char i = 'A'; i < 'D'; i++) { Console.WriteLine("print character {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId); } Console.WriteLine("Hello World!"); } public static void NumberCount(object i) { Console.WriteLine("the number is {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId); } }
運行結果為:
2.5.2 有參數有返回值的委托
對於有參數有返回值的委托,采用異步調用實現,如下所示:
2.6 異步實現
2.6.1 Task.Result
..NET中引入了System.Threading.Tasks,簡化了異步編程的方式,而不用直接和線程、線程池打交道。 System.Threading.Tasks中的類型被稱為任務並行庫(TPL),TPL使用CLR線程池(TPL創建的線程都是后台線程)自動將應用程序的工作動態分配到可用的CPU的中。
Result方法可以返回Task執行后的結果。但是在.NET CORE的webapi中使用result方法來獲取task的輸出值,會造成當前API線程阻塞等待到task執行完成后再繼續。以下代碼中,get方法中的線程id-57,調用一個新線程執行task后,等待TaskCaller()執行結果(threadid=59),待TaskCaller()方法執行完成后,原來的線程繼續之后之后的語句,輸出threadid=57
public class ValuesController:Controller { //async/await是用來進行異步調用的形式, [HttpGet("get")] public async Task<string> Get() { var info = string.Format("api執行線程{0}",Thread.CurrentThread.ManagedThreadId);//get方法中的線程 //調用新線程執行task任務 var infoTask = TaskCaller().Result;//調用result方法獲取task的值 var infoTaskFinished = string.Format("api執行線程(taks調用completed){0}", Thread.CurrentThread.ManagedThreadId); return string.Format("{0},{1},{2}", info, infoTask, infoTaskFinished); } public async Task<string> TaskCaller() { await Task.Delay(5000); return string.Format("task 執行線程{0}", Thread.CurrentThread.ManagedThreadId); } }
運行結果如下:
2.6.2 Async&Await
c#中async關鍵字用來指定方法,Lambda表達式或匿名方法自動以異步的方式來調用。async/await是用來進行異步調用的形式,內部采用線程池進行管理。如果使用await,在調用await tasjCall()是不會阻塞get方法的主線程,主線程會被釋放,新的線程執行完task后繼續執行await后的代碼,從而減少了線程切換的開銷,而之前的線程則空閑了。
public class ValuesAwaitController : Controller { [HttpGet("get")] public async Task<string> Get() { var info = string.Format("api執行線程{0}",Thread.CurrentThread.ManagedThreadId);//get方法中的線程 //調用新線程執行task任務 var infoTask = await TaskCaller();//使用await調用不會阻塞Get()中線程 var infoTaskFinished = string.Format("api執行線程(taks調用completed){0}", Thread.CurrentThread.ManagedThreadId); return string.Format("{0},{1},{2}", info, infoTask, infoTaskFinished); } public async Task<string> TaskCaller() { await Task.Delay(5000); return string.Format("task 執行線程{0}", Thread.CurrentThread.ManagedThreadId); } }
運行結果如下:
Task.result與await關鍵字具有類似的功能可以獲取到任務的返回值,但本質上Task.result會讓外層函數執行線程阻塞知道任務完成,而使用await外層函數線程不會阻塞,而是通過任務執行線程來執行await后的代碼。
- 默認創建的Thread是前台線程,創建的Task為后台線程;
- ThreadPool創建的線程都是后台線程;
- 任務並行庫(TPL)使用的是線程池計數;
- 調用async標記的方法,剛開始是同步執行,只有當執行到await標記的方法中的異步任務時,才會掛起。