線程系列08,實現線程鎖的各種方式,使用lock,Montor,Mutex,Semaphore以及線程死鎖


當涉及到多線程共享數據,需要數據同步的時候,就可以考慮使用線程鎖了。本篇體驗線程鎖的各種用法以及線程死鎖。主要包括:

 

使用lock處理數據同步
使用Monitor.Enter和Monitor.Exit處理數據同步
使用Mutex處理進程間數據同步
使用Semaphore處理數據同步
線程死鎖


□ 使用lock處理數據同步

 

假設有一個類,主要用來計算該類2個字段的商,在計算商的方法之內讓被除數自減,即被除數有可能為零。使用lock語句塊保證每次只有一個線程進入該方法。

    class ThreadSafe
    {
        static readonly object o = new object();
        private static int _val1, _val2;
        public ThreadSafe(int val1, int val2)
        {
            _val1 = val1;
            _val2 = val2;
        }
        public void Calculate()
        {
            lock (o)
            {
                --_val2;
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1/_val2);
                }
                else
                {
                    Console.WriteLine("_val2為零");
                }
                
            }
        }
    }

○ new object()創建的對象實例,也被稱作同步對象
○ 同步對象必須是引用類型
○ 同步對象通常是私有的、靜態的  

 

客戶端有一個靜態字段val2被ThreadSafe的2個實例方法共用。

    class Program
    {
        private static int val2 = 2;
        static void Main(string[] args)
        {
            ThreadSafe ts1 = new ThreadSafe(2, val2);
            ThreadSafe ts2 = new ThreadSafe(2, val2);
            Thread[] threads = new Thread[2];
            threads[0] = new Thread(ts1.Calculate);
            threads[1] = new Thread(ts2.Calculate);
            threads[0].Start();
            threads[1].Start();
            Console.ReadKey();
        }
    }

36

○ 雖然ThreadSafe的2個實例方法共用了客戶端靜態字段val2,因為有了lock的存在,保證了val2的數據同步
○ 使用lock出現異常,需要手動處理

 

□ 使用Monitor.Enter和Monitor.Exit處理數據同步

 

把上面的Calculate方法修改為:

        public void Calculate()
        {
            Monitor.Enter(o);
            _val2--;
            try
            {
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1 / _val2);
                }
                else
                {
                    Console.WriteLine("被除數為零");
                }
            }
            finally
            {
                Monitor.Exit(o);
            }
        }

37
○ 能得到相同的結果。      
○ lock其實是語法糖,其內部的實現邏輯就是Monitor.Enter和Monitor.Exit的實現邏輯

 

如果把Monitor.Exit注釋掉,會發生什么呢?

        public void Calculate()
        {
            Monitor.Enter(o);
            _val2--;
            try
            {
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1 / _val2);
                }
                else
                {
                    Console.WriteLine("被除數為零");
                }
            }
            finally
            {
                //Monitor.Exit(o);
            }
        }

38
可見,如果沒有Monitor.Exit,會捕捉不到異常。

 

不過,以上代碼還有一些不易察覺的、潛在的問題:如果在執行Monitor.Enter方法的時候出現異常,線程將拿不到鎖;如果在Monitor.Enter與try之間出現異常,由於無法執行try...catch語句塊,鎖得不到釋放。

 

為了解決以上問題, CLR 4.0給出了一個Monitor.Enter的重載方法。

public static void Enter (object obj, ref bool lockTaken);


現在,如果在執行Monitor.Enter方法的時候失敗,即沒有拿到鎖,lockTaken就為false,finally語句塊中無需釋放鎖;如果在Monitor.Enter之后出現異常,因為線程拿到了鎖,lockTaken就為true,最后在finally語句塊中釋放鎖。

 

