1. 概念介紹web
1.1 線程編程
線程是操做系統可以進行運算調度的最小單位,包含在進程之中,是進程中的實際運做單位。一條線程指的時進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。.NET 中System.Thread下能夠建立線程。c#
1.2 主線程windows
每一個windows進程都包含一個用作程序入口點的主線程。進程入口點(main方法)中建立的第一個線程稱爲主線程,調用main方法時,主線程被建立。 api
1.3 前臺線程安全
默認狀況下,Thread.Start()方法建立的線程都是前臺線程,屬性isBackground=true/false可以設置線程的線程是否爲後臺線程。前臺線程能阻止應用程序的終結,只有全部的前臺線程執行完畢,CLR(Common Language Runtime,公共語言運行庫)才能關閉應用程序。前臺線程屬於工做者線程。多線程
1.4 後臺線程併發
後臺線程經過isBackground設置,它不會影響應用程序的終結,當全部前臺線程執行完畢後,後臺線程不管是否執行完畢,都會被終結。通常後臺線程用來作可有可無的任務(如郵箱天氣更新等),後臺線程也屬於工做者線程。異步
2.多線程實現async
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種:
其中,參數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後的代碼。