《CLR via C#》读书笔记-线程同步(二)

来源:互联网 发布:js picker 编辑:程序博客网 时间:2024/05/18 06:00

目录

  1. 互锁构造
  2. 简单的spin lock
  3. interlock anything

1、互锁构造(internallocked)
易失构造是通过volatilewrite和volatileread两个方法完成对一个字段的写与读,但这使用者两个方法时有个bug,即在字段的读写过程中会有其他线程获取这个数据,并对数据进行修改。例如:

private int v_value=0;//线程1的方法实现,当v_value等于0的时候,将字段设为1public void Thread1(){    if(VolatileRead(ref v_value)==0){        //但是这儿会存在另一个线程,使用v_value设为另外一个值        VolatileWrite(ref v_value,1);        ...    }}

即,易失构造无法通过volatileread和volatilewrite方法实现一个内存栅栏,从而保证在对数据操作过程中数据不被修改。为了解决这个问题,CLR就提供了system.threading.interlocked类提供了各种方法,其里面的每个方法执行的一次原子性的读写的操作,通过原子性的操作(读、写、判断)保证“每次只允许一个线程操作”,通过使用internallocked类而实现的最简单的构造就称之为“internallocked构造”(“互锁构造”这个翻译容易给人造成困扰,个人感觉最合适的叫法就是“internallocked构造”)。
写到这儿忽然想起上一篇忘了一个方法:MemoryBarrier()。MSDN里的说明如下:

Synchronizes memory access as follows: The processor executing the current thread cannot reorder instructions in such a way that memory accesses prior to the call to MemoryBarrier execute after memory accesses that follow the call to MemoryBarrier.

因此MemoryBarrier方法的作用就是形成一道篱笆,在篱笆之前的加载、存储等操作要在篱笆之前完成,篱笆之后的操作要在篱笆之后完成。论坛上的这个回复很犀利,很搞笑,可以看看,具体链接论坛连接。
MemoryBarrier的用法
MemoryBarrier方法可以这样使用:

x=a; //在篱笆前的操作要在“篱笆”语句之前完成(之前的语句顺序仍有可能被编译器优化)//在篱笆之后的操作要在“篱笆”语句之后完成Thread.MemoryBarrier(); //这就是篱笆y=1; 

MemoryBarrier方法不能访问内存,只是强迫编译器按照“程序顺序”完成编译。
因此,MemoryBarrier与interlocked类中方法还是有很大的差别的。话题扯远了,再回来讲讲interlocked类中常用的方法
interlocked类中常用的方法

//return ++locationpublic static int Increment(ref int location);//return --locationpublic static int Decrement(ref int location);//return (location+=value)public static int Add(ref int location,int value);//将value赋location,并返回location的旧值// int old=location; location=value; return old;public static int Exchange(ref int location,int value);//比较location与comparand,若两者相等,则将value赋予location,并返回location的旧值// int old=location;// if(location==comparand) location=value;// return oldpublic static int CompareExchange(ref int location,int value,int comparand);

这些方法都能保证原子性的访问数据。《CLR via C#》第三版的P714,作者举了一个例子,很不错。在此mark下

