第13章 C#中的多線程

13多線程 編程

13.1 線程概述 安全

計算機的操做系統多采用多任務和分時設計。多任務是指在一個操做系統中開以同時運行多個程序。例如,能夠在使用QQ聊天的同時聽音樂,即有多個獨立的任務,每一個任務對應一個進程,每一個進程也可產生多個線程。 網絡

13.1.1 進程 多線程

認識進程先從程序開始,程序(Program)是對數據描述與操做的代碼的集合,如Office中的Word,影音風暴等應用程序。 併發

進程(Process)是程序的一次動態執行過程,它對應了從代碼加載、執行至執行完畢的一個完整過程,這個過程也是進程自己從產生、發展直至消亡的過程。操做系統同時管理一個計算機系統中的多個進程,讓計算機系統中的多個進程輪流使用CPU資源,或者共享操做系統的其它資源。 異步

進程的特定是: 函數

  • 進程是系統運行程序的基本單位
  • 每個進程都有本身獨立的一塊內存空間、一組系統資源。
  • 每個進程的內部數據和狀態都是徹底獨立的。

在操做系統中能夠有多個進程,這些進程包括系統進程(由操做系統內部創建的進程)和用戶進程(由用戶程序創建的進程)。能夠從Windows任務管理器中查看已啓動的進程。圖13-1中標註了已啓動的Office Word進程。 性能

 

13-1 Windows任務管理器中的Office World進程 學習

13.1.2 線程 測試

線程是進程中執行運算的最小單位,可完成一個獨立的順序控制流程。每一個進程中,必須至少創建一個線程(這個線程稱爲主線程)來做爲這個程序運行的入口點。若是在一個進程中同時運行了多個線程,用來完成不一樣的工做,則稱之爲"多線程"。在操做系統將進程分紅多個線程後,實際上每一個任務是一個線程,多個線程共享相同的地址空間而且共同分享同一個進程,這些線程能夠在操做系統的管理下併發執行。從而大大提升了程序的運行效率。雖然線程的執行看似是多個線程同時執行,但實際上並不是如此。因爲單CPU的計算機中,CPU同時只能執行一條指令,所以,在僅有一個CPU的計算機上不可能同時執行多個任務。而操做系統爲了能提升程序的運行效率,將CPU的執行時間分紅多個時間片,分配給不一樣的線程,當一個時間片執行完畢後,該線程就可能讓出CPU使用權限交付給下一個時間片的其它線程執行。固然有可能相鄰的時間片分配給同一線程。之因此表面上看是多個線程同時執行,是由於不一樣線程之間切換的時間很是短,也許僅僅是幾毫秒,對普通人來講是難以感知的,這樣就看似多個線程在同時執行了。

爲了更好地瞭解線程,下面舉一個通俗的例子。張平是某互聯網公司的開發人員,有時工做比較單一,僅是開發項目,寫代碼,這就好像程序只執行一個線程。在單線程環境中,每一個程序執行的方式是隻有一個處理順序。但對開發人員來講,這只是一種理想的狀態,有時還須要兼顧其它工做,如修改Bug,參與技術交流、編寫項目文檔,和其它部門同時溝通項目需求等。項目經理通常會但願張平一天內多項工做都會有進展,可是在同一時間,張平只能作一個項目工做。這就好像程序開啓了多個線程,能夠處理多個不一樣任務,可是CPU在同一時刻,只能執行該進程的一個線程。

13.1.3 多線程的好處

多線程做爲一種多任務併發的工做方式,有着普遍的應用,合理地使用線程,將減小開發和維護的成本,甚至能夠改善複雜應用程序的性能。使用多線的優點以下。

  • 充分利用CPU的資源:執行單線程程序時,若程序發生阻塞,CPU可能會處於空閒狀態,這將形成計算機資源浪費,而使用多線程能夠在某個線程處理休眠或阻塞狀態時運行其它線程,這樣,大大提升了資源利用率。
  • 簡化編程模型:一個既長又複雜的進程能夠考慮分爲多個線程,成爲幾個獨立的運行部分,如使用時、分、秒來描述當前時間,若是寫成單線程程序可能須要多循環判斷,而若是使用多線程,時、分、秒各使用一個線程控制,每一個線程僅需實現簡單的流程,簡化了程序邏輯,這樣更有助於開發人員對程序的理解和維護。
  • 帶來良好的用戶體驗:因爲多個線程能夠交替執行,減小或避免了因程序阻塞或意外狀況形成的響應過慢現象,下降了用戶等待的概率。

