關於C#多執行緒、易失域、鎖的分享

Tulip123發表於2019-07-24

一、多執行緒

  windows系統是一個多執行緒的作業系統。一個程式至少有一個程式,一個程式至少有一個執行緒。程式是執行緒的容器,一個C#客戶端程式開始於一個單獨的執行緒,CLR(公共語言執行庫)為該程式建立了一個執行緒,該執行緒稱為主執行緒。例如當我們建立一個C#控制檯程式,程式的入口是Main()函式,Main()函式是始於一個主執行緒的。它的功能主要 是產生新的執行緒和執行程式。

  在軟體中,如果有一種操作可以被多人同時呼叫,我們就可以建立多個執行緒同時處理,以提高任務執行效率。這時,操作就被分配到各個執行緒中分別執行。

在C#中我們可以使用Thread類和ThreadStart委託,他們都定義在System.Threading名稱空間中。

  ThreadStart委託型別用於定義線上程中的工作,就像我們在使用其他的委託型別一樣,可以使用方法名來建立此委託型別物件,如“new ThreadStart(test)”

多執行緒優點:
(1)多執行緒技術使程式的響應速度更快 ,因為使用者介面可以在進行其它工作的同時一直處於活動狀態;
(2)多執行緒可以提高CPU的利用率,因為當一個執行緒處於等待狀態的時候,CPU會去執行另外的執行緒;
(3)佔用大量處理時間的任務可以定期將處理器時間讓給其它任務;
(4)可以隨時停止任務;
(5)可以分別設定各個任務的優先順序以優化效能。

多執行緒缺點:
(1)等候使用共享資源時造成程式的執行速度變慢。這些共享資源主要是獨佔性的資源 ,如寫檔案等。
(2)對執行緒進行管理要求額外的 CPU開銷。執行緒的使用會給系統帶來上下文切換的額外負擔。當這種負擔超過一定程度時,多執行緒的特點主要表現在其缺點上,比如用獨立的執行緒來更新陣列內每個元素。
(3)執行緒的死鎖。即較長時間的等待或資源競爭以及死鎖等多執行緒症狀。
(4)對公有變數的同時讀或寫。當多個執行緒需要對公有變數進行寫操作時,後一個執行緒往往會修改掉前一個執行緒存放的資料,從而使前一個執行緒的引數被修改;另外 ,當公用變數的讀寫操作是非原子性時,在不同的機器上,中斷時間的不確定性,會導致資料在一個執行緒內的操作產生錯誤,從而產生莫名其妙的錯誤,而這種錯誤是程式設計師無法預知的。

執行緒生命週期

執行緒生命週期開始於 System.Threading.Thread 類的物件被建立時,結束於執行緒被終止或完成執行時。

下面列出了執行緒生命週期中的各種狀態:

    • 未啟動狀態:當執行緒例項被建立但 Start 方法未被呼叫時的狀況。
    • 就緒狀態:當執行緒準備好執行並等待 CPU 週期時的狀況。
    • 不可執行狀態:下面的幾種情況下執行緒是不可執行的:
      • 已經呼叫 Sleep 方法
      • 已經呼叫 Wait 方法
      • 通過 I/O 操作阻塞
    • 死亡狀態:當執行緒已完成執行或已中止時的狀況

Thread 類常用的屬性和方法

  

最簡單的多執行緒例子,程式碼如下:

static void Main(string[] agrs)
        {
            ThreadStart threadWork = new ThreadStart(test);
            Thread t1 = new Thread(threadWork);
            t1.Name = "t1";
            Thread t2 = new Thread(threadWork);
            t2.Name = "t2";
            Thread t3 = new Thread(threadWork);
            t3.Name = "t3";
            //開始執行
            t1.Start();
            t2.Start();
            t3.Start();
            Console.ReadKey();
        }
        static public void  test(){
            Console.WriteLine("{0},hello,小菜鳥",Thread.CurrentThread.Name);
        }

使用多執行緒另一個重要的問題就是對於公共資源分配的控制,比如,火車的座位是有限的,在不同購票點買票時,就需要對座位資源進行合理分配;在電影院看電影也是這樣的,座位只有那麼多,我們不可能100個座位賣出200張票,這樣是不可以的也是不應該的,那麼接下來我們就要看看該如何解決這個問題。

二、易失域

對於類中的成員使用volatile修飾符,它就會被宣告為易失域。對於易失域,在多執行緒環境中,每個執行緒中對此域的讀取(易失讀取,volatile read)和寫入(易失寫入,volatile write)操作都會觀察其他執行緒中的操作,並進行操作的順序執行,這樣就保持易失域使用的一致性了。

volatile的作用是: 作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。多執行緒的程式,共同訪問的記憶體當中,多個執行緒都可以操縱,從而無法判定何時這個變數會發生變化

可以這樣簡單理解:執行緒是並行的,但對volatile的訪問是順序排除的,避免出現髒值。

理解:

Volatile 字面的意思時易變的,不穩定的。在C#中也差不多可以這樣理解。

