程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 線程系列08,實現線程鎖的各種方式,使用lock,Montor,Mutex,Semaphore以及線程死鎖,montormutex

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

編輯:C#入門知識

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


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

 

※ 使用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();
        }
    }

○ 雖然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);
            }
        }


○ 能得到相同的結果。      
○ 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);
            }
        }


可見,如果沒有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();
        }
    }

以上是分別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();
        }
    }


可見,舞池最多可容納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("轉賬完成");
        }
    }

正如死鎖的定義:線程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以及線程死鎖


Linux 多線程 死鎖問題解

這麼專業的問題還是不要在這問了,白費時間和精力!本人的多線程死鎖還一直是個難題,再加上socket通訊的阻塞與非阻塞,非常不好辦。
網上也就解決點常識性的,別的還是需要閉門造車的精神多做研究吧
 

多線程同步時幾個的具體不同與應用場合?

1、 Event  用事件(Event)來同步線程是最具彈性的了。一個事件有兩種狀態:激發狀態和未激發狀態。也稱有信號狀態和無信號狀態。事件又分兩種類型:手動重置事件和自動重置事件。手動重置事件被設置為激發狀態後,會喚醒所有等待的線程,而且一直保持為激發狀態,直到程序重新把它設置為未激發狀態。自動重置事件被設置為激發狀態後,會喚醒“一個”等待中的線程,然後自動恢復為未激發狀態。所以用自動重置事件來同步兩個線程比較理想。MFC中對應的類為CEvent.。CEvent的構造函數默認創建一個自動重置的事件,而且處於未激發狀態。共有三個函數來改變事件的狀態:SetEvent,ResetEvent和PulseEvent。用事件來同步線程是一種比較理想的做法,但在實際的使用過程中要注意的是,對自動重置事件調用SetEvent和PulseEvent有可能會引起死鎖,必須小心。  多線程同步-event  在所有的內核對象中,事件內核對象是個最基本的。它包含一個使用計數(與所有內核對象一樣),一個BOOL值(用於指明該事件是個自動重置的事件還是一個人工重置的事件),還有一個BOOL值(用於指明該事件處於已通知狀態還是未通知狀態)。事件能夠通知一個線程的操作已經完成。有兩種類型的事件對象。一種是人工重置事件,另一種是自動重置事件。他們不同的地方在於:當人工重置的事件得到通知時,等待該事件的所有線程均變為可調度線程。當一個自動重置的事件得到通知時,等待該事件的線程中只有一個線程變為可調度線程。  當一個線程執行初始化操作,然後通知另一個線程執行剩余的操作時,事件使用得最頻繁。在這種情況下,事件初始化為未通知狀態,然後,當該線程完成它的初始化操作後,它就將事件設置為已通知狀態,而一直在等待該事件的另一個線程在事件已經被通知後,就變成可調度線程。  當這個進程啟動時,它創建一個人工重置的未通知狀態的事件,並且將句柄保存在一個全局變量中。這使得該進程中的其他線程能夠非常容易地訪問同一個事件對象。程序一開始創建了三個線程,這些線程在初始化後就被掛起,等待事件。這些線程要等待文件的內容讀入內存,然後每個線程都會訪問這段文件內容。一個線程進行單詞計數,另一個線程運行拼寫檢查,第三個線程運行語法檢查。這3個線程函數的代碼的開始部分都相同,每個函數都調用WaitForSingleObject.,這將使線程暫停運行,直到文件的內容由主線程讀入內存為止。一旦主線程將數據准備好,它就調用SetEvent,給事件發出通知信號。這時,系統就使所有這3個輔助線程進入可調度狀態,它們都獲得了C P U時間,並且可以訪問內存塊。這3個線程都必須以只讀方式訪問內存,否則會出現內存錯誤。這就是所有3個線程能夠同時運行的唯一原因。如果計算機上配有三個以上CPU,理論上這個3個線程能夠真正地同時運行,從而可以在很短的時間內完成大量的操作  如果你使用自動重置的事件而不是人工重置的事件,那麼應用程序的行為特性就有很大的差別。當主線程調用S e t E v e n t之後,系統只允許一個輔助線程變成可調度狀態。同樣,也無法保證系統將使哪個線程變為可調度狀態。其余兩個輔助線程將繼續等待。已經變為可調度狀態的線程擁有對內存塊的獨占訪問權。  讓我們重新編寫線程的函數,使得每個函數在返回前調用S e t E v e n t函數(就像Wi n M a i n函數所做的那樣)。  當主線程將文件內容讀入內存後,它就調用SetEvent函數,這樣操作西永就會使這三個在等待的線程中的一個成為可調度線程。我們不知道系統將首先......余下全文>>
 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved