使用线程时最头痛的就是共享资源的同步问题,处理不好会得到错误的结果,C#处理共享资源有以下几种:
1、lock锁
需要注意的地方:
1).lock不能锁定空值某一对象可以指向Null,但Null是不需要被释放的。(请参考:认识全面的null)
2).lock不能锁定string类型,虽然它也是引用类型的。因为字符串类型被CLR“暂留”
这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中
的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
3).lock锁定的对象是一个程序块的内存边界
4).值类型不能被lock,因为前文标红字的“对象被释放”
,值类型不是引用类型的
5).lock就避免锁定public 类型或不受程序控制的对象。
应用场景:经常会应用于防止多线程操作导致公用变量值出现不确定的异常,用于确保操作的安全性
2、
互斥锁(Mutex)
互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它。
互斥锁可适用于一个共享资源每次只能被一个线程访问的情况
我们可以把Mutex看作一个出租车,乘客看作线程。乘客首先等车,然后上车,最后下车。当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而 线程与C# Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待C# Mutex对象被释放,如果它等待的C# Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个 C# Mutex对象的线程都只有等待。
如果要获取一个互斥锁。应调用互斥锁上的WaitOne()方法,该方法继承于Thread.WaitHandle类
它处于等到状态直至所调用互斥锁可以被获取,因此该方法将组织住主调线程直到指定的互斥锁可用,如果不需要拥有互斥锁,用ReleaseMutex方法释放,从而使互斥锁可以被另外一个线程所获取.
3、semaphore
其中lock 和mutex 差不多,都是锁定同一个资源,不同之处mutex在整个进程中都可以访问到。
而semaphore是锁定多个资源,比如同一时期只能有两个线程访问,其它线程只能等待其中之一释放锁才能使用,Semaphore就是一个可以多次进入的“Mutex”。Mutex永远只允许一个线程拥有它,而Semaphore可以允许多个线程请求,因此Semaphore被用于管理一次可以允许多个线程进入并发访问资源的情况。
下面是一个简单的例子,:
class Program { static Semaphore sp = new Semaphore(2,2); static void Main(string[] args) { DoWork(); Console.Read(); } private static void DoWork() { for (int i = 0; i < 10; i++) { Task.Run(() => { sp.WaitOne(); Console.WriteLine("线程:"+Thread.CurrentThread.ManagedThreadId+",开始运行"); Thread.Sleep(new Random().Next(1000)); Console.WriteLine("线程:" + Thread.CurrentThread.ManagedThreadId + ",结束此运行"); sp.Release(); }); } } }
另举一个复杂一些的例子:学生都去图书馆查资料,图书馆共有3台电脑,如果来的人超过3人则需要排队等待,此例子中还要注意一点,那个学生选择那台电脑,学生找空闲电脑用Mutex锁定电脑对象,否则定位的电脑可能是错误的(可能会出现多名同学使用同一台电脑的情况,使用mutex锁定资源,这样才能确保一台空闲电脑只能是一名学生选择)
class Program { //图书馆拥有的公用计算机 private const int ComputerNum = 3; private static Computer[] LibraryComputers; //同步信号量 public static Semaphore sp = new Semaphore(ComputerNum, ComputerNum); static void Main(string[] args) { //图书馆拥有ComputerNum台电脑 LibraryComputers = new Computer[ComputerNum]; for (int i = 0; i < ComputerNum; i++) LibraryComputers[i] = new Computer("Computer" + (i + 1).ToString()); int peopleNum = 0; Random ran = new Random(); Thread user; System.Console.WriteLine("敲任意键模拟一批批的人排队使用{0}台计算机,ESC键结束模拟……", ComputerNum); //每次创建若干个线程,模拟人排队使用计算机 while (System.Console.ReadKey().Key != ConsoleKey.Escape) { peopleNum = ran.Next(0, 10); System.Console.WriteLine(" 有{0}人在等待使用计算机。", peopleNum); Task[] ts = new Task[peopleNum]; for (int i = 0; i <peopleNum; i++) { int n = i+1; ts[i]=Task.Run(() => { UseComputer("User" + n.ToString()); }); } Task.WaitAll(ts); Console.WriteLine("All threads finished!"); } } static Mutex m = new Mutex(); //线程函数 static void UseComputer(Object UserName) { sp.WaitOne();//等待计算机可用 //查找可用的计算机 Computer cp = null; m.WaitOne(); for (int i = 0; i < ComputerNum; i++) { if (LibraryComputers[i].IsOccupied == false) { LibraryComputers[i].IsOccupied = true; cp = LibraryComputers[i]; break; } } m.ReleaseMutex(); //使用计算机工作 cp.Use(UserName.ToString()); //不再使用计算机,让出来给其他人使用 sp.Release(); } } class Computer { public readonly string ComputerName = ""; public Computer(string Name) { ComputerName = Name; } //是否被占用 public bool IsOccupied = false; //人在使用计算机 public void Use(String userName) { System.Console.WriteLine("{0}开始使用计算机{1}", userName, ComputerName); Thread.Sleep(new Random().Next(2000, 6000)); //随机休眠,以模拟人使用计算机 System.Console.WriteLine("{0}结束使用计算机{1}", userName, ComputerName); IsOccupied = false; } }
4.
AutoResetEvent 允许线程通过发信号互相通信。通常,此通信涉及线程需要独占访问的资源。
线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号。如果 AutoResetEvent 处于非终止状态,则该线程阻塞,并等待当前控制资源的线程
通过调用 Set 发出资源可用的信号。
调用 Set 向 AutoResetEvent 发信号以释放等待线程。AutoResetEvent 将保持终止状态,直到一个正在等待的线程被释放,然后自动返回非终止状态。如果没有任何线程在等待,则状态将无限期地保持为终止状态。
可以通过将一个布尔值传递给构造函数来控制 AutoResetEvent 的初始状态,如果初始状态为终止状态,则为 true;否则为 false。
举例:面试时,每次叫一个,只能一个人进去。
class Program { static AutoResetEvent are = new AutoResetEvent(false); static void Main(string[] args) { //10个人排队,叫一声,进一个 for (int i = 0; i < 10; i++) { Task.Run(() => { are.WaitOne(); Console.WriteLine(Thread.CurrentThread.ManagedThreadId+"进门"); Thread.Sleep(new Random().Next(1000)); Console.WriteLine(Thread.CurrentThread.ManagedThreadId + "出门"); }); } System.Console.WriteLine("G:放行,ESC:退出 "); Action action = new Action(() => { are.Set(); }) ; while (true) { ConsoleKey key = Console.ReadKey(true).Key; if (key == ConsoleKey.G) action(); if (key == ConsoleKey.Escape) { break; } } } }
5、ManualResetEvent
ManualResetEvent就像一个信号灯,可以利用它的信号,控制当前线程是挂起状态还是运行状态。
它有几个常用的方法:Reset(),Set(),WaitOne();
初始化该对象时,可以指定其默认的状态(有信号/无信号);
在初始化以后,该对象将保持原来的状态不变,直到它的Reset()或者Set()方法被调用;
Reset()方法将其设置为无信号状态,Set()方法将其设置为有信号状态;
WaitOne()方法在无信号状态下,可以使当前线程挂起;注意这里说的是当前线程;
直到调用了Set()方法,该线程才被激活。
在多线程的代码里,可以使用一个ManualResetEvent对象来控制线程所有线程;
只要在调用WaitOne()方法前,调用Reset()方法,因为WaitOne()控制的是当前线程;
但是这样做,ManualResetEvent对象的管理逻辑会变得复杂;
所以这里建议一条线程一个ManualResetEvent对象。
举例://模拟3辆汽车过红绿灯
class Program { static ManualResetEvent mre = new ManualResetEvent(false); static void Main(string[] args) { //模拟3辆汽车过红绿灯 for (int i = 0; i < 3; i++) { Task.Run(() => { int count = 0; while (true) { mre.WaitOne(); Thread.Sleep(new Random().Next(5000)); count++; Console.WriteLine(Thread.CurrentThread.ManagedThreadId + "第{0}次开始运行", count); } }); } Action stop = delegate() { mre.Reset(); Console.WriteLine("红灯"); }; Action go = delegate() { mre.Set(); Console.WriteLine("绿灯"); }; System.Console.WriteLine("G:绿灯,R:红灯 "); while (true) { var k = Console.ReadKey(true).Key; if (k == ConsoleKey.G) { go(); } else if (k == ConsoleKey.R) { stop(); } else { System.Console.WriteLine("G:绿灯,R:红灯 "); } } } }