多线程环境中,不使用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);}
}
我们增加了多线程,就是让A和B方法同时执行,此时,结果就是不可控制的。有时候先执行B方法,有时候先执行A方法。
void Start()
{//A();//B();Thread t1 = new Thread(A);Thread t2 = new Thread(B);t1.Start();t2.Start();
}
先执行A方法 :
先执行B方法:
对于为什么先执行A,后执行B,或者先执行B,后执行A,这个是操作系统根据CPU自动计算出来的。可见,我们的问题就来了。能不能这样,既能多线程执行,又可控制A和B的顺序呢?这个就要用到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方法,随机出现");}
}
结果:
lock
,所以C没有得到控制;最后完成了A;再运行B。使用lock
时注意共享资源使用,不然可能造成deadlock。
使用monitor
类 其拥有TryEnter
方法,该方法接收一个超时参数。如果我们能够在获取被lock
保护的资源之前,超时参数过期。则该方法会返回false
。
我们lock
的一般是对象,不是值类型和字符串。
lock
值类型?lock(1)
呢?lock
本质上Monitor.Enter
,Monitor.Enter
会使值类型装箱,每次lock
的是装箱后的对象。lock
其实是类似编译器的语法糖,因此编译器直接限制住不能lock
值类型。退一万步说,就算能编译器允许你lock(1)
,但是object.ReferenceEquals(1,1)
始终返回false
(因为每次装箱后都是不同对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样能够访问里面的代码,达不到同步的效果。同理lock((object)1)
也不行。lock
字符串?lock("xxx")
字符串呢?MSDN上的原话是:锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。
lock
对象public
类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则lock(this)
可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。lock(this)
只对当前对象有效,如果多个对象之间就达不到同步的效果。private static readonly object obj = new object();
lock
代码段中改变obj
的值,其它线程就畅通无阻了,因为互斥锁的对象变了,object.ReferenceEquals
必然返回false
。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");}
}));
其实lock
相当于Monitor
:
lock(o);
{do
}
等价于
Monitor.Enter(o);
{do
}
Monitor.Exit(o);
真正实现了线程同步功能的,就是System.Threading.Monitor
类型,lock
关键字只是用来代替调用Enter
、Exit
方法。
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方法,随机出现");}
}