13.2 C#中實現多線程

.NET中,Thread類將線程所必需的功能作了封裝,位於System.Threading命名空間。

13.2.1 主線程

在程序啓動時,一個線程馬上運行,該線程一般稱爲程序的主線程。程序的Main()方法是主線程的入口。每一個進程都至少有一個主線程。它是程序開始時就執行的。主線程的重要性體如今如下兩方面。

  • 它是產生其它子線程的線程
  • 一般它必須最後完成執行,由於它執行各類關閉動做。

儘管主線程在程序啓動時自動建立,但它能夠由一個Thread對象控制。下面的實例1顯示如何引用主線程。

示例1

     static void Main(string[] args)
    {
        //
判斷當前線程是否已經命名
        if (Thread.CurrentThread.Name == null)
        {
            Thread.CurrentThread.Name = "MainThread";
        }
        Console.WriteLine("
當前線程的名稱爲:"+Thread.CurrentThread.Name);
    }

在實例1中,Thread類的CurrentThread靜態屬性能夠得到當前主線程對象,線程對象的Name屬性表示線程的名稱,此屬性默認值爲null,且只可賦值一次。若是第二次賦值,則會引起異常,故在實例1中先判斷線程對象是否已經命名。程序運行結果如圖13-2所示。

 

 

13-2 顯示當前線程名稱

開發中,用戶編寫的線程通常是指除了主線程以外的其它線程。使用一個線程的過程能夠分爲如下三個步驟。

1)定義一個線程對象,同時指明這個線程所要執行的代碼,即指望完成的功能。

2)啓動線程。

3)終止線程。

13.2.2 建立線程

下面來編寫代碼,建立兩個線程,在線程中輸出1~100的整數。

實例2

static void Main(string[] args)
{
//
建立第一個線程對象
Thread thread1 = new Thread(DoWork);
thread1.Name = "myThread1";
//
建立第二個線程對象
Thread thread2 = new Thread(DoWork);
thread2.Name = "myThread2";
//
啓動線程
thread1.Start();
thread2.Start();
}
//
線程要調用的方法
static void DoWork()
{
for(int i=1;i<=100;i++)
{
Console.WriteLine(Thread.CurrentThread.Name+":"+i);
}
}

實例2中,經過Thread類的構造函數建立線程對象,Thread的構造函數形式的以下:

public Thread(ThreadStart start);

其參數start的類型ThreadStart是一個委託。表示要執行的方法。

Thread對象的Start()方法用來啓動線程。程序運行結果如圖13-3所示。

 

 

13-3 建立線程並啓動

經過圖13-3的結果能夠看出,兩個線程對象調用Start()方法後,各自都會輸出100之內的整數,互不影響,並行執行。在.NET中每一個線程都有本身獨立的內存棧,因此每一個線程的本地變量都相互獨立。但因爲CPU在一個時間點只能執行一個線程,所以多個線程是交替執行的,得到CPU時間片的線程即刻執行,當前時間片執行完畢後,CPU就會執行得到下一個時間片的線程。分配的時間片長度不是徹底一致的,可多可少,所以每次運行的結果有所不一樣,總之,是輪換交替執行的。

13.3 線程的狀態

任何線程通常都具備五種狀態,即建立、就緒、運行、阻塞、死亡狀態。線程狀態的轉移與方法之間的關係如圖13-4所示。

 

 

13-4 線程狀態的轉移與方法之間的關係

  • 建立狀態
    在程序中建立一個線程對象後,新的線程對象就處於建立狀態,此時,他已經獲取了相應的資源,但尚未處於可運行的狀態,這時能夠設置
    Thread對象的屬性,如線程名稱、優先級等。
  • 就緒狀態
    線程建立以後,就能夠經過調用
    Start()方法啓動線程,即進入就緒狀態。此時,線程將進入線程隊列排隊,等待CPU資源,這代表它已經具有了運行條件,在未得到CPU資源時,仍不能真正執行。舉例來講,去醫院看病,某主任的專家號天天只有20個,掛上號的患者還需在分診處等待叫號。這裏每一個掛到專家號的患者能夠當作一個就緒狀態的線程。
  • 運行狀態
    當就緒狀態的線程得到
    CPU資源時,便可轉入運行狀態。對只有一個CPU的計算機而言,任什麼時候刻只能有一個處於運行狀態的線程佔用CPU,即得到CPU資源。延續上面醫院看病的例子,被叫到的患者才能真正就診,而每一個主任專家在一個時刻只能爲一個患者看病。
  • 阻塞狀態
    一個正在運行的線程因某種緣由不能繼續運行時,進入阻塞狀態。阻塞狀態是一種"不可運行"的狀態,而處於這種狀態的線程在獲得一個特定的事件以後會轉回可運行狀態。舉例來講,輪到小張看病了,醫生爲查明緣由要求他去作個化驗,醫生獲得化驗結果後才能繼續診斷,若是把醫生給小張看病看做一個線程,則該線程此時即處於阻塞狀態。
    可能使線程暫停執行的條件以下。
    • 因爲線程優先級比較低,所以它不能得到CPU資源。
    • 使用Sleep()方法使線程休眠。
    • 經過調用Wait()方法,使線程等待。
    • 經過調用Yield()方法,線程顯示出讓CPU控制權。
    • 線程因爲等待一個文件,I/O事件被阻塞。
  • 死亡狀態
    一個線程運行完畢,線程則進入死亡狀態。處於死亡狀態的線程不具備繼續運行的能力。

13.4 線程的調度
在單CPU的計算機中,一個時刻只有一個線程運行,所謂多線程的併發運行,實際上是指從宏觀上看,各個線程輪流得到CPU資源的使用權,分別執行各自的任務。.NET Framework能夠對線程進行調度。線程調度是指按照特定機制爲多個線程分配CPU的使用權。
13.4.1
線程的優先級
當同一時刻有一個或多個線程處於運行狀態時,它們須要排隊等待CPU資源,每一個線程會自動得到一個線程的優先級(Priority),優先級的高低反映線程的重要或緊急程度。那麼此刻,通常狀況下優先級高的線程得到CPU資源的機率較大,但這個結果不是絕對的,線程優先級調度是一個複雜的過程。
Thread
對象能夠經過Priority屬性(枚舉類型)設置線程的優先級,優先級從低到高的五個取值爲LowestBelowNormalNormalAboveNormalHighest
咱們對實例2中的代碼進行修改,分別設置連個線程的優先級,如實例3所示。
實例3
static void Main(string[] args)
{
//
建立第一個線程對象
Thread thread1 = new Thread(DoWork);
thread1.Name = "myThread1";
//
建立第二個線程對象
Thread thread2 = new Thread(DoWork);
thread2.Name = "myThread2";
//
設置線程的優先級
thread1.Priority = ThreadPriority.Highest;
thread2.Priority = ThreadPriority.Lowest;
//
啓動線程
thread1.Start();
thread2.Start();
}
//
線程要調用的方法
static void DoWork()
{
for(int i=1;i<=100;i++)
{
Console.WriteLine(Thread.CurrentThread.Name+":"+i);
}
}
運行結果如圖13-5所示。

 

 

13-5 線程的優先級

從圖13-5中看出,優先級高的thread1對象優先執行完成。
13.4.2 線程的休眠
在程序中容許一個線程進行暫時休眠,直接使用
Thread.Sleep()方法便可實現線程的休眠。Sleep()方法的定義語法以下。
public static void Sleep(int millisecondsTimeout)
參數表示休眠時長,單位爲毫秒。線程由運行中的狀態進入不可運行狀態,睡眠時間事後線程會再次進入可運行狀態。
實例
4模擬主線程休眠五秒後開始執行。
實例
4
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Wait");
WaitBySec(5); //
讓主線程等待5秒再執行
Console.WriteLine("Start");
}

