C#基礎之lock

1.lock的本質程序員

  實現線程同步的第一種方式是咱們常用的lock關鍵字,它將包圍的語句塊標記爲臨界區,這樣一次只有一個線程進入臨界區並執行代碼。下面第一段的幾行代碼是關於lock關鍵字的使用方式,但更重要的是咱們能夠經過這個例子來看到lock關鍵字的本質。第二段是這個方法的IL指令集,從中能夠看到lock其實也是一個語法糖,它的內部實現是採用了監視器Monitor。第三段代碼是我寫的lock內部實現的C#代碼,因爲lock內部有finally關鍵字,這將保證內部最後必定會執行exit方法。而若是咱們本身使用Monitor的話有可能會一不當心忘記調用exit方法,所以使用lock更加可靠。在使用lock關鍵字時必須使用一個引用類型的參數,我若是將i字段放入lock會提示報錯,可見使用lock沒法將i進行裝箱。固然這只是猜想,本質的緣由將在後面解釋。在上例中我new了一個o對象,這樣鎖的範圍只包括lock語句塊。若是lock中傳進來的參數是一個外部對象,那麼鎖的範圍將擴展到這個對象。官方文檔上有這樣一句話:「嚴格來講,提供的對象是用來惟一地標識由多個線程共享的資源,因此它能夠是任意類型。然而實際上,此對象一般表示須要進行線程同步的資源「。從這句話能夠知道選擇引用參數時,應該選擇多個線程須要操做的共享對象,像我這裏隨便建立的object對象不是官方推薦的作法。數組

        public void MyLock()
        {
            int i = 0;
            object o=new object();
            lock (o)
            {
                i = 5;
            }
        }
View Code
.method public hidebysig instance void MyLock() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 num,
        [1] object obj2,
        [2] bool flag,
        [3] object obj3,
        [4] bool flag2)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.0 
    L_0003: newobj instance void [mscorlib]System.Object::.ctor()
    L_0008: stloc.1 
    L_0009: ldc.i4.0 
    L_000a: stloc.2 
    L_000b: ldloc.1 
    L_000c: dup 
    L_000d: stloc.3 
    L_000e: ldloca.s flag
    L_0010: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
    L_0015: nop 
    L_0016: nop 
    L_0017: ldc.i4.5 
    L_0018: stloc.0 
    L_0019: nop 
    L_001a: leave.s L_002e
    L_001c: ldloc.2 
    L_001d: ldc.i4.0 
    L_001e: ceq 
    L_0020: stloc.s flag2
    L_0022: ldloc.s flag2
    L_0024: brtrue.s L_002d
    L_0026: ldloc.3 
    L_0027: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_002c: nop 
    L_002d: endfinally 
    L_002e: nop 
    L_002f: ret 
    .try L_000b to L_001c finally handler L_001c to L_002e
}
View Code
        public void MyLock2()
        {
            int i = 0;
            //注意isOk必須設置爲false。若是加鎖成功則會將isOk設置爲true,不然爲false。
            //在加鎖過程當中若是沒有異常,那麼會將isOk設置爲true。
            bool isOk=false;
            object o = new object();
            Monitor.Enter(o, ref isOk);
            try
            {
                i = 5;
            }
            finally
            {
                Monitor.Exit(o);
            }
        }
View Code

2.lock的參數  ide

  在給lock傳遞參數時首先要避免使用public對象,由於有可能外部程序也在對這個對象加鎖,好比下面第一段代碼。能夠看到有可能出現一種狀況,那就是主線程執行到lock(objA)時,正好線程t執行到lock(objB)。此時objA被t鎖住,objB又被主線程鎖住,死鎖就這樣發生了。其次,若是使用public對象,那麼程序員可能會使用lock(this)、lock(typeof(myType))、lock("string")。而上述三種狀況微軟都是不建議咱們使用的,對於後兩種狀況msdn已經解釋的很清楚這裏我就不寫了。多是一開始看了網上一些錯誤的帖子,讓我困擾了一段時間的是lock(this)。msdn原話是」lock(this) 可能會有問題,由於不受控制的代碼也可能會鎖定該對象。這可能致使死鎖「。我蠻想知道獲得這樣一種狀況,使用lock(this)會發送死鎖,而不使用lock(this)則不會發生死鎖。假設要發生死鎖,那麼有2個多個線程將互相等待。如今我要使用lock(this)來讓死鎖發生,所謂this就是鎖定了當前執行方法的實例對象,而這個實例對象是public的。所以這種狀況和上面使用public對象發生死鎖的本質是同樣的,那就是外部線程也要對該實例對象加鎖,從而形成了2個線程相互等待的狀況。好比下面第二段代碼,結果和第一段代碼相似(以下圖),只不過在代碼中使用了this關鍵字,一樣也發生了死鎖。學習