編譯器在優化程式碼時,可能會把經常用到的程式碼存在Cache裡面,然後下一次呼叫就直接讀取Cache而不是記憶體,這樣就大大提高了效率。但是問題也隨之而來了。

在多執行緒程式中,如果把一個變數放入Cache後,又有其他執行緒改變了變數的值,那麼本執行緒是無法知道這個變化的。它可能會直接讀Cache裡的資料。但是很不幸,Cache裡的資料已經過期了,讀出來的是不合時宜的髒資料。這時就會出現bug。

用Volatile宣告變數可以解決這個問題。用Volatile宣告的變數就相當於告訴編譯器,我不要把這個變數寫Cache,因為這個變數是可能發生改變的。

下面貼栗子程式碼:

using System;
using System.Threading;
 
namespace demoVolatile
{
    class Program
    {
        //多個執行緒訪問的變數,標記為Volatile
        //在這裡如果不標記可能會賣出不止10張票
        volatile static int TicketCount = 10;
        static void SellTicket()
        {
            while (TicketCount > 0)
            {
                TicketCount--;
                Console.WriteLine("{0} 賣出了一張票", Thread.CurrentThread.Name);
            }
            Console.WriteLine("{0} 下班了", Thread.CurrentThread.Name);
        }
        static void Main(string[] args)
        {
            ThreadStart threadWork = new ThreadStart(SellTicket);
            Thread t1 = new Thread(threadWork);
            t1.Name = "t1";
            Thread t2 = new Thread(threadWork);
            t2.Name = "t2";
            Thread t3 = new Thread(threadWork);
            t3.Name = "t3";
            //開始執行
            t1.Start();
            t2.Start();
            t3.Start();
            Console.ReadKey();
        }
    }
}

三、鎖

我們都知道,lock 關鍵字可以用來確保程式碼塊完成執行,而不會被其他執行緒中斷。也就是,說在多執行緒中,使用lock關鍵字,可以讓被lock的物件,一次只被一個執行緒使用。

lock語句根本使用的就是Monitor.Enter和Monitor.Exit,也就是說lock(this)時執行Monitor.Enter(this),大括號結束時執行Monitor.Exit(this). 也就是說,Lock關鍵字,就是一個語法糖而已。

使用lock需要注意的地方:
1.lock不能鎖定空值某一物件可以指向Null,但Null是不需要被釋放的。(請參考:認識全面的null)
2.lock不能鎖定string型別,雖然它也是引用型別的。因為字串型別被CLR“暫留”
3.lock鎖定的物件是一個程式塊的記憶體邊界
4.值型別不能被lock,因為前文標紅字的“物件被釋放”,值型別不是引用型別的
5.lock就避免鎖定public 型別或不受程式控制的物件。

using System;
using System.Threading.Tasks;

public class Account
{
    private readonly object balanceLock = new object();
    private decimal balance;

    public Account(decimal initialBalance)
    {
        balance = initialBalance;
    }

    public decimal Debit(decimal amount)
    {
        lock (balanceLock)
        {
            if (balance >= amount)
            {
                Console.WriteLine($"Balance before debit :{balance, 5}");
                Console.WriteLine($"Amount to remove     :{amount, 5}");
                balance = balance - amount;
                Console.WriteLine($"Balance after debit  :{balance, 5}");
                return amount;
            }
            else
            {
                return 0;
            }
        }
    }

    public void Credit(decimal amount)
    {
        lock (balanceLock)
        {
            Console.WriteLine($"Balance before credit:{balance, 5}");
            Console.WriteLine($"Amount to add        :{amount, 5}");
            balance = balance + amount;
            Console.WriteLine($"Balance after credit :{balance, 5}");
        }
    }
}

class AccountTest
{
    static void Main()
    {
        var account = new Account(1000);
        var tasks = new Task[100];
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Run(() => RandomlyUpdate(account));
        }
        Task.WaitAll(tasks);
    }

    static void RandomlyUpdate(Account account)
    {
        var rnd = new Random();
        for (int i = 0; i < 10; i++)
        {
            var amount = rnd.Next(1, 100);
            bool doCredit = rnd.NextDouble() < 0.5;
            if (doCredit)
            {
                account.Credit(amount);
            }
            else
            {
                account.Debit(amount);
            }
        }
    }
}

作用:當同一個資源被多個執行緒讀,少個執行緒寫的時候,使用讀寫鎖

引用:https://blog.csdn.net/weixin_40839342/article/details/81189596 

問題: 既然讀讀不互斥,為何還要加讀鎖

答:     如果只是讀,是不需要加鎖的,加鎖本身就有效能上的損耗

            如果讀可以不是最新資料,也不需要加鎖

            如果讀必須是最新資料,必須加讀寫鎖

            讀寫鎖相較於互斥鎖的優點僅僅是允許讀讀的併發,除此之外並無其他。

注意:不要使用ReaderWriterLock,該類有問題

 

ok,今天的分享就到這裡了,如有錯誤的地方請指出,謝謝。

 

相關文章