[C# 線程處理系列]專題四:線程同步

來源:互聯網
上載者:User

 

目錄:

一、線程同步概述

二、線程同步的使用

三 、總結

 

一、線程同步概述

 

前面的文章都是講建立多線程來實現讓我們能夠更好的響應應用程式,然而當我們建立了多個線程時,就存在多個線程同時訪問一個共用的資源的情況,在這種情況下,就需要我們用到線程同步,線程同步可以防止資料(共用資源)的損壞。

然而我們在設計應用程式還是要盡量避免使用線程同步, 因為線程同步會產生一些問題:

1. 它的使用比較繁瑣。因為我們要用額外的代碼把多個線程同時訪問的資料包圍起來,並擷取和釋放一個線程同步鎖,如果我們在一個代碼塊忘記擷取鎖,就有可能造成資料損毀。

2. 使用線程同步會影響效能,擷取和釋放一個鎖肯定是需要時間的吧,因為我們在決定哪個線程先擷取鎖時候, CPU必須進行協調,進行這些額外的工作就會對效能造成影響

3. 因為線程同步一次只允許一個線程訪問資源,這樣就會阻塞線程,阻塞線程會造成更多的線程被建立,這樣CPU就有可能要調度更多的線程,同樣也對效能造成了影響。

所以在實際的設計中還是要盡量避免使用線程同步,因此我們要避免使用一些共用資料,例如靜態欄位。

 

二、線程同步的使用

 

2.1 對於使用鎖效能的影響

上面已經說過使用鎖將會對效能產生影響, 下面通過比較使用鎖和不使用鎖時消耗的時間來說明這點

using System;using System.Diagnostics;using System.Threading;namespace InterlockedSample{    // 比較使用鎖和不使用鎖鎖消耗的時間    // 通過時間來說明使用鎖效能的影響    class Program    {        static void Main(string[] args)        {            int x = 0;            // 迭代次數為500萬            const int iterationNumber = 5000000;             // 不採用鎖的情況            // StartNew方法 對新的 Stopwatch 執行個體進行初始化,將已耗用時間屬性設定為零,然後開始測量已耗用時間。            Stopwatch sw = Stopwatch.StartNew();            for (int i = 0; i < iterationNumber; i++)            {                x++;            }            Console.WriteLine("Use the all time is :{0} ms", sw.ElapsedMilliseconds);            sw.Restart();            // 使用鎖的情況            for (int i = 0; i < iterationNumber; i++)            {                Interlocked.Increment(ref x);            }            Console.WriteLine("Use the all time is :{0} ms", sw.ElapsedMilliseconds);            Console.Read();        }    }}

運行結果(這是在我電腦上啟動並執行結果)從結果中可以看出加了鎖的運行速度慢了好多(慢了11倍 197/18 ):

 

2.2 Interlocked實現線程同步

Interlocked類提供了為多個線程共用的變數提供原子操作,當我們在多線程中對一個整數進行遞增操作時,就需要實現線程同步。

因為增加變數操作(++運算子)不是一個原子操作,需要執行下列步驟:

1)將執行個體變數中的值載入到寄存器中。

2)增加或減少該值。

3)在執行個體變數中儲存該值。

如果不使用 Interlocked.Increment方法,線程可能會在執行完前兩個步驟後被搶先。然後由另一個線程執行所有三個步驟,此時第一個線程還沒有把變數的值儲存到執行個體變數中去,而另一個線程就可以把執行個體變數載入到寄存器裡面讀取了(此時載入的值並沒有改變),所以會導致出現的結果不是我們預期的,相信這樣的解釋可以協助大家更好的理解Interlocked.Increment方法和 原子性操作,

下面通過一段代碼來示範下加鎖和不加鎖的區別(開始講過加鎖會對效能產生影響,這裡將介紹加鎖來解決線程同步的問題,得到我們預期的結果):

不加鎖的情況:

class Program    {        static void Main(string[] args)        {     for (int i = 0; i < 10; i++)            {                Thread testthread = new Thread(Add);                testthread.Start();            }            Console.Read();        }        // 共用資源        public static int number = 1;        public static void Add()        {            Thread.Sleep(1000);            Console.WriteLine("the current value of number is:{0}", ++number);        }}

運行結果(不同電腦上可能啟動並執行結果和我的不一樣,但是都是得到不是預期的結果的):

為瞭解決這樣的問題,我們可以通過使用 Interlocked.Increment方法來實現原子的自增操作。

代碼很簡單,只需要把++number改成Interlocked.Increment(ref number)就可以得到我們預期的結果了,在這裡代碼和運行結果就不貼了。

總之Interlocked類中的方法都是執行一次原子讀取以及寫入的操作的。

 

2.3 Monitor實現線程同步

對於上面那個情況也可以通過Monitor.Enter和Monitor.Exit方法來實現線程同步。C#中通過lock關鍵字來提供簡化的文法(lock可以理解為Monitor.Enter和Monitor.Exit方法的文法糖),代碼也很簡單:

 

using System;using System.Threading;namespace MonitorSample{    class Program    {        static void Main(string[] args)        {            for (int i = 0; i < 10; i++)            {                Thread testthread = new Thread(Add);                testthread.Start();            }            Console.Read();        }        // 共用資源        public static int number = 1;        public static void Add()        {            Thread.Sleep(1000);            //獲得獨佔鎖定            Monitor.Enter(number);            Console.WriteLine("the current value of number is:{0}", number++);            // 釋放指定對象上的獨佔鎖定。            Monitor.Exit(number);        }    }}

運行結果當然是我們所期望的:

在 Monitor類中還有其他幾個方法在這裡也介紹,只是讓大家引起注意下,一個Wait方法,很明顯Wait方法的作用是:釋放某個對象上的鎖以便允許其他線程鎖定和訪問這個對象。第二個就是TryEnter方法,這個方法與Enter方法主要的區別在於是否阻塞當前線程,當一個對象通過Enter方法擷取鎖,而沒有執行Exit方法釋放鎖,當另一個線程想通過Enter獲得鎖時,此時該線程將會阻塞,直到另一個線程釋放鎖為止,而TryEnter不會阻塞線程。具體代碼就不不寫出來了。

 

2.4 ReaderWriterLock實現線程同步

如果我們需要對一個共用資源執行多次讀取時,然而用前面所講的類實現的同步鎖都只允許一個線程允許,所有線程將阻塞,但是這種情況下肯本沒必要堵塞其他線程, 應該讓它們並發的執行,因為我們此時只是進行讀取操作,此時通過ReaderWriterLock類可以很好的實現讀取並行。

示範代碼為:

 

using System;using System.Collections.Generic;using System.Threading;namespace ReaderWriterLockSample{    class Program    {        public static List<int> lists = new List<int>();        // 建立一個對象        public static ReaderWriterLock readerwritelock = new ReaderWriterLock();        static void Main(string[] args)        {            //建立一個線程讀取資料            Thread t1 = new Thread(Write);            t1.Start();            // 建立10個線程讀取資料            for (int i = 0; i < 10; i++)            {                Thread t = new Thread(Read);                t.Start();            }            Console.Read();                        }        // 寫入方法        public static void Write()        {            // 擷取寫入鎖,以10毫秒為逾時。            readerwritelock.AcquireWriterLock(10);            Random ran = new Random();            int count = ran.Next(1, 10);            lists.Add(count);            Console.WriteLine("Write the data is:" + count);            // 釋放寫入鎖            readerwritelock.ReleaseWriterLock();        }        // 讀取方法        public static void Read()        {            // 擷取讀取鎖            readerwritelock.AcquireReaderLock(10);            foreach (int li in lists)            {                // 輸出讀取的資料                Console.WriteLine(li);            }            // 釋放讀取鎖            readerwritelock.ReleaseReaderLock();        }    }}

運行結果:

 

三、總結

本文中主要介紹如何?多線程同步的問題, 通過線程同步可以防止共用資料的損壞,但是由於擷取鎖的過程會有效能損失,所以在設計應用過程中盡量減少線程同步的使用。本來還要介紹互斥(Mutex), 訊號量(Semaphore), 事件構造的, 由於篇幅的原因怕影響大家的閱讀,所以這剩下的內容放在後面介紹的。 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.