class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            Thread t = new Thread(myClass.LockFunc);
            t.Start();

            lock (myClass.objB)
            {
                Console.WriteLine("我是主線程,已對objB加鎖,立刻加鎖objA");
                /*
                 結果是:我是主線程,已對objB加鎖,立刻加鎖objA
                        我是線程t,已對objA加鎖,立刻加鎖objB
                 可見此時發送了死鎖,這是一種狀況,多試幾回會出現順利執行的結果。
                 */
                lock (myClass.objA)
                {
                    Console.WriteLine("我是主線程,已對objA、objB都加鎖");
                }
            }
        }
    }

    class MyClass
    {
        public object objA = new object();
        public object objB = new object();

        public void LockFunc()
        {
            lock (objA)
            {
                Console.WriteLine("我是線程t,已對objA加鎖,立刻加鎖objB");
                lock (objB)
                {
                    Console.WriteLine("我是線程t,已對objA、objB都加鎖");
                }
            }
        }
    }
View Code
    class Program
    {
        static void Main(string[] args)
        {
            object obj=new object();
            MyClass myClass = new MyClass();
            Thread t = new Thread(myClass.LockFunc);
            t.Start(obj);

            lock (myClass)
            {
                Console.WriteLine("我是主線程,已對myClass加鎖,立刻加鎖obj");
                lock (obj)
                {
                    Console.WriteLine("我是主線程,已對obj、myClass都加鎖");
                }
            }
        }
    }

    class MyClass
    {
        public void LockFunc(object obj)
        {
            lock (obj)
            {
                Console.WriteLine("我是線程t,已對obj加鎖,立刻加鎖myClass");
                lock (this)
                {
                    Console.WriteLine("我是線程t,已對obj、myClass都加鎖");
                }
            }
        }
    }
View Code

 

  如今若是要設置lock參數,咱們知道要首選私有成員。不過除了將對象設置爲私有成員外,咱們還應該最好將其設置爲只讀成員。若是沒有將對象設置爲只讀成員,那這種狀況必定要注意,由於它會讓鎖失效。代碼以下所示, 這段代碼的運行結果是下面的第一張圖。從圖中能夠看到由於使用了lock加鎖所以count永遠不會大於100且必定連續的前後出現"加20前"、"加20後"。如今將代碼中的objA=objB取消註釋再執行,獲得的結果如第二張圖所示,能夠發現如今並無線程同步了。第一句是"線程1執行中count加20前",若是線程同步那麼接下來必定是"線程1執行中count加20後",但是結果倒是線程2在執行,這說明鎖失效了。能夠看到表面緣由是改變了objA指向的對象,從而致使鎖失效。可是再往深處想,我很好奇加鎖到底在對象上作了什麼?雖然改變了objA指向的對象,但確實不是有一個對象已經被加鎖了嗎?ui

  在學習了同步塊索引後,這些問題的答案也就出現了。咱們知道每個對象內存中都有一個同步塊索引和類型指針,當在堆上建立一個對象時它的同步塊索引會被設置爲一個負數,這代表如今沒有線程對它加鎖。在CLR初始化時它會分配一個同步塊數組,這個數組的大小是能夠動態改變的且不在GC中。當使用lock對一個obj加鎖時,將會讓obj內存中的同步塊索引與CLR中的同步塊數組中的某一項關聯起來,也就是讓同步塊索引能夠索引到同步塊數組中的這一項。當再也不有須要對象同步的線程時,這個對象的同步塊索引將會被再次重置爲負數,同步塊數組的這一項將能夠繼續與對象進行關聯。而線程執行時若是發現要鎖定的對象中的同步塊索引已經指向了同步塊數組,那麼該線程將會進入等待隊列。再來看這個例子,在對objA加鎖後,objA的同步塊索引已經關聯同步塊數組。執行objA=objB後,objA執行了堆中的objB堆空間,而objB的同步塊索引是沒有加鎖的,所以線程2執行到lock(objA)時實際上是對objB加了鎖。因此線程1執行的過程當中線程2也進入到了咱們但願的同步區,結果中顯示的是線程2進入同步區後一口氣執行執行完畢。若是線程2只執行了一部分,此時線程1遇到了lock(objA),那麼此時它會等待線程2執行完畢,所以結果輸出中線程2都是一口氣執行完畢。固然在屢次運行程序過程當中還有不一樣的結果,一是線程1執行完畢後線程2才進入;二是線程1執行到lock(objA)時正好線程2已經釋放對objB的加鎖,這樣線程1就能夠順利的對objA(其實是objB)加鎖了,那麼此時線程2就要等待線程1了。this

