一、多線程相關的基本概念
進程(Process):是系統中的一個基本概念。 一個正在運行的應用程序在操作系統中被視為一個進程,包含着一個運行程序所需要的資源,進程可以包括一個或多個線程 。進程之間是相對獨立的,一個進程無法訪問另一個進程的數據(除非利用分布式計算方式),一個進程運行的失敗也不會影響其他進程的運行,Windows系統就是利用進程把工作划分為多個獨立的區域的。進程可以理解為一個程序的基本邊界。
線程(Thread):是 進程中的基本執行單元,是操作系統分配CPU時間的基本單位 ,在進程入口執行的第一個線程被視為這個進程的 主線程 。
多線程能實現的基礎:
1、CPU運行速度太快,硬件處理速度跟不上,所以操作系統進行分時間片管理。這樣,宏觀角度來說是多線程並發 ,看起來是同一時刻執行了不同的操作。但是從微觀角度來講,同一時刻只能有一個線程在處理。
2、目前電腦都是多核多CPU的,一個CPU在同一時刻只能運行一個線程,但是 多個CPU在同一時刻就可以運行多個線程 。
多線程的優點:
可以同時完成多個任務;可以讓占用大量處理時間的任務或當前沒有進行處理的任務定期將處理時間讓給別的任務;可以隨時停止任務;可以設置每個任務的優先級以優化程序性能。
多線程的缺點:
1、 內存占用 線程也是程序,所以線程需要占用內存,線程越多,占用內存也越多(每個線程都需要開辟堆棧空間,多線程時有時需要切換時間片)。
2、 管理協調 多線程需要協調和管理,所以需要占用CPU時間以便跟蹤線程,線程太多會導致控制太復雜。
3、 資源共享 線程之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。
二、C#中的線程使用
2.1 基本使用
2.1.1 無參時
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.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.2 常用的屬性和方法
| 屬性名稱 | 說明 |
|---|---|
| CurrentThread | 獲取當前正在運行的線程。 |
| ExecutionContext | 獲取一個 ExecutionContext 對象,該對象包含有關當前線程的各種上下文的信息。 |
| IsBackground | bool,指示某個線程是否為后台線程。 |
| IsThreadPoolThread | bool,指示線程是否屬於托管線程池。 |
| ManagedThreadId | int,獲取當前托管線程的唯一標識符。 |
| Name | string,獲取或設置線程的名稱。 |
| Priority | 獲取或設置一個值,該值指示線程的調度優先級 。 Lowest<BelowNormal<Normal<AboveNormal<Highest |
| ThreadState | 獲取一個值,該值包含當前線程的狀態。 Unstarted、Sleeping、Running 等 |
| 方法名稱 | 說明 |
|---|---|
| GetDomain() | 返回當前線程正在其中運行的當前域。 |
| GetDomainId() | 返回當前線程正在其中運行的當前域Id。 |
| Start() | 執行本線程。(不一定立即執行,只是標記為可以執行) |
| Suspend() | 掛起當前線程,如果當前線程已屬於掛起狀態則此不起作用 |
| Resume() | 繼續運行已掛起的線程。 |
| Interrupt() | 中斷處於 WaitSleepJoin 線程狀態的線程。 |
| Abort() | 終結線程 |
| Join() | 阻塞調用線程,直到某個線程終止。 |
| Sleep() | 把正在運行的線程掛起一段時間。 |
看一個簡單的演示線程方法的栗子:

View Code
當點擊Start按鈕,線程啟動文本框會開始追加【第x次】字符串;點擊Suspend按鈕,線程掛起,停止追加字符串;點擊Resume按鈕會讓掛起線程繼續運行;點擊Interrupt按鈕彈出一個異常信息,線程狀態從WaitSleepJoin變為Running,線程繼續運行;點擊Abort按鈕會彈出一個異常信息並銷毀線程。
一點補充:Suspend、Resume方法已不建議使用,推薦使用AutoResetEvent和ManualResetEvent來控制線程的暫停和繼續,用法也十分簡單,這里不詳細介紹,有興趣的小伙伴可以研究下。
2.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則執行的結果時:

2.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窗體的控件
解決的方法:
public Form1()
{
InitializeComponent();
}
//點擊按鈕開啟一個新線程
private void myBtn_Click(object sender, EventArgs e)
{
Thread thread1 = new Thread(SetValues);
thread1.IsBackground = true;
thread1.Start();
}
//新線程給文本框賦值
private void SetValues()
{
Action<int> setVal = (i) => { this.myTxtBox.Text = i.ToString(); };
for (int i = 0; i < 10000; i++)
{
this.myTxtBox.Invoke(setVal, i);
}
}

Invoke:在“擁有控件的基礎窗口句柄的線程” 即在本例的主線程上執行委托,這樣就不存在跨線程訪問了 ,因此還是線程安全的。
參考文章:

