UnityC#的lock用法简记

简述

多线程环境中,不使用lock锁,会形成竞争条件,导致错误。
使用lock锁可以保证当有线程操作某个共享资源时,能使该代码块按照指定的顺序执行,其他线程必须等待直到当前线程完成操作。
即是多线程环境,如果一个线程锁定了共享资源,需要访问该资源的其他线程则会处于阻塞状态,并等待直到该共享资源接触锁定。

private object o = new object();//创建一个对象
public void Work()
{
    
    
  lock(o)//锁住这个对象
  {
    
    
    //做一些必须按照顺序做的事情
  }
}

代码实例

一、单线程

看此代码,是从上面开始执行,先执行A,再执行B,这就是单线程程序,按照顺序执行的,此时结果是可以控制的。

using System.Threading;
using UnityEngine;

public class Program : MonoBehaviour
{
    
    
    static int a = 0;
    static int b = 0;
    private static object o = new object();

    void Start()
    {
    
    
        A();
        B();
    }

    private static void A()
    {
    
    
        a += 2;
        Debug.Log("我是A方法,a=" + a);
        Thread.Sleep(5000);//暂停5秒
        b += 2;
        Debug.Log("我是A方法,b=" + b);
    }
    private static void B()
    {
    
    
        b++;
        Debug.Log("我是B方法,b=" + b);
        Thread.Sleep(1000); //暂停1秒
        a++;
        Debug.Log("我是B方法,a=" + a);
    }
}

结果

二、多线程无lock

我们增加了多线程,就是让A和B方法同时执行,此时,结果就是不可控制的。有时候先执行B方法,有时候先执行A方法。

void Start()
{
    
    
    //A();
    //B();
    Thread t1 = new Thread(A);
    Thread t2 = new Thread(B);
    t1.Start();
    t2.Start();
}

先执行A方法 :
A
先执行B方法:
B
对于为什么先执行A,后执行B,或者先执行B,后执行A,这个是操作系统根据CPU自动计算出来的。可见,我们的问题就来了。能不能这样,既能多线程执行,又可控制A和B的顺序呢?这个就要用到lock了。

三、多线程使用lock

所以,我们要的效果就是,在多线程的情况下,要么先执行A,要么先执行B。不能让A和B进行嵌套执行,必须按照顺序。程序一旦进入lock,那么就锁住,锁住的这段代码,此时只能有一个线程去访问,只有等这个线程访问结束了,其他线程才能访问。
为了增加对比,我们再增加一个C方法:

using System.Threading;
using UnityEngine;

public class Program : MonoBehaviour
{
    
    
    static int a = 0;
    static int b = 0;
    private static object o = new object();

    void Start()
    {
    
    
        //A();
        //B();
        Thread t1 = new Thread(A);
        Thread t2 = new Thread(B);
        t1.Start();
        t2.Start();
        Thread t3 = new Thread(C);
        t3.Start();
    }

    private static void A()
    {
    
    
        lock (o)
        {
    
    
            a += 2;
            Debug.Log("我是A方法,a=" + a);
            Thread.Sleep(5000);//暂停5秒
            b += 2;
            Debug.Log("我是A方法,b=" + b);
        }
    }
    private static void B()
    {
    
    
        lock (o)
        {
    
    
            b++;
            Debug.Log("我是B方法,b=" + b);
            Thread.Sleep(1000); //暂停1秒
            a++;
            Debug.Log("我是B方法,a=" + a);
        }
    }
    private static void C()
    {
    
    
        Debug.Log("我是C方法,随机出现");
    }
}

结果:

  1. C随机出现;先A,再B。
    结果一
  2. A先运行;再随机除了C,因为C没有被lock,所以C没有得到控制;最后完成了A;再运行B。
    结果二
  3. C随机出现;先B,再A。
    结果三

死锁

使用lock时注意共享资源使用,不然可能造成deadlock
使用monitor类 其拥有TryEnter方法,该方法接收一个超时参数。如果我们能够在获取被lock保护的资源之前,超时参数过期。则该方法会返回false

注意

我们lock的一般是对象,不是值类型和字符串。

  1. 为什么不能lock值类型?
    比如lock(1)呢?lock本质上Monitor.EnterMonitor.Enter会使值类型装箱,每次lock的是装箱后的对象。lock其实是类似编译器的语法糖,因此编译器直接限制住不能lock值类型。退一万步说,就算能编译器允许你lock(1),但是object.ReferenceEquals(1,1)始终返回false因为每次装箱后都是不同对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样能够访问里面的代码,达不到同步的效果。同理lock((object)1)也不行。
  2. lock字符串?
    那么lock("xxx")字符串呢?MSDN上的原话是:

锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。

  1. MSDN推荐的lock对象
    通常,最好避免锁定public类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则lock(this)可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。
    而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。
    自定义类推荐用私有的只读静态对象,比如:private static readonly object obj = new object();
    为什么要设置成只读的呢?这是因为如果在lock代码段中改变obj的值,其它线程就畅通无阻了,因为互斥锁的对象变了,object.ReferenceEquals必然返回false

拓展

lock->Invoke

C#的环境.Net Framenwok 4.8。
在主线程和线程通使用lock同步Invoke(new Action(() =>{})会导致死锁
多线程处理先加锁后委托会死锁:

lock (logShowLock)
{
    
    
    Invoke(new Action(() => {
    
     Console.WriteLine("test"); }));
}

优化方法:

Invoke(new Action(() =>
{
    
    
    lock (logShowLock)
    {
    
    
        Console.WriteLine("test");
    }
}));

Monitor

其实lock相当于Monitor

lock(o);
{
    
    
	do 
}

等价于

Monitor.Enter(o);
{
    
    
	do
}
Monitor.Exit(o);

真正实现了线程同步功能的,就是System.Threading.Monitor类型,lock关键字只是用来代替调用EnterExit方法。
Enter相当于进入这个代码块,Exit是退出这个代码块。当这个代码块再运行的时候,其他线程就不能访问。Monitor中的{}可以去掉,不影响。
可以将上面lock示例修改为Monitor,如下:

using System.Threading;
using UnityEngine;

public class Program : MonoBehaviour
{
    
    
    static int a = 0;
    static int b = 0;
    private static object o = new object();

    void Start()
    {
    
    
        //A();
        //B();
        Thread t1 = new Thread(A);
        Thread t2 = new Thread(B);
        t1.Start();
        t2.Start();
        Thread t3 = new Thread(C);
        t3.Start();
    }

    private static void A()
    {
    
    
        Monitor.Enter(o);
        {
    
    
            a += 2;
            Debug.Log("我是A方法,a=" + a);
            Thread.Sleep(5000);//暂停5秒
            b += 2;
            Debug.Log("我是A方法,b=" + b);
        }
        Monitor.Exit(o);
    }
    private static void B()
    {
    
    
        Monitor.Enter(o);
        {
    
    
            b++;
            Debug.Log("我是B方法,b=" + b);
            Thread.Sleep(1000); //暂停1秒
            a++;
            Debug.Log("我是B方法,a=" + a);
        }
        Monitor.Exit(o);
    }
    private static void C()
    {
    
    
        Debug.Log("我是C方法,随机出现");
    }
}

参考链接

  1. https://blog.csdn.net/u012563853/article/details/124767902
  2. https://www.bbsmax.com/A/Gkz1xx0GdR/
  3. https://blog.csdn.net/u011555996/article/details/103916175
  4. https://blog.csdn.net/weixin_44193637/article/details/127617322

猜你喜欢

转载自blog.csdn.net/f_957995490/article/details/128736353