所以,Calculate方法更健壯的寫法為:

        public void Calculate()
        {
            bool lockTaken = false;
            _val2--;
            try
            {
                Monitor.Enter(o, ref lockTaken);
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1 / _val2);
                }
                else
                {
                    Console.WriteLine("被除數為零");
                }
            }
            finally
            {
                if (lockTaken)
                {
                    Monitor.Exit(o);
                }
            }
        }

另外,Monitor還提供了多個靜態方法TryEnter的重載,可以指定在某個時間段內獲取鎖。

 

□ 使用Mutex處理進程間數據同步

 

Mutex的作用和lock相似,不過與lock不同的是:Mutex可以跨進程實施線程鎖。Mutex有2個重要的靜態方法:

○ WaitOne:阻止當前線程,如果收到當前實例的信號,則為true,否則為false
○ ReleaseMutex:用來釋放鎖,只有獲取鎖的線程才可以使用該方法,與lock一樣

 

Mutex一個經典應用就是:同一時間只能允許一個實例出現。

    class Program
    {
        static Mutex mutex = new Mutex(true,"darren.mutex");
        static void Main(string[] args)
        {
            if (!mutex.WaitOne(2000))//如果找到互拆體,即有另外一個相同的實例在運行着
            {
                Console.WriteLine("另外一個實例已經在運行着了~~");
                Console.ReadLine();
            }
            else//如果沒有發現互拆體
            {
                try
                {
                    RunAnother();
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }
        static void RunAnother()
        {
            Console.WriteLine("我是模擬另外一個實例正在運行着~~不過可以按回車鍵退出");
            Console.ReadLine();
        }
    }

40

以上是分別2次雙擊應用程序后的結果。

 

□ 使用Semaphore處理數據同步

 

Semaphore可以被形象地看成是一個舞池,比如該舞池最多能容納100人,超過100,都要在舞池外邊排隊等候進入。如果舞池中有一個人離開,在外面等候隊列中排在最前面的那個人就可以進入舞池。

 

如果舞池的容量是1,這時候Semaphore就和Mutex與lock很像了。不過,與Mutex和lock不同的是,任何線程都可以釋放Semaphore。

    class Program
    {
        static Semaphore _semaphore = new Semaphore(3,3);
        static void Main(string[] args)
        {
            Console.WriteLine("ladies and gentleman,舞會開始了~~");
            for (int i = 1; i <= 5; i++)
            {
                new Thread(IWannaDance).Start(i);
            }
        }
        static void IWannaDance(object id)
        {
            Console.WriteLine(id + "想跳舞");
            _semaphore.WaitOne();
            Console.WriteLine(id + "進了");
            Thread.Sleep(3000);
            Console.WriteLine(id + "准備離開舞池了");
            _semaphore.Release();
        }
    }

41
可見,舞池最多可容納3人,超過3人都得排隊。

 

□ 線程死鎖

 

有2個線程:線程1和線程2。有2個資源,資源1和資源2。線程1已經拿到了資源1的鎖,還想拿資源2的鎖,線程2已經拿到了資源2的鎖,同時還想拿資源1的鎖。線程1和線程2都沒有放棄自己的鎖,還同時想要另外的鎖,這就形成線程死鎖。就像2個小孩,手上都有自己的玩具,卻還想要對方的玩具,誰也不肯讓誰。

 

舉一個銀行轉賬的例子來呈現線程死鎖。

 

首先是銀行賬戶,提供了存款和取款的方法。

    public class Account
    {
        private double _balance;
        private int _id;
        public Account(int id, double balance)
        {
            this._id = id;
            this._balance = balance;
        }
        public int ID
        {
            get { return _id; }
        }
        //取款
        public void Withdraw(double amount)
        {
            _balance -= amount;
        }
        //存款
        public void Deposit(double amount)
        {
            _balance += amount;
        }
    }

 

其次是用來轉賬的一個管理類。

    public class AccountManager
    {
        private Account _fromAccount;
        private Account _toAccount;
        private double _amountToTransfer;
        public AccountManager(Account fromAccount, Account toAccount, double amount)
        {
            this._fromAccount = fromAccount;
            this._toAccount = toAccount;
            this._amountToTransfer = _amountToTransfer;
        }
        //轉賬
        public void Transfer()
        {
            Console.WriteLine(Thread.CurrentThread.Name + "正在" + _fromAccount.ID.ToString() + "獲取鎖");
            lock (_fromAccount)
            {
                Console.WriteLine(Thread.CurrentThread.Name + "已經" + _fromAccount.ID.ToString() + "獲取到鎖");
                Console.WriteLine(Thread.CurrentThread.Name + "被阻塞1秒");
                //模擬處理時間
                Thread.Sleep(1000);
                Console.WriteLine(Thread.CurrentThread.Name + "醒了,想想獲取" + _toAccount.ID.ToString() + "的鎖");
                lock (_toAccount)
                {
                    Console.WriteLine("如果造成線程死鎖,這里的代碼就不執行了~~");
                    _fromAccount.Withdraw(_amountToTransfer);
                    _toAccount.Deposit(_amountToTransfer);
                }
            }
        }
    }

○ 使用了2個lock,稱為"嵌套鎖",當一個方法中調用另外的方法,通常使用"嵌套鎖"
○ 第1個lock下的Thread.Sleep(1000)讓線程阻塞1秒,好讓另一個線程進來
○ 把"正在獲取XX鎖","已經獲取到XX鎖"......等狀態,打印到控制台上

 

客戶端開2個線程,一個線程賬戶A向賬戶B轉賬,另一個線程賬戶B向賬戶A轉賬。

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("准備轉賬了");
            Account accountA = new Account(1, 5000);
            Account accountB = new Account(2, 3000);
            AccountManager accountManagerA = new AccountManager(accountA, accountB, 1000);
            Thread threadA = new Thread(accountManagerA.Transfer);
            threadA.Name = "線程A";
            AccountManager accountManagerB = new AccountManager(accountB, accountA, 2000);
            Thread threadB = new Thread(accountManagerB.Transfer);
            threadB.Name = "線程B";
            threadA.Start();
            threadB.Start();
            threadA.Join();
            threadB.Join();
            Console.WriteLine("轉賬完成");
        }
    }