public static void WaitBySec(int s)
{
for(int i=0;i<s;i++)
{
Console.WriteLine(i+1+"
");
Thread.Sleep(1000); //
睡眠1
}
}
}
運行結果如圖13-6所示。

 

 

13-6 線程的休眠

13.4.3 線程的強制運行

Join()方法使當前線程暫停執行,等待調用該方法的線程結束後再繼續執行本線程。

下面經過實例來具體看一下Join()方法的應用。實例5爲使用Join()方法阻塞線程的案例。

實例5

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

class Program

{

static void Main(string[] args)

{

Console.WriteLine("*********線程強制執行**********");

//建立子線程並啓動

Thread temp = new Thread(DoWork);

temp.Name = "MyThread1";

temp.Start();

for(int i=0;i<20;i++)

{

if (i == 5)

temp.Join(); //阻塞主線程,子線程強制執行

Thread.Sleep(100);

Console.WriteLine("主線程運行:"+i);

}

Console.ReadKey();

}

static void DoWork()

{

for(int i=1;i<=10;i++)

{

Thread.Sleep(100); //增長線程交替執行的概率

Console.WriteLine(Thread.CurrentThread.Name+":"+i);

}

}

}

在示例5中,主線程的i的值爲5時,子線程調用Join()方法,阻塞主線程,子線程強制執行,直到子線程運行完畢後,主線程才能繼續執行。運行結果如圖13-7所示。

 

 

13-7 線程的強制執行

13.4.4 線程的禮讓

Yield()方法定義的語法以下。

public static bool Yield();

Yield()方法可暫停當前線程執行,容許其它具備相同優先級的線程得到運行機會,該線程仍處於就緒狀態,不轉爲阻塞狀態,此時,系統選擇其它相同或更高優先級線程執行。若無其它相同或更高優先級線程,則該線程繼續執行。返回值爲bool類型,若是操做系統轉而執行另外一個線程,則爲 true;不然爲 false。示例6實現了兩個線程之間的禮讓。

注意

使用Yield()的線程禮讓只是提供一種可能,可是不能保證必定會禮讓,由於禮讓的線程處於就緒狀態,還有可能被線程調度程序再次選中。

示例6

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

class Program

{

static void Main(string[] args)

{

Thread thA = new Thread(DoWork);

thA.Name = "線程A";

Thread thB = new Thread(DoWork);

thB.Name = "線程B";

thA.Start();

thB.Start();

Console.ReadKey();

}

static void DoWork()

{

for(int i=0;i<5;i++)

{

Console.WriteLine(Thread.CurrentThread.Name+"正在運行:"+i);

if (i==3)

{

Console.Write("線程禮讓:");

Thread.Yield();

}

}

}

}

運行結果如圖13-8所示

 

 

13-8 線程的禮讓

從程序的運行結果中能夠放發現,每當線程知足條件(i==3)時,建議當前線程暫停,而讓其它線程先執行,固然,這僅是提供一種可能。

13.5 線程的同步

13.5.1 多線程共享數據引起的問題

前面學習的線程都是獨立的,並且異步執行,也就是說每一個線程都包含了運行時所須要的數據或方法,而不須要外部資源或方法,也沒必要關心其它線程的狀態和行爲。可是常常有一些同時運行的線程須要共享數據,此時就須要考慮其它線程的狀態和行爲,不然不能保證程序運行結果的正確性。

舉個例子來講,咱們都熟悉每一年春運搶票的場景。之前須要親自到火車站或者售票點排隊買票,火車站每一個車次會按期定量發放車票,先到先得。如今互聯網愈來愈發達,能夠網上買票了,這樣又多了一個更加方便的購票渠道。如今,咱們使用多線程來模擬多人買票的過程。每一個人搶到票的機會均等,這樣,能夠把每個看做是一個線程,購票過程是線程的執行體,而每售出一張票,總票數就會減小,所以注意,預發售的火車票總數是多線程所共同操做的數據。假定如今有三我的搶十張票,實現思路以下。

1)定義類Site模擬售票網站。發放固定車次的車票,這裏爲簡化過程,設定預出售的車票總共十張,定義變量Count記錄剩餘票數,變量Num記錄當前售出第幾張票。

