C#進階知識點概要(2) - 線程並發鎖

來源:互聯網
上載者:User

標籤:全域   不同   thread   var   c#   factory   web開發   排它鎖   set   

本文目錄:

  • 線程的簡單使用
  • 並發和非同步區別
  • 並發控制 - 鎖
  • 線程的訊號機制
  • 線程池中的線程
  • 案例:支援並發的非同步日誌組件
線程的簡單使用

常見的並發和非同步大多是基於線程來實現的,所以本文先講線程的簡單使用方法。

使用線程,我們需要引用System.Threading命名空間。建立一個線程最簡單的方法就是在 new 一個 Thread,並傳遞一個ThreadStart委託(無參數)或ParameterizedThreadStart委託(帶參數),如下:

class Program {    static void Main(string[] args) {        // 使用無參數委託ThreadStart        Thread t = new Thread(Go);        t.Start();        // 使用帶參數委託ParameterizedThreadStart        Thread t2 = new Thread(GoWithParam);        t2.Start("Message from main.");        t2.Join();// 等待線程t2完成。        Console.WriteLine("Thread t2 has ended!");        Console.ReadKey();    }    static void Go() {        Console.WriteLine("Go!");    }    static void GoWithParam(object msg) {        Console.WriteLine("Go With Param! Message: " + msg);        Thread.Sleep(1000);// 類比耗時操作    }}

運行結果:

線程的用法,我們只需要瞭解這麼多。下面我們再來通過一段代碼來講講並發和非同步。

並發和非同步區別

關於並發和非同步,我們先來寫一段代碼,類比多個線程同時寫1000條日誌:

class Program {    static void Main(string[] args) {        Thread t1 = new Thread(Working);        t1.Name = "Thread1";        Thread t2 = new Thread(Working);        t2.Name = "Thread2";        Thread t3 = new Thread(Working);        t3.Name = "Thread3";        // 依次啟動3個線程。        t1.Start();        t2.Start();        t3.Start();        Console.ReadKey();    }    // 每個線程都同時在工作    static void Working() {        // 類比1000次寫日誌操作        for (int i = 0; i < 1000; i++) {            //  非同步寫檔案            Logger.Write(Thread.CurrentThread.Name + " writes a log: " + i + ", on " + DateTime.Now.ToString() + ".\n");        }// 做一些其它的事件        for (int i = 0; i < 1000; i++) { }    }}

代碼很簡單,相信大家都能看得懂。Logger 大家可以把它看做是一個寫日誌的組件,先不關心它的具體實現,只要知道它是一個提供了寫日誌功能的組件就行。

那麼,這段代碼跟並發和非同步有什麼關係呢?

我們先用一張圖來描述這段代碼:

觀察,3個線程同時調用Logger寫日誌,對於Logger來說,3個線程同時交給了它任務,這種情況就是並發。對於其中一個線程來說,它在工作過程中,在某個時間請求Logger幫它寫日誌,同時又繼續在自己的其它工作,這種情況就是非同步

(經讀者反饋,為不“誤導”讀者(儘管我個人不覺得是誤導。之前我的定義和解釋不全 面,沒有從作業系統和CPU層次去區分這兩個概念。我的文章不喜歡搬教科書,只是想用通俗易讀的白話讓大家理解),為了知識的專業性和嚴謹,現已把我理解 的對並發和非同步定義刪除,感謝園友們的熱心討論)。

 

接下來,我們繼續講幾個很有用的有關線程和並發的知識 - 鎖、訊號機制和線程池。

並發控制 - 鎖

CLR 會為每個線程分配自己的記憶體堆空間,以使他們的本地變數保持分離互不干擾。

線程之間也可以共用通用的資料,比如同一對象的某個屬性或全域靜態變數。但線程間共用資料是存在安全問題的。舉個例子,下面的主線程和新線程共用了變數done,done用來標識某件事已經做過了(告訴其它線程不要再重複做了):

class Program {    static bool done;    static void Main(string[] args) {        new Thread(Go).Start(); // 在新的線程上調用Go        Go(); // 在主線程上調用Go        Console.ReadKey();    }    static void Go() {        if (!done) {            Thread.Sleep(500); // 類比耗時操作            Console.WriteLine("Done");             done = true;        }    }}

輸出結果:

輸出了兩個“Done”,事件被做了兩次。由於沒有控制好並發,這就出現了線程的安全問題,無法保證資料的狀態。

要解決這個問題,就需要用到鎖(Lock,也叫排它鎖或互斥鎖)。使用lock語句,可以保證共用資料只能同時被一個線程訪問。lock的資料對象 要求是不能null的參考型別的對象,所以lock的對象需保證不可為空。為此需要建立一個不為空白的對象來使用鎖,修改一下上面的代碼如下:

class Program {    static bool done;    static object locker = new object(); // !!    static void Main(string[] args) {        new Thread(Go).Start(); // 在新的線程上調用Go        Go(); // 在主線程上調用Go        Console.ReadKey();    }    static void Go() {        lock (locker) {            if (!done) {                Thread.Sleep(500); // Doing something.                Console.WriteLine("Done");                done = true;            }        }    }}

再看結果:

使用鎖,我們解決了問題。但使用鎖也會有另外一個安全執行緒問題,那就是“死結”,死結的機率很小,但也要避免。保證“上鎖”這個操作在一個線程上執行是避免死結的方法之一,這種方法在下文案例中會用到。

這裡我們就不去深入研究“死結”了,感興趣的朋友可以去查詢相關資料。

線程的訊號機制

有時候你需要一個線程在接收到某個訊號時,才開始執行,否則處於等待狀態,這是一種基於訊號的事件機制。.NET架構提供一個 ManualResetEvent類來處理這類事件,它的 WaiOne 執行個體方法可使當前線程一直處於等待狀態,直到接收到某個訊號。它的Set方法用於開啟發送訊號。下面是一個訊號機制的使用樣本:

static void Main(string[] args) {    var signal = new ManualResetEvent(false);    new Thread(() => {        Console.WriteLine("Waiting for signal...");        signal.WaitOne();        signal.Dispose();        Console.WriteLine("Got signal!");    }).Start();    Thread.Sleep(2000);    signal.Set();// 開啟“訊號”    Console.ReadKey();}

運行結果:

當執行Set方法後,訊號保持開啟狀態,可通過Reset方法將其關閉,若不再需要,通過Dispose將其釋放。如果預期的等待時間很短,可以用 ManualResetEventSlim代替ManualResetEvent,前者在等待時間較短時效能更好。訊號機制非常有用,後面的日誌案例會用 到它。

線程池中的線程

線程池中的線程是由CLR來管理的。在下面兩種條件下,線程池能起到最好的效用:

  • 任務啟動並執行時候比較短(<250ms),這樣CLR可以充分調配現有的空閑線程來處理該任務;
  • 大量時間處於等待(或阻塞)的任務不去支配線程池的線程。

要使用線程中的線程,主要有下面兩種方式:

// 方式1:Task.Run,.NET Framework 4.5 才有Task.Run (() => Console.WriteLine ("Hello from the thread pool"));// 方式2:ThreadPool.QueueUserWorkItemThreadPool.QueueUserWorkItem (t => Console.WriteLine ("Hello from the thread pool"));

線程池使得線程可以充分有效地被使用,減少了任務啟動的延遲。但是不是所有的情況都適合使用線程池中的線程,比如下面要講的日誌案例 - 非同步寫檔案。

這裡講線程池,是為了讓大家大致瞭解什麼時候用線程池中的線程,什麼時候不用。即,耗時間長度或有阻塞情況的不用線程池中的線程。

建立不走線程池中的線程,可以直接通過new Thread來建立,也可以通過下面的代碼來建立:

Task task = Task.Factory.StartNew (() => ...,TaskCreationOptions.LongRunning);// 注意必須帶TaskCreationOptions.LongRunning參數

這裡用到了Task,大家不用關心它,後續博文會詳細講。

關於線程的知識很多,這裡不再深入了,因為這些已經足夠讓我們應付Web開發了。

案例:支援並發的非同步日誌組件

上文的“並發和非同步區別”的代碼中我們用到了一個Logger類,現在我們就來做一個這樣的Logger。

基於上面的知識,我們可以實現應用程式的並發寫日誌日誌功能。在應用程式中,寫日誌是常見的功能,簡單分析一下該功能的需求:

  1. 在後台非同步執行,和其它線程互不影響。
    根據上文線程池的兩個最優使用條件,由寫日誌線程會長時間處於阻塞(或運行等待)狀態,所以它不適合使用線程池。即不能使用Task.Run,而最好使用new Thread。

  2. 支援並發,即多個任務(分布在不同線程上)可同時調用寫日誌功能,但需保證安全執行緒。
    支援並發,必然要用到鎖,但要完全保證安全執行緒,那就要想辦法避免“死結”。只要我們把“上鎖”的操作始終由同一個線程來做即可避免“死結”問題,但這樣的話,並發請求的任務只能放在隊列中由該線程依次執行(因為是後台執行,無需即時響應使用者,所以可以這麼做)。

  3. 單個執行個體,單個線程。
    任何地方調用寫日誌功能都調用的是同一個Logger執行個體(顯然不能每次寫日誌都建立一個執行個體),即需使用單例模式。不管有多少任務調用寫日誌功能,都必須始終使用同一個線程來處理這些寫日誌操作,以保證不佔用過多的線程資源和避免建立線程帶來的延遲。

運用上面的知識,我們來寫一個這樣的類。簡單理一下思路:

  1. 需要一個用來存放寫日誌任務的隊列。
  2. 需要有一個訊號機制來標識是否有新的任務要執行。
  3. 當有新的寫日誌任務時,將該任務加入到隊列中,並發出訊號。
  4. 用一個方法來處理隊列中的任務,當接收新任務訊號時,就依次調用隊列中的任務。

開發一個功能前需要有個簡單的思路,保證心裏面有底。具體開發的時候會發現問題,然後再去補充擴充和完善等。剛開始很難想得太周全,先有個簡單的思路,然後代碼寫起來!

下面是這樣一個Logger類初步實現:

public class Logger {    // 用於存放寫日誌任務的隊列    private Queue<Action> _queue;    // 用於寫日誌的線程    private Thread _loggingThread;    // 用於通知是否有新日誌要寫的“訊號器”    private ManualResetEvent _hasNew;    // 建構函式,初始化。    private Logger() {        _queue = new Queue<Action>();        _hasNew = new ManualResetEvent(false);        _loggingThread = new Thread(Process);        _loggingThread.IsBackground = true;        _loggingThread.Start();    }    // 使用單例模式,保持一個Logger對象    private static readonly Logger _logger = new Logger();    private static Logger GetInstance() {        /* 不安全的程式碼        lock (locker) {            if (_logger == null) {                _logger = new Logger();            }        }*/        return _logger;    }    // 處理隊列中的任務    private void Process() {        while (true) {            // 等待接收訊號,阻塞線程。            _hasNew.WaitOne();            // 接收到訊號後,重設“訊號器”,訊號關閉。            _hasNew.Reset();             // 由於隊列中的任務可能在極速地增加,這裡等待是為了一次能處理更多的任務,減少對隊列的頻繁“進出”操作。            Thread.Sleep(100);            // 開始執行隊列中的任務。            // 由於執行過程中還可能會有新的任務,所以不能直接對原來的 _queue 進行操作,            // 先將_queue中的任務複製一份後將其清空,然後對這份拷貝進行操作。            Queue<Action> queueCopy;            lock (_queue) {                queueCopy = new Queue<Action>(_queue);                _queue.Clear();            }            foreach (var action in queueCopy) {                action();            }        }    }    private void WriteLog(string content) {        lock (_queue) { // todo: 這裡存線上程安全問題,可能會發生阻塞。            // 將任務加到隊列            _queue.Enqueue(() => File.AppendAllText("log.txt", content));        }        // 開啟“訊號”        _hasNew.Set();    }    // 公開一個Write方法供外部調用    public static void Write(string content) {        // WriteLog 方法只是向隊列中新增工作,執行時間極短,所以使用Task.Run。        Task.Run(() => GetInstance().WriteLog(content));    }}

類寫好了,用上文“並發和非同步區別”中的代碼測試一下這個Logger類,在我的電腦上啟動並執行一次結果:

 共3000條日誌,結果沒有問題。

上面的Logger類注釋寫得很詳細,我就不再解析了。

通過這個樣本,目的是讓大家掌握線程和並發在開發中的基本應用和要注意的問題。

遺憾的是這個Logger類並不完美,而且存線上程安全問題(代碼中用紅色字型標出),雖然實際環境機率很小。可能上面代碼多次運行都很難看到有異常發生(我多次運行未發生異常),但同時再添加幾個線程可能就會有問題了。

那麼,如何解決這個安全執行緒問題呢?

 

引用地址:http://www.cnblogs.com/kesimin/p/5085460.html

C#進階知識點概要(2) - 線程並發鎖

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.