雁过请留痕...
代码改变世界

基元线程同步——Interlocked Anything模式

2012-08-29 14:19  xiashengwang  阅读(963)  评论(2编辑  收藏  举报

上一篇基元线程同步——基础,非阻塞同步(VolatileRead,VolatileWrite,volatile,Interlocked) 已经对Interlocked类做了比较详细的分析,这一篇是对Interlocked类的一个模式进行补充说明。如果没用过Interlocked类,可以看看上面的这篇文章。

这个模式的名字是Jeffrey给起的,它究竟要解决什么问题,我们为什么要用它?带着这些疑问,我们来看看它的应用场景。

看看下面这个设定最大值的例子:

        private static Int32 Maximum(ref int target, int value)
        {
            int oldValue;
            oldValue = target;

            //计算最大值
            int temp = Math.Max(target, value);

            //注意:这里可能有其他线程恰好修改了target的值

            //如果target是oldValue,则替换成最大的值
            Interlocked.CompareExchange(ref target, temp, oldValue);

            return target;
        }

测试代码:

            int v1 = 10;

            v1 = Maximum(ref v1, 20);
            Console.WriteLine("v1:" + v1);

如果恰好有一个线程在Interlocked.CompareExchange这句代码执行前修改了target的值为15,那么我们这里的最大值20就不会生效,因为CompareExchange的比较条件将会失败。我们得到的输出,有可能是:v1:15。

这样的结果可能不是我们的预期,问题在于上面的代码只调用了CompareExchange一次,如果不成功就跳过了这次修改。所以,我们可以考虑对CompareExchange做一个循环判断,不成功就再度尝试修改,直到成功为止。而这正是Jeffery所说的Interlock Anything模式。

我们进一步修改上面的代码:

        private static Int32 Maximum(ref int target, int value)
        {
            int oldValue, currentValue;

            currentValue = target;
            do
            {
                //oldValue记录当前循环开始时的原始值
                oldValue = currentValue;

                //计算最大值
                int temp = Math.Max(target, value);

                //注意:这里可能有其他线程恰好修改了target的值

                //如果target是oldValue,则替换成最大的值。
                //如果被其他线程修改了,currentValue返回的将会是被其他线程修改后的最新值,比如:15
                currentValue = Interlocked.CompareExchange(ref target, temp, oldValue);
            }
            while (currentValue != oldValue);//如果被修该,则进入下一次循环,尝试再次修改

            return target;
        }

注释中已经写得很清楚了,可能你需要反复的思考一下上面的代码,理解其意图。这里只是对Int32的值进行了修改,利用这个模式实际可以对任何引用类型的值进行修改。从而避免多线程访问共同变量带来的冲突。

对于这个模式的应用,我们看看微软给我们的示范:在实现事件(Event)时是如何运用了这个模式的。

首先定义下面的一个事件:

        public delegate bool UploadFileCompleteEventHander(string fileName);
        public event UploadFileCompleteEventHander UploadFileComplete;

我们知道,事件本身还是用代理来实现的,只不过编译器帮我们做了这部分工作,反编译这个事件的代码:

public event UploadFileCompleteEventHander UploadFileComplete
{
    add
    {
        UploadFileCompleteEventHander hander2;
        UploadFileCompleteEventHander uploadFileComplete = this.UploadFileComplete;
        do
        {
            hander2 = uploadFileComplete;
            UploadFileCompleteEventHander hander3 = (UploadFileCompleteEventHander) Delegate.Combine(hander2, value);
            uploadFileComplete = Interlocked.CompareExchange<UploadFileCompleteEventHander>(ref this.UploadFileComplete, hander3, hander2);
        }
        while (uploadFileComplete != hander2);
    }
    remove
    {
        UploadFileCompleteEventHander hander2;
        UploadFileCompleteEventHander uploadFileComplete = this.UploadFileComplete;
        do
        {
            hander2 = uploadFileComplete;
            UploadFileCompleteEventHander hander3 = (UploadFileCompleteEventHander) Delegate.Remove(hander2, value);
            uploadFileComplete = Interlocked.CompareExchange<UploadFileCompleteEventHander>(ref this.UploadFileComplete, hander3, hander2);
        }
        while (uploadFileComplete != hander2);
    }
}

看见了吗,上面的add和Remove方法,对代理的添加和删除就是用的这个模式,多么经典。保证多个线程注册一个事件时,不会出现冲突。

总结:

1,一个变量可能被多线程访问时,可以考虑运用这个模式。

2,它的速度比阻塞式同步,如lock等,要快很多。

3,它能对任何类型的变量进行赋值,而不仅仅局限于Int32,等原子操作类型。