c#多線程(一)——基礎概念和基本使用
一、多線程相關的基本概念
進程(Process):是系統中的一個基本概念。 一個正在運行的應用程序在操作系統中被視為一個進程,包含著一個運行程序所需要的資源,進程可以包括一個或多個線程 。進程之間是相對獨立的,一個進程無法訪問另一個進程的數據(除非利用分布式計算方式),一個進程運行的失敗也不會影響其他進程的運行,Windows系統就是利用進程把工作劃分為多個獨立的區域的。進程可以理解為一個程序的基本邊界。
線程(Thread):是 進程中的基本執行單元,是操作系統分配CPU時間的基本單位 ,在進程入口執行的第一個線程被視為這個進程的 主線程 。在.NET應用程序中,都是以Main()方法作為入口的,當調用此方法時系統就會自動創建一個主線程。
多線程能實現的基礎:
1、CPU運行速度太快,硬件處理速度跟不上,所以操作系統進行分時間片管理。這樣,宏觀角度來說是多線程並發 ,看起來是同一時刻執行了不同的操作。但是從微觀角度來講,同一時刻只能有一個線程在處理。
2、目前電腦都是多核多CPU的,一個CPU在同一時刻只能運行一個線程,但是 多個CPU在同一時刻就可以運行多個線程 。
多線程的優點:
可以同時完成多個任務;可以讓占用大量處理時間的任務或當前沒有進行處理的任務定期將處理時間讓給別的任務;可以隨時停止任務;可以設置每個任務的優先級以優化程序性能。
多線程的缺點:
1、 內存占用 線程也是程序,所以線程需要占用內存,線程越多,占用內存也越多。
2、 管理協調 多線程需要協調和管理,所以需要占用CPU時間以便跟蹤線程,線程太多會導致控制太復雜。
3、 資源共享 線程之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。
二、C#中的線程使用
1、基本使用
1、無參
①調用靜態方法
static void Main(string[] args) { //無參調用靜態方法 Thread thread1 = new Thread(Func1); thread1.Start(); } staticvoid Func1() { Console.WriteLine("這是靜態方法"); }
②調用實例方法
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ThreadTest test = new ThreadTest(); 6 //無參調用實例方法 7 Thread thread1 = new Thread(test.Func2); 8 thread1.Start(); 9 Console.ReadKey(); 10 } 11 } 12 13 class ThreadTest 14 { 15 public void Func2() 16 { 17 Console.WriteLine("這是實例方法"); 18 } 19 }
2、有參數時
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ThreadTest test = new ThreadTest(); 6 //有參調用實例方法,ParameterizedThreadStart是一個委托,input為object,返回值為void 7 Thread thread1 = new Thread(new ParameterizedThreadStart(test.Func1)); 8 thread1.Start("有參的實例方法"); 9 Console.ReadKey(); 10 } 11 } 12 class ThreadTest 13 { 14 public void Func1(object o) 15 { 16 Console.WriteLine(o); 17 } 18 }
2、常用的屬性和方法
屬性名稱 | 說明 |
---|---|
CurrentThread | 獲取當前正在運行的線程。 |
ExecutionContext | 獲取一個 ExecutionContext 對象,該對象包含有關當前線程的各種上下文的信息。 |
IsBackground | bool,指示某個線程是否為後臺線程。 |
IsThreadPoolThread | bool,指示線程是否屬於托管線程池。 |
ManagedThreadId | int,獲取當前托管線程的唯一標識符。 |
Name | string,獲取或設置線程的名稱。 |
Priority |
獲取或設置一個值,該值指示線程的調度優先級 。 Lowest<BelowNormal<Normal<AboveNormal<Highest |
ThreadState |
獲取一個值,該值包含當前線程的狀態。 Unstarted、Sleeping、Running |
方法名稱 | 說明 |
---|---|
Abort() | 終止本線程。 |
GetDomain() | 返回當前線程正在其中運行的當前域。 |
GetDomainId() | 返回當前線程正在其中運行的當前域Id。 |
Suspend() | 掛起當前線程,如果當前線程已屬於掛起狀態則此不起作用 |
Interrupt() | 中斷處於 Wait/Sleep/Join 線程狀態的線程。 |
Resume() | 繼續運行已掛起的線程。 |
Start() | 執行本線程。(不一定立即執行,只是標記為可以執行) |
Join() | 阻塞調用線程,直到某個線程終止時為止。 |
Sleep() | 把正在運行的線程掛起一段時間。 |
3、線程同步
所謂同步: 是指在某一時刻只有一個線程可以訪問變量 。
c#為同步訪問變量提供了一個非常簡單的方式,即使用c#語言的關鍵字Lock,它可以把一段代碼定義為互斥段,互斥段在一個時刻內只允許一個線程進入執行,實際上是Monitor.Enter(obj),Monitor.Exit(obj)的語法糖。在c#中,lock的用法如下:
lock (obj) { dosomething... }
obj代表你希望鎖定的對象,註意一下幾點:
1. lock不能鎖定空值 ,因為Null是不需要被釋放的。 2. 不能鎖定string類型 ,雖然它也是引用類型的。因為字符串類型被CLR“暫留”,這意味著整個程序中任何給定字符串都只有一個實例,具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。 3. 值類型不能被lock ,每次裝箱後的對象都不一樣 ,鎖定時會報錯 4 避免鎖定public類型 如果該實例可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的代碼也可能會鎖定該對象。這可能導致死鎖,即兩個或更多個線程等待釋放同一對象。
推薦使用 private static readonly類型的對象,readonly是為了避免lock的代碼塊中修改對象,造成對象改變後鎖失效。
以書店賣書為例
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 BookShop book = new BookShop(); 6 //創建兩個線程同時訪問Sale方法 7 Thread t1 = new Thread(book.Sale); 8 Thread t2 = new Thread(book.Sale); 9 //啟動線程 10 t1.Start(); 11 t2.Start(); 12 Console.ReadKey(); 13 } 14 } 15 class BookShop 16 { 17 //剩余圖書數量 18 public int num = 1; 19 private static readonly object locker = new object(); 20 public void Sale() 21 { 22 23 lock (locker) 24 { 25 int tmp = num; 26 if (tmp > 0)//判斷是否有書,如果有就可以賣 27 { 28 Thread.Sleep(1000); 29 num -= 1; 30 Console.WriteLine("售出一本圖書,還剩余{0}本", num); 31 } 32 else 33 { 34 Console.WriteLine("沒有了"); 35 } 36 } 37 } 38 }
代碼執行結果時:
如果不添加lock則執行的結果時:
4、跨線程訪問
例子:點擊測試按鈕,給文本框賦值
代碼如下:
1 private void myBtn_Click(object sender, EventArgs e) 2 { 3 Thread thread1 = new Thread(SetValue); 4 thread1.Start(); 5 6 } 7 private void SetValue() 8 { 9 for (int i = 0; i < 10000; i++) 10 { 11 this.myTxtBox.Text = i.ToString(); 12 } 13 }
執行代碼會出現如下錯誤:
出現該錯誤的原因是:myTxtBox是由主線程創建的,thread1線程是另外一個線程,在.NET上執行的是托管代碼, C#強制要求這些代碼必須是線程安全的,即不允許跨線程訪問Windows窗體的控件
解決的方法:
1 private delegate void setTextValueCallBack(int value); 2 //聲明回調 3 private setTextValueCallBack setCallBack; 4 private void myBtn_Click(object sender, EventArgs e) 5 { 6 //實例化回調 7 setCallBack = new setTextValueCallBack(SetValue); 8 //創建一個線程去執行這個方法:創建的線程默認是前臺線程 9 Thread thread = new Thread(Test); 10 //將線程設置為後臺線程 11 thread.IsBackground = true; 12 thread.Start(); 13 } 14 15 private void Test() 16 { 17 for (int i = 0; i < 10000; i++) 18 { 19 //使用回調 20 this.myTxtBox.Invoke(setCallBack, i); 21 } 22 } 23 24 /// <summary> 25 /// 定義回調使用的方法 26 /// </summary> 27 /// <param name="value"></param> 28 private void SetValue(int value) 29 { 30 this.myTxtBox.Text = value.ToString(); 31 }
invoke:在“擁有控件的基礎窗口句柄的線程” 即在本例的主線程上執行委托,這樣就不存在跨線程訪問了 ,因此還是線程安全的。
參考文章:
[1] https://www.cnblogs.com/dotnet261010/p/6159984.html
[2] https://www.cnblogs.com/wwj1992/p/5976096.html
c#多線程(一)——基礎概念和基本使用