2實現售票方法SaleTicket()。網站將持續提供售票服務,所以,這裏使用到循環語句,Count做爲循環變量。在循環體中,當還有餘票時,購票過程分爲如下兩步。

第一步,修改數據,指當前售票序號(Num)以及剩餘票數(Count)

第二步,顯示售票信息。

在兩步之間,爲模擬網絡延遲,使用Sleep()方法設置線程休眠500毫秒。

3)定義測試類模擬多人搶票。建立三個線程,指定線程名稱,並啓動線程。

代碼如示例7所示。

示例7

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

public class Site

{

int Count = 10; //記錄剩餘票數

int Num = 0; //記錄買到第幾張票

//售票方法

public void SaleTicket()

{

while(true)

{

//沒有餘票時跳出循環

if (Count <= 0)

break;

//第一步:修改數據

Num++;

Count--;

Thread.Sleep(500); //模擬網絡延時

//第二步:顯示信息

Console.WriteLine("{0}搶到第{1}票,剩餘{2}票!",

Thread.CurrentThread.Name,Num,Count);

}

}

}

//測試類Main()方法

static void Main(string[] args)

{

Site site = new Site();

Thread person1 = new Thread(site.SaleTicket);

person1.Name = "王小毛";

Thread person2 = new Thread(site.SaleTicket);

person2.Name = "搶票代理";

Thread person3 = new Thread(site.SaleTicket);

person3.Name = "黃牛黨";

person1.Start();

person2.Start();

person3.Start();

}

示例7中,Main()方法中建立三個線程模擬三人開始搶票,而且啓動線程。運行結果如圖13-9所示。

 

 

13-9 多線程模擬網絡購票

從圖13-8中發現,最終顯示結果存在如下問題。

  • 不是從第一張票開始。
  • 存在多人搶到一張票的狀況。
  • 有些票號沒有被搶到。

這是因爲多線程併發執行操做同一共享資源時,將帶來數據不安全問題。例如,在上述案例中。三我的共同搶票,各自執行完第一步修改數據,此時等待網絡延時,再執行第二步顯示信息時,因爲前面修改了三次數據後Count值爲7Num值爲3,最終顯示三我的都搶到了第三張票。這固然僅僅是一種狀況。

要解決此類問題,就須要保證一我的在搶票過程未結束前,不容許其餘人同時搶票。這在開發中,就須要使用線程同步。

注意

示例7中提出的問題僅在多線程共享統一資源時產生,如三人共搶十張票;反之,在不存在資源共享時無需考慮此類問題。

13.5.2 線程同步的實現

當兩個或多個線程須要訪問同一資源時,須要以某種順序來確保該資源某一時刻只能被一個線程使用,這就稱爲線程同步。

C#中,咱們能夠經過lock語句實現線程同步。

資料

C#中實現線程的同步有幾種方法:lockMutexMonitorSemaphoreInterlockedReaderWriterLock等。同步策略也能夠分爲同步上下文、同步代碼區、手動同步幾種方式。你們能夠查閱MSDN自行學習。

lock 關鍵字將語句塊標記爲臨界區,方法是獲取給定對象的互斥鎖,執行語句,而後釋放該鎖。lock 確保當一個線程位於代碼的臨界區時,另外一個線程不進入臨界區。若是其餘線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。其語法以下:

lock(locker)
{
//
須要同步的代碼
}

提供給 lock 關鍵字的參數locker必須爲基於引用類型的對象,該對象用來定義鎖的範圍,能夠是任意類實例。

針對示例7的狀況,咱們用lock語句完成線程同步。如實例8所示

示例8

public class Site
{
int Count = 10; //
記錄剩餘票數
int Num = 0; //
記錄買到第幾張票
//
定義鎖定對象
private object locker = new object();

public void SaleTicket()
{
while(true)
{
lock (locker)
{
//
沒有餘票時跳出循環
if (Count <= 0)
break;
//
第一步:修改數據
Num++;
Count--;
Thread.Sleep(500); //
模擬網絡延時
//
第二步:顯示信息
Console.WriteLine("{0}
搶到第{1}票,剩餘{2}票!",
Thread.CurrentThread.Name, Num, Count);
}
}
}

}

當同步對共享資源的線程訪問時,請鎖定專用對象實例(例如,private object locker = new object();)或另外一個不太可能被代碼無關部分用做 lock 對象的實例。避免對不一樣的共享資源使用相同的 lock 對象實例,由於這可能致使死鎖或鎖爭用。具體而言,避免將如下對象用做 lock 對象:

  • this(調用方可能將其用做 lock)。
  • Type 實例(能夠經過 typeof 運算符或反射獲取)。
  • 字符串實例,包括字符串文本。

運行結果如圖13-10所示。

 

 

13-10 使用線程同步的網絡購票

13.5.3 線程安全的類型

若所在的進程中有多個線程在同時運行,而這些線程同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。

一個類在被多個線程訪問時,無論運行時環境執行這些線程有什麼樣的時序安排,它必須是以固定的、一致的順序執行。這樣的類型稱爲線程安全的類型。

ArrayList集合爲例,在向ArrayList對象添加一個元素的時候,由兩步來完成:

1)將索引值加1,即爲集合擴容,確保可裝入新元素。

2)在新增位置存放數據。

Add()方法是非線程安全的,若有線程A和線程B向同一個ArrayList對象中添加元素,兩個線程同時得到Count值爲5,以後同時執行完加1操做再賦值給Count,兩個線程爲同一個位置元素賦值,後一個覆蓋前一個,引起數據不安全問題。說明ArrayList是非線程安全的類型。

資料

ArrayList類能夠經過Synchronized()方法用來獲得一個線程安全的ArrayList對象,語法以下:

ArrayList arr = ArrayList.Synchronized(new ArrayList());

另外Hashtable也有相似的方法。

.Net 4,新增System.Collections.Concurrent 命名空間中提供多個線程安全集合類,這些類提供了不少有用的方法用於訪問集合中的元素,從而能夠避免使用傳統的鎖(lock)機制等方式來處理併發訪問集合。所以當有多個線程併發訪問集合時,應首先考慮使用這些類代替 System.Collections System.Collections.Generic 命名空間中的對應類型。具體以下。

1.ConcurrentQueue

表示線程安全的先進先出 (FIFO) 集合。Enqueue(T) 方法用於將對象添加到 ConcurrentQueue 的結尾處。

2.ConcurrentStack

表示線程安全的後進先出 (LIFO) 集合。 ConcurrentStack 提供了幾個主要操做:

  • Push 在頂部插入一個元素ConcurrentStack
  • TryPop 從頂部移除一個元素ConcurrentStack,或返回false若是不能刪除該項。
  • TryPeek 返回位於頂部的元素ConcurrentStack但不會刪除從ConcurrentStack
  • TryPopRangePushRange方法提供了有效推送和彈出的單個操做中的多個元素。

3.ConcurrentBag

表示對象的線程安全的無序集合。 Add(T)方法用於將對象添加到 ConcurrentBag 中。

4.ConcurrentDictionary

Dictionary相似,表示可由多個線程同時訪問的鍵/值對的線程安全集合。 經常使用方法以下:

  • TryAdd(TKey, TValue) :嘗試將指定的鍵和值添加到 ConcurrentDictionary 中。
  • TryGetValue(TKey, TValue) :嘗試從 ConcurrentDictionary 獲取與指定的鍵關聯的值。
  • TryRemove(TKey, TValue) :嘗試從 ConcurrentDictionary 中移除並返回具備指定鍵的值。
  • TryUpdate(TKey, TValue, TValue) :若是具備 key 的現有值等於 comparisonValue,則將與 key 關聯的值更新爲 newValue

經過上述提供的安全類,咱們能夠方便的併發訪問集合中的元素,而不須要之前的Synchronized方法或者lock(SyncRoot)等處理方式。

在多線程操做中,須要選擇線程安全的類型或經過同步操做避免多個線程共享資源時引起的問題,但線程的同步也會損失性能,所以,爲達到安全性和效率的平衡,可根據實際場景來選擇合適的類型。

相關文章
相關標籤/搜索