class Program
    {
        static void Main(string[] args)
        {
            MyLock myLock=new MyLock();
            Thread thread1 = new Thread(myLock.Thread1Func);
            thread1.Start();
            Thread thread2 = new Thread(myLock.Thread2Func);
            thread2.Start();
        }
    }

    class MyLock
    {
        int count = 0;
        object objA = new object();
        object objB = new object();
        public void Thread1Func()
        {
            for (int i = 0; i < 5; i++)
                StartEatPear("線程1");
        }
        public void Thread2Func()
        {
            for (int i = 0; i < 5; i++)
                StartEatPear("線程2");
        }
        public int  StartEatPear(string str)
        {
            lock (objA)
            {
                //在臨界區中修改objA會致使鎖失效
                //objA = objB;
                if (count <100)
                {
                    Console.WriteLine(str+"執行中,count加20前:" + count);
                    count = count + 20;
                    Console.WriteLine(str + "執行中,count加20後:" + count);
                }
                else
                {
                    Console.WriteLine(str + "執行中,count將return:" + count);
                    return count;
                }
            }
            return count;
        }
        

    }
View Code

3.仍是lock(this)spa

   在寫使用this發生死鎖時,我寫着好玩用了雙this,代碼以下。剛開始我很好奇線程執行到第二個this時會不會在這裏等待,不過我以爲既然是同一個線程執行,內部應該會採起措施來讓線程繼續執行。最後結果是輸出了2條語句,線程執行到第二個this時沒有等待而是直接執行。查閱資料知道加鎖的流程後這個問題才解決。咱們知道lock實際上是調用了靜態方法Enter,這個方法首先會作判斷,若是此時這個對象沒有被鎖住且沒有線程在等待,那麼這個對象將與同步塊相關聯以達到同步的效果。不然,也就是說這個對象已被鎖住了,接下來有2種狀況。一種狀況是當前線程正好是將對象加鎖的線程,那麼此時會設置一個字段加1;另外一種狀況是當前線程不是將該對象加鎖的線程,所以當前線程只能進入等待隊列了。這樣只要是一樣的線程,即便遇到多個相同的lock語句將會接續執行而不會等待。線程

  在使用lock(this)時,因爲已對這個對象加鎖,所以其餘線程沒法再對這個對象加鎖,如今加鎖的本質也清楚了,但是這彷佛只是在調用Monitor.Enter()方法時進行的。加了鎖以後對於這個對象咱們只知道沒法再次加鎖,可是到底還可不能夠訪問呢?以下面第二段代碼,我建立的線程t1在無限循環的執行臨界區,除非isgo被設置爲false不然將不會有線程能夠對myClass對象加鎖。接着我在主線程調用NotLockMe方法,程序執行結果如代碼中所示,主線程依舊能夠訪問myClass對象。這說明加鎖並非像我原先理解的那樣會徹底將這個對象鎖住而其餘地方沒法訪問這個對象,加鎖僅僅只是讓其餘線程不能夠對已加鎖的對象再進行加鎖,這個概念要理解清楚。順其天然的對於寫加鎖代碼我有一個想法,是否是能夠設置一個bool值,若是在預期時間內沒有出現想要的結果,咱們能夠經過在外部設置這個bool值讓臨界區退出執行。小弟新手一枚,代碼寫的還不夠多,學的還不夠深刻,若有錯誤還請各位前輩指出!3d

 class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            Thread t = new Thread(myClass.LockFunc);
            t.Start();
        }
    }

    class MyClass
    {
        public void LockFunc()
        {
            lock (this)
            {
                Console.WriteLine("我是第一個this");
                lock (this)
                {
                    Console.WriteLine("我是第二個this");
                }
            }
        }
    }
View Code
 class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            Thread t1 = new Thread(myClass.LockMe);
            t1.Start();
            Thread.Sleep(100);

            //調用沒有被lock的方法
            myClass.NotLockMe();

            //結果是:
            //            I am locked
            //            I am not locked
        }
    }
    class MyClass
    {
        private bool isgo = true;
        public void LockMe()
        {
            lock (this)
            {
                while (isgo)
                {
                    Console.WriteLine("I am locked");
                    Thread.Sleep(500);
                }
            }
        }
        //全部線程均可以同時訪問的方法
        public void NotLockMe()
        {
            isgo = false;
            Console.WriteLine("I am not locked");
        }
    }
View Code

 聲明:本文原創發表於博客園,做者爲方小白,若有錯誤歡迎指出 。本文未經做者許可不準轉載,不然視爲侵權。指針

相關文章
相關標籤/搜索