1. 程式人生 > 實用技巧 >c# thread4——lock,死鎖,以及monitor關鍵字

c# thread4——lock,死鎖,以及monitor關鍵字

多執行緒的存在是提高系統效率,挖掘cpu效能的一種手段,那麼控制它,能夠協同多個執行緒不發生bug是關鍵。

首先我們來看一段不安全的多執行緒程式碼。

public abstract class CalculateBase
{
        public int count = 0;
        public object _lock = new object();
        public abstract void Operation();
}

public class NoSafeCalculate : CalculateBase
{
        public override
void Operation() { count++; count--; } }

static void Main(string[] args)
{
            CalculateBase calculate = new NoSafeCalculate();
            Thread thread1 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            Thread thread2 
= new Thread(() => { for (int i = 0; i < 1000000; i++) { calculate.Operation(); } }); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine(calculate.count); Console.ReadLine(); }

很簡單的一段程式碼,大致意思就是啟動兩個執行緒去加減一個變數,由於是一個例項化物件,所以變數也是同一個,按道理來說,變數自增一次自減一次,應該是不變的,在單執行緒中確實如此,但是在多執行緒中結果就會發生變化。

我們來執行一下。

發現結果並不是0,而是226.並且每次執行都是不一樣的結果。

為什麼會出現這種情況?

相信很多人都知道

1.++,--不是原子操作,舉個例子a++其實是多步操作。1)計算a+1的值。2)將值賦值給a,那麼多執行緒在執行這種操作的時候就很容易引起併發問題,結果不一致。(c#自帶原子性操作函式)

2.值的不一致。比如我這邊的count在執行++操作,但是還沒有執行完,另外一個執行緒也執行到了++操作,結果兩個++執行完之後,最終只是+了一次。

要解決這種多執行緒併發問題,如果不考慮效能,那麼lock關鍵字將是很好的選擇

我們來看看安全的程式碼

public abstract class CalculateBase
    {
        public int count = 0;
        public object _lock = new object();
        public abstract void Operation();
    }

    public class NoSafeCalculate : CalculateBase
    {
        public override void Operation()
        {
                count++;
                count--;
        }
    }
    public class SafeCalculate : CalculateBase
    {
        public override void Operation()
        {
            lock (_lock)
            {
                count++;
                count--;
            }
        }
    }

static void Main(string[] args)
        {
            CalculateBase calculate = new SafeCalculate();
            Thread thread1 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            Thread thread2 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            Console.WriteLine(calculate.count);
            Console.ReadLine();

        }

很顯然,相較於不安全的程式碼我們只是多了一個lock的作用域,我們在Operation方法中添加了一個lock作用域將我們的自增和自減包裹起來,並且傳入了一個object型別的_lock引數。

結果:0

多次測試結果都是0,那麼可以說線城是安全了,那麼lock關鍵字的作用是什麼。

c#中lock關鍵字等同於java中的synchionzed,意思為"同步",也就是說被它包裹的程式碼段都將以同步的方式進行,也就是同時以單執行緒執行。

那麼它是如何做到的?其實就是傳入的引數的作用了,我們看到傳入了一個_lock引數,其實它就是一個"鎖的鑰匙",誰能拿到他誰就能開啟鎖,執行程式碼。所以每次有一個執行緒拿到鎖後,其他的執行緒只能等待這個執行緒執行完然後將鎖釋放,其他執行緒才能夠繼續執行。

(附加:為什麼用object型別作為鎖的鑰匙,string或者基本型別可以麼?有什麼區別)

何為死鎖

執行緒死鎖是指由於兩個或者多個執行緒互相持有對方所需要的資源會互相等待對方釋放資源,導致這些執行緒處於等待狀態,無法前往執行。如果執行緒都不主動釋放所佔有的資源,將產生死鎖。

我們來完成一個死鎖的demo

public class DeadLock
    {
        public object _lock = new object();
        public object _lock1 = new object();
        public void Into()
        {
            lock (_lock)
            {
                Thread.Sleep(2000);
                lock (_lock1)
                {
                    Console.WriteLine("I am success come in");
                }
            }
        }
        public void Out()
        {
            lock (_lock1)
            {
                Thread.Sleep(1000);
                lock (_lock)
                {
                    Console.WriteLine("I am success go out");
                }
            }
        }
    }

static void Main(string[] args)
{
            DeadLock dead = new DeadLock();
            Thread thread1 = new Thread(() => { dead.Into(); });
            Thread thread2 = new Thread(() => { dead.Out(); });
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            Console.WriteLine("執行結束");
}

執行程式碼:

那基本上是這輩子看不到列印"執行結束"這四個字了。

其實上面的例子就是一個死鎖的典型場景,一個執行緒拿住了A鑰匙,打開了A鎖,執行完後需要B鑰匙,才能開啟B鎖,可是B鑰匙永遠也拿不到,因為另外一個執行緒正佔用這B鑰匙,在等待A鎖的釋放

那麼怎麼解決這種問題呢。

1.儘量用不同的物件作為鎖的"鑰匙"

2.使用c#提供的monitor關鍵字

我們將上面的程式碼進行修改。

public class DeadLock
{
        public object _lock = new object();
        public object _lock1 = new object();
        public void Into()
        {
            lock (_lock)
            {
                Thread.Sleep(2000);
                lock (_lock1)
                {
                    Console.WriteLine("I am success come in");
                }
            }
        }
        public void Out()
        {
            lock (_lock1)
            {
                Thread.Sleep(1000);
                if (Monitor.TryEnter(_lock, 5))
                {
                    Console.WriteLine("go out success");
                }
                else
                {
                    Console.WriteLine("timeout");
                }

            }
        }
}

此時我們再來看看執行結果:

可以看到我們的程式死鎖走出來了。

我們的monitor的作用就是會接受一個超時的引數,如果在時間內拿到鎖,則返回true否則返回false,避免了死鎖

最後補充下,其實lock也是Monitor的一個語法糖,分解lock的程式碼我們就能夠了解。

public void demoLock()
{
            bool getlock = false;
            try
            {
                Monitor.Enter(_lock, ref getlock);
            }
            finally
            {
                if (getlock)//如果拿到鎖則釋放鎖
                {
                    //業務
                    //...
                    Monitor.Exit(_lock);
                }
            }
}

轉載:https://www.cnblogs.com/qwqwQAQ/p/11980498.html