internal sealed class MultiWebRequests{    //这儿是作者实现的一个工具类    private AsyncCoordinator m_ac = new AsyncCoordinator();    //初始化两个请求,使用数组m_requests存储    private WebRequest[] m_requests = new WebRequest[]{        WebRequest.Create("http://baidu.com");        WebRequest.Create("http://Microsoft.com");    }    //用于存放请求结果的数组    private WebResponse[] m_results = WebResponse[2];    //构造器    public MultiWebRequests(int timeout = Timeout.Infinite){        //在对象初始化时,以异步方式发起请求        for(int i=0;i<m_requests.Length;i++){            m_ac.AboutToBegin(1);            m_requests[i].BeginGetResponse(EndGetResponse,n);        }        //通知工具类,所有的请求都已经发出,然后异步的方式调用AllDone方法        m_ac.AllBegun(AllDone, timeout);    }    //定义一个取消方法    public void Cancel(){ m_ac.Cancel(); }    //每个请求响应的处理方法    private void EndGetResponse(IAsyncResult result){        //通过n得出,处理的是哪个请求的响应        int n = result.AsyncState;        //将数据保存到响应的数组内        m_results[n]=m_requests[n].EndGetResponse(n);        //通知工具类,一个请求已经结束        m_ac.JustEnded();    }    //当处理完所有的请求后,进行相应的处理、数据展示    private void AllDone(CoordinationStatus status){        switch(status){            case CoordinationStatus.Cancel:                Console.WriteLine("The operation was cancelled");                break;            case CoordinationStatus.Timeout;                Console.WriteLine("The operation was timeout");                break;            case CoordinationStatus.AllDone;                Console.WriteLine("Here is the results from all the web server");                for(int i=0;i<m_requests.Length;i++){                    Console.WriteLine("{0} return {1} bytes",                    m_results[i].ResponseUri,m_results[i].ContentLength);                }                break;        }    }}

上面的这段代码基本上没有什么问题,就是有个m_ac.AllBegin(AllDone, timeout);这句话不解,看样子是一个APM编程方式,但是AllDone的参数有点怪。具体可以查看作者的工具类,工具类的代码如下:

//定义枚举类型的状态结果internal enum CoordinationStatus { AllDone, Timeout, Cancel};//工具类的具体定义internal sealed class AsyncCoordinator{    private int m_opCount=1;//用于记录操作数量    private int m_statusReport=0;    private Action<CoordinationStatus> m_callback;    private Timer m_timer; //    public void AboutToBegin(int opsToAdd=1){        Interlocked.Add(ref opCount,opsToAdd);    }    public void JustEnded(){        if(Interlocked.Decrement(ref opCount)==0){            ReportStatus(CoordinationStatus.AllDone);        }    }    public void AllBegun(Action<CoordinationStatus> callback,int timeout=Timeout.Infinite){        m_callback=callback;        if(timeout!=Timeout.Infinite)            m_timer = new Timer(TimeExiried,null,timeout,Timeout.Infinite);        JustEnded();    }    private void TimeExiried(object o){ ReportStatus(CoordinationStatus.Timeout); }    public void Cancel(){ ReportStatus(CoordinationStatus.Timeout); }    //在工具类中,最关键的就是这个地方。不管有几个请求,只要是任何一个触发ReportStatus方法,    //就会调用m_callback的方法    private void ReportStatus(CoordinationStatus status){        if(Interlocked.Exchange(ref m_statusReport,1)==0)            m_callback(status);    }}

真是厉害!在这儿一个小例子里,有APM、Timer、Interlocked、请求与响应结合起来。厉害!
上面的工具类就是一个用于显示状态的状态类,以后可以在自己的项目中用起来。
简单的自旋
interlocked类简单数据类型可进行原子性的读与写,但若操作一个数据、List时,如何保证只允许一个线程访问数据,这时候作者推荐了一种简单的自旋实现方式,如下:

//简单的自旋internal struct SimpleSpinLock{    private int m_ResoureInUse=0;    public void Enter(){        while(InterLocked.Exchange(m_ResoureInUse,1)==0){            //Magic Black区域        }    }    public void Leave(){        Thread.VolatileWrite(m_ResoureInUse,0);    }}

在使用时,如下所示:

public sealed class SomeResource{    private SimpleSpinLock m_sl = new SimpleSpinLock();    public void AccessResource(){        m_sl.Enter();        //相应的处理逻辑        m_sl.Leave();    }}

以上就是一个简单的自旋锁,保证每次只允许一个线程访问
简单自旋锁存在的问题
当正在自旋的线程优先级比占有锁的线程优先级高时,就会造成“活锁”。优先级高的一直在自旋,而优先级低虽占有锁,但因优先级低,无法执行相关操作,结果就造成“活锁”情况。因此这种简单的自旋锁只应该应用于保护那些执行的非常快的代码区域。
在线程中引入处理延时
在“简单的自旋”中有一行注释//Magic Black区域,用一句话来定义就是:一个先到先被坑的“陷阱”。具体理解如下:若2个线程同时访问同一数据,程序中又使用了SimpleSpinLock类来保证只允许一个线程获得同步锁,另一线程在外围自旋等待。若这个区域(//Magic Black区域)若没有任何的代码,则正在自旋的线程会非常快速的执行while循环,线程的上下文切换等耗时、耗资源的行为,导致拥有锁的线程无法尽快的处理完相关逻辑。因此有很多人在研究,如何在//Magic Black区域内让占有同步锁线程先暂停,让在外面自旋的线程先处理!待外围的线程处理完后,原来占有同步锁的线程再运行。因此称之为:一个先到先被坑的“陷阱”。
为了实现这个“陷阱”,目前有三种方法:
1、方式1:sleep
拥有同步锁的线程可以调用线程sleep方法,通过自身睡眠的方式,让出资源,提供给外围线程使用。但这儿就有一个问题,拥有同步锁的线程应该“睡眠”多久比较合适?
1.1 Sleep(Timeout.Infinate):告诉系统永远不要调用线程。
1.2 Sleep(0):告诉系统线程放弃了它剩余的时间片段,强迫系统调用其他线程。
1.3 Sleep(1):迫使系统以1毫秒为单位进行上下文切换。
2、方式2:Yield
拥有同步锁的线程,调用Yield方法,告诉系统“我很好,你不用照顾我,你去看看现是否存在已经准备就绪的线程,若有就先去执行它吧!”
3、方式3:SpinWait
这个方法就是让当前线程一直占用 CPU,一直到你指定的时间为止。
这三个方法总结:
1、sleep与SpinWait
1.1调用SpinWait方法的线程在等待的时间内一直占用CPU;Sleep是放弃了该线程的时间片段。MSDN的解释如下:

Contrast this with the Sleep method. A thread that calls Sleep yields the rest of its current slice of processor time, even if the specified interval is zero.

1.2 唤醒时间上的差别。唤醒调用Sleep的线程需要有额外的唤醒时间,而SpinWait没有唤醒时间。
1.3 MSDN补充。MSDN上建议不要使用SpinWait,尽量使用 Monitor.Enter相关方法
2 Sleep与Yield
yield方法的性能介于sleep(0)与sleep(1)之间。当线程调用yield()方法,并返回true后,系统将会调用已经就绪的线程,调用yield()方法线程的时间片段将会被用于就绪的线程。
3 Interlocked Anything
作业在interlocked构造的最后举例说明,可以通过Interlocked.CompareExchange()方法实现实现任何形式的互锁构造。其例子如下:

public static int Maximum(ref int target,int value){    int startValue=target,currentValue,desiredValue;    do{        //首先将初始值赋给currentValue变量,而startValue变量就相当于一个保存“旧值”的中间人        //startValue不会参与任何判断逻辑操作中,因此在整个过程中其值是保持不变的        currentValue=startValue;        //使用currentValue、value、desiredValue进行逻辑操作        desiredValue=Math.Max(currentValue,value);        //逻辑结束后,在推出之前需要判断target的值是否被其他线程改变        //下面这行也可以用如下进行Thread.VolatileRead(ref target)进行替代        startValue = Interlocked.CompareExchange(ref target,desiredValue,currentValue);    }while(startValue!=currentValue)    return desiredValue;}

代码中有一行:startValue = Interlocked.CompareExchange(ref target,desiredValue,currentValue);其目的就是将ref target最新值返回给startValue,然后用currentValue与其比较,从而判断target是否被其他线程修改过。

以上用户模式小节结束

https://www.codeproject.com/articles/37282/memory-model-memory-barrier-and-singleton-pattern

0 0