39

正如死鎖的定義:線程A獲取鎖1,線程2獲取鎖2,線程A想獲取鎖2,同時線程B想獲取鎖1。結果:線程死鎖。

 

○ 獲取鎖和釋放鎖的過程是相當快的,大概在幾十納秒的數量級
○ 線程鎖能解決並發問題,但如果持有鎖的時間過長,會增加線程死鎖的可能

 

 

總結:
○ 同一進程內,在同一時間,只有一個線程獲取鎖,占用一個資源或一段代碼,使用lock或Monitor.Enter/Monitor.Exit
○ 同一進程或不同進程內,在同一時間,只有一個線程獲取鎖,占用一個資源或一段代碼,使用Mutex
○ 同一進程或不同進程內,在同一時間,規定有限的線程占有一個資源或一段代碼,使用Semaphore
○ 使用線程鎖的時候要注意造成線程死鎖,當線程持有鎖的時間過長,容易造成線程死鎖

 

線程系列包括:

線程系列01,前台線程,后台線程,線程同步

線程系列02,多個線程同時處理一個耗時較長的任務以節省時間

線程系列03,多線程共享數據,多線程不共享數據

線程系列04,傳遞數據給線程,線程命名,線程異常處理,線程池

線程系列05,手動結束線程

線程系列06,通過CLR代碼查看線程池及其線程

線程系列07,使用lock語句塊或Interlocked類型方法保證自增變量的數據同步

線程系列08,實現線程鎖的各種方式,使用lock,Montor,Mutex,Semaphore以及線程死鎖

線程系列09,線程的等待、通知,以及手動控制線程數量

線程系列10,無需顯式調用線程的情形


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM