标签:
线程同步
CLR为每个线程分配了线程栈,用于保存本地变量,这样可以保证本地变量是独立的,案例:
static void Main(string[] args) { ThreadStart ts = new ThreadStart(Print); new Thread(ts).Start(); Print(); } static void Print() { for (int i = 0; i < 3; i++) { Console.Write(i); } }
在主线程和工作线程中都使用了i变量,但它们是相互独立的,彼此不会影响.线程之间也能共享资源,比如前面的那个shareValue变量的案例.当多个线程访问同一对象时,如果不加以协调,可能会出现结果不正确的情况:
static int i = 0; static void Main(string[] args) { ThreadEntry(); ThreadEntry(); } static void ThreadEntry() { Console.WriteLine("i={0}",i); i++; }
这段代码在主线程上执行的结果为:
i=0 i=1
如果让一个ThreadEntry(0方法由主线程执行,另一个由工作线程执行:
static void Main(string[] args) { new Thread(ThreadEntry).Start(); ThreadEntry(); }
结果可能是:
i=0 i=0
这是因为两个线程同时进入了ThreadEntry()方法,向控制台输出后采取修改i的值.如果i作为if或者while的判断语句,例如(ifi==1){//},那么这段代码能否执行时不确定的.
线程同步就是协调多个线程间的并发操作,已获得符合预期的,确定的执行结果,消除多线程应用程序中的不确定性,它包含两个方面:
(1).保护资源(或代码),即确保资源(或代码)同时只能由一个线程(或指定个数的线程)访问,一般措施是获取锁和释放锁(后面简称锁机制).
(2).协调线程对资源(或代码)的访问顺序,即确定某一资源(或代码)只能先有线程T1访问,再由线程T2访问,一般措施是采用信号量(后面简称信号量机制).当T2线程访问资源时,必须等待线程T1先访问,线程T2访问完后,发出信号量,通知线程T2可以访问.
System.Threading.Monitor对资源进行保护的思路很简单,即使用排它锁.当线程A需要访问某一资源(对象,方法,类型成员,代码段)时,对其进行加锁,线程A获取到锁以后,任何其他线程吐过再次对资源进行访问,则将其放到等待队列中,直到线程A释放锁以后,再将线程从队列中取出.
Monitor的Enter()静态方法用于获取锁,Exit()静态方法用于释放锁.
public static void Enter(object obj);
public static void Exit(object obj);
现在使用Monitor解决和上面类似的问题,创建一个自定义的类型Resource,然在主线程和worker线程上调用他的Record()方法:
public class Resource { public string Called; public void Record() { this.Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond); Console.WriteLine(Called); } } class Program { private Resource res = new Resource(); static void Main(string[] args) { Thread.CurrentThread.Name = "Main"; Program p = new Program(); Thread worker = new Thread(p.ThreadEntry); worker.Name = "Worker"; worker.Start(); p.ThreadEntry(); } void ThreadEntry() { Monitor.Enter(res); res.Record(); Monitor.Exit(res); } }
上面的代码将ThreadEntry()改为了实例方法,因此要先创建一个Program的实例.在ThreadEntry()方法中,在访问res对象前对其进行枷锁,在访问之后释放锁.
通过输出结果可以看到Main()线程先占用了res,并调用Record(),而worker线程后占用res,两个线程的切换时间还是存在的.如果注释掉Monitro.Enter()和Monitor.Exit()语句,此时再看一下输出.
因为两个线程同时进入到了Record()并获取到了Called,而此时Called为空.
Monitor有一个限制,就是只能对引用类型加锁.如果将上面的Resource类型改为结构类型,则会抛出异常.解决的办法是将锁加在其他的引用类型上,比如System.Object.此时,线程并不会访问该引用类型的任何属性和方法,该对象的作用仅仅是协调各个线程.换言之,之前的操作流程是占用A,操作A,释放A.现在的流程是占用B,操作A,释放B.当这样做的时候,要保证所有线程在加锁和释放锁的时候都是针对同一个对象B.
public struct Resource { public string Called; public void Record() { this.Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond); Console.WriteLine(Called); } } class Program { private Resource res = new Resource(); private object lockObj = new object(); static void Main(string[] args) { Thread.CurrentThread.Name = "Main"; Program p = new Program(); Thread worker = new Thread(p.ThreadEntry); worker.Name = "Worker"; worker.Start(); p.ThreadEntry(); } void ThreadEntry() { Monitor.Enter(lockObj); res.Record(); Monitor.Exit(lockObj); } }
使用这种方式的时候,需要创建一个没有实际意义的,专门用于协调线程的对象.对上面的案例进行以下修改,让他们锁定不同的对象:
class Program { private Resource res = new Resource(); private object lockObj = new object(); private object lockObj2 = new object(); static void Main(string[] args) { Thread.CurrentThread.Name = "Main"; Program p = new Program(); ParameterizedThreadStart ts = new ParameterizedThreadStart(p.ThreadEntry); Thread worker = new Thread(p.ThreadEntry); worker.Name = "Worker"; worker.Start(p.lockObj); p.ThreadEntry(p.lockObj2); } void ThreadEntry(object obj) { Monitor.Enter(obj); res.Record(); Monitor.Exit(obj); } }
观察一下输出又不符合预期了.
采用上面的方式时,需要创建一个专门用于协调线程的System.Object对象,有时候显得代码不够简洁.另一种做法是使用System.Type对象作为锁,使用System.Type对象是基于这样一个事实:多次调用typeof(Type)获取的是同一个对象:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; namespace 线程同步 { public static class Resource { public static string Called; public static void Record() { Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond); Console.WriteLine(Called); } } class Program { static void Main(string[] args) { Program p = new Program(); Type t1 = typeof(Resource); Type t2 = typeof(Resource); Type t3 = typeof(Resource); Console.WriteLine(t1==t2); Console.WriteLine(t2==t3); } void ThreadEntry() { Monitor.Enter(typeof(Resource)); Resource.Record(); Monitor.Exit(typeof(Resource)); } } }
这样就又可以了.
考虑这样一种情况:Record()方法抛出了异常.假设主线程先执行ThreadEntry()方法,那么当Record()抛出异常之后,将不会执行Monitor.Exit()方法.程序的输出结果可能是:Main[569],并且会一直等待下去.因为woker线程是前台线程,他一直在等待释放锁,而主线程知道结束都未释放锁.解决的方法是将worker线程设为后台线程,但前面已经说过这是不妥的做法,用这种方式结束线程是不合适的.还有一种方法是将Monitor.Exit()方法放到finally块中:
void ThreadEntry() { Monitor.Enter(res); try { res.Record(); } finally { Monitor.Exit(res); } }
由于Monitor的这种规模是太常见了,.NET提供了lock语句进行简化,上面的代码等价于:
void ThreadEntry() { lock (res) { res.Record(); } }
lock语句只专注于获取锁,释放锁,并不提供处理异常的简写方法,如果要处理异常,还需要使用try/catch块:
void ThreadEntry() { lock (res) { try { res.Record(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } }
类型Resource不是类型安全的,它的内部并没有采取线程安全(Thread-Safe)的措施.最好将获取锁,释放锁的逻辑放到Resource内部实现:
public class Resource { public string Called; public void Record() { lock (this) { this.Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond); Console.WriteLine(Called); } } }
因为这种模式常见,因此,.NET提供了内置的编译器支持,称作同步方法(synchronized methods),只要为方法添加System.Runtime.CompilerServices.MethodImpl标记,并将位置参数设置为MethodImplOptions.Synchronized即可,下面方法的效果与上面的相同的:
[MethodImpl(MethodImplOptions.Synchronized)] public void Record() { this.Called += string.Format("{0}[{1}]", Thread.CurrentThread.Name, DateTime.Now.Millisecond); Console.WriteLine(Called); }
也能对类型的成员访问加锁:
public class Resource { private int _index; public int Index { get { lock (this) { return _index; } } set { lock (this) { _index = value; } } } }
上面的锁的粒度太小了,如果一个类的字段太多,那就会麻烦死,也容易形成BUG,所以将ThreadEntry()改为:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; using System.Runtime.CompilerServices; namespace 线程同步 { public class Resource { private int _index; public int Index { get { lock (this) { return _index; } } set { lock (this) { _index = value; } } } } class Program { static Resource res = new Resource(); void ThreadEntry() { for (int i = 0; i <= 2; i++) { res.Index = res.Index + 1; } } static void Main(string[] args) { Thread.CurrentThread.Name = "Main "; Program p = new Program(); Thread worker = new Thread(p.ThreadEntry); worker.Name = "Worker"; worker.Start();//worker线程执行一遍 p.ThreadEntry();//主线程执行一遍 Console.WriteLine(res.Index); } } }
res.Index=res.Index+1语句先调用了get代码段,随后调用了set代码段.我盟期望的结果是6,但实际结果是不确定的.因为两个线程可能先后进入了get语句,获取了相同的Index值,然后又先后进入了set语句段,最终的结果可能是3.如果想获取期望的结果,那么应该讲res.Index=res.Index+1视为操作单元为其加锁,此时get和set代码段才会顺序执行.
void ThreadEntry() { for (int i = 0; i <= 2; i++) { lock (res) { res.Index = res.Index + 1; Console.WriteLine("{0}: {1}",Thread.CurrentThread.Name,res.Index); } } }
能看到输出结果是交替的,如果想要他们顺序输出,则需要再次提高锁的粒度,将锁加在for循环外部:
void ThreadEntry() { lock (res) { for (int i = 0; i <= 2; i++) { res.Index = res.Index + 1; Console.WriteLine("{0}: {1}", Thread.CurrentThread.Name, res.Index); } } }
可见,锁的粒度对于程序执行的顺序和结果是很重要的.
前面使用Monitor保证了资源只能同时由一个线程访问,但是没有限制资源先由T1访问还是T2访问,由于Start()方法实际执行时间的不确定,因此结果可能是主线程先访问,也可能是worker线程先访问.通常,两个线程执行的是不同的任务,比如工作线程获取并计算数据,主线程显示数据.那么此时顺就很重要,来看下面的案例:
public class Resource { public string Data; } class Program { private Resource res = new Resource(); void ThreadEntry() { res.Data = "Retrived"; Monitor.Pulse(res); } static void Main(string[] args) { Thread.CurrentThread.Name = "Main "; Program p = new Program(); Thread worker = new Thread(p.ThreadEntry); worker.Name = "Worker"; worker.Start(); Console.WriteLine("Data={0}",p.res.Data); } }
其中worker线程用于获取数据(设置res.Data属性的值),主线程则用于显示数据,上面的结果虽然会报错,但是第一行可能是”Data=”,因为主线程运行到Console.WriteLine()语句的时候,worker线程尚未执行.咱们使用Monitor类型的Wait()和Pulse()静态方法:
public static bool Wait(object obj); public static void Pulse(object obj);
Wait(0用于暂停当前线程并等待信号(调用了Wait()方法的线程,状态为WaitSleepJoin),Pulse()方法则用于发出信号,收到信号的新城将会执行后续代码.这两个方法都必须置于lock块内,并且两个方法接收的对象与lock接受的对象相同.修改上面的代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; using System.Runtime.CompilerServices; namespace 线程同步 { public class Resource { public string Data; } class Program { private Resource res = new Resource(); void ThreadEntry() { lock (res) { res.Data = "Retrived"; Monitor.Pulse(res); } } static void Main(string[] args) { Thread.CurrentThread.Name = "Main "; Program p = new Program(); Thread worker = new Thread(p.ThreadEntry); worker.Name = "Worker"; worker.Start(); lock (p.res) { if (string.IsNullOrEmpty(p.res.Data)) { Monitor.Wait(p.res); } Console.WriteLine("Data={0}",p.res.Data); } } } }
Main()方法中的if语句很重要.因为有可能worker线程已经向res.Data赋值并执行过了Monitor.Pulse(),如果此时至宣城再去执行Mointor.Wait(),那么它永远也等不到这个信号量,会一直等待下去.可以删除if语句,然后在worker.Start()语句后添加Thread.Sleep(100)来模拟这种情况:
worker.Start();
Thread.Sleep(100);
lock (p.res)
{
Monitor.Wait(p.res);
Console.WriteLine("Data={0}", p.res.Data);
}
此时程序会阻塞在Wait()方法的位置,为了避免这种情况,可以调用Wait()的重载方法,传入一个int类型的参数,该参数指示线程等待的时间,以毫秒为单位.如果在等待期间收到信号(其他线程调用了Monitor.Pulse()),则返回true;如果超时,则返回false.修改lock块中的语句:
lock (p.res) { bool isTimeOut = Monitor.Wait(p.res,100); Console.WriteLine(isTimeOut); Console.WriteLine("Data={0}", p.res.Data); }
有时候,多个线程等待同一个数据,此时可以调用Monitor.PulseAll()方法,通知所有等待的线程.
使用Monitor进行线程同步,可能会出现的一种比较棘手的问题就是死锁,简单来说死锁就是两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力进行干预(例如调用Abort()),则它们都将无法推进下去.一个线程T1占用资源R1,同时试图访问资源R2,另一个线程T2占用资源R2,同时试图访问资源R1.结果就是两个线程彼此等待,谁也无法进行下去.案例如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; using System.Runtime.CompilerServices; namespace 线程同步 { public class Resource { public string Data; } class Program { private Resource mainRes = new Resource() { Data="mainRes"}; private Resource workerRes = new Resource() { Data="workerRes"}; static void Main(string[] args) { } static void T1(Program p) { lock (p.mainRes) { Thread.Sleep(10); lock (p.workerRes) { Console.WriteLine(p.workerRes.Data);//死锁 } } } void T2() { lock (workerRes) { Thread.Sleep(10); lock (mainRes) { Console.WriteLine(mainRes.Data);//死锁 } } } } }
主线程占用了mainRes,同时尝试访问workerRes;工作线程占用了workerRes,同时尝试访问mainRes,最后的结果是两个线程都卡在了内部的elock语句的位置.上面问题解决方案是使用Monitor.TryEnter()方法,这个方法不会像Monitor.Enter()方法那样一直等待锁,而是立即返回.如果能获取锁,返回true,否则返回false.可以修改内部的lock语句,使用for循环,尝试三次获取锁,如果失败则放弃并退出.修改T1,T2.
static void T1(Program p) { lock (p.mainRes) { Thread.Sleep(10); int i = 0; while (i < 3) { if (Monitor.TryEnter(p.workerRes)) { Console.WriteLine(p.workerRes.Data); Monitor.Exit(p.workerRes); break; } else { Thread.Sleep(1000); } i++; } if (i == 3) { Console.WriteLine("{0}: Tried 3 times , Deadlock", Thread.CurrentThread.Name); } } }
在实际开发中,在哪个位置产生了死锁事先不知道,但可以使用这里的方法来对可能产生死锁的位置进行测试.
线程的东西就先说这么多,这里楼主关于.NET的东西就告一段落了,以后楼主的打算是学学Linux!!!还有Docker!!!顺道学学Ruby,这三个东西以前楼主都是学过的,在Linux上玩玩Docker,Kali Linux的大部分文件都是Ruby写的,所以这三个东西的应该一起学起来不难.
标签:
原文地址:http://blog.csdn.net/shanyongxu/article/details/51264482