欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#多线程编程中的锁系统(二)

程序员文章站 2022-07-22 19:39:34
上章主要讲排他锁的直接使用方式。但实际当中全部都用锁又太浪费了,或者排他锁粒度太大了。 这一次我们说说升级锁和原子操作。 目录 1:volatile 2: ...

上章主要讲排他锁的直接使用方式。但实际当中全部都用锁又太浪费了,或者排他锁粒度太大了。 这一次我们说说升级锁和原子操作。

目录
1:volatile
2:  interlocked
3:readerwriterlockslim
4:总结

一:volatile

简单来说: volatile关键字是告诉c#编译器和jit编译器,不对volatile标记的字段做任何的缓存。确保字段读写都是原子操作,最新值。

这不就是锁吗?   其这货它根本不是锁, 它的原子操作是基于cpu本身的,非阻塞的。 因为32位cpu执行赋值指令,数据传输最大宽度4个字节。

所以只要在4个字节以下读写操作的,32位cpu都是原子操作。volatile 它就是利用这个特性来的。

好残酷的事实?不然,微软大法这样是为了提高jit性能效率,对有些数据进行缓存了(多线程下)。

复制代码 代码如下:

  //正确
       public volatile int32 score1 = 1;
        //报错
        public volatile int64 score2 = 1;

看上面的例子,我们定义8个字节长度score2就不行了。  因为8个字节,32位cpu就分成2个指令执行了。自然就无法保证原子操作了。

这么细节的,忘了怎么办,那岂不是坑人啊。  于是微软大法直接一棍子打死,限制4个字节以下的类型字段才能用volatile,具体什么、看msdn吧。

C#多线程编程中的锁系统(二)

那今天我知道了。我编译平台改成64位上,只在64位cpu用volatile  int64,行不行?  不行,编译器报错。说了一棍子打死了。。

(^._.^)ノ  好吧,其实可以用intptr这个。

 volatile多数情况下很有用处的,毕竟锁的性能开销还是很大的。我们可以把当成轻量级的锁,根据具体场景合理使用,能提高不少程序性能。

线程中的thread.volatileread 和thread.volatilewrite 就是volatile的复杂版。

二:interlocked

msdn 描述:为多个线程共享的变量提供原子操作。主要函数如下:

interlocked.increment    原子操作,递增指定变量的值并存储结果。
interlocked.decrement       原子操作,递减指定变量的值并存储结果。
interlocked.add        原子操作,添加两个整数并用两者的和替换第一个整数

interlocked.compareexchange(ref a, b, c);  原子操作,a参数和c参数比较,  相等b替换a,不相等不替换。

基本用法就不多说了。直接来段clr via c# interlock anything的例子:

复制代码 代码如下:

public static int maximum(ref int target, int value)
        {
            int currentval = target, startval, desiredval;  //记录前后值
            do
            {
                startval = currentval; //记录循环迭代的初始值。
                desiredval = math.max(startval, value); //基于startval和value计算期望值desiredval

                //高并发下,线程被抢占情况下,target值会发生改变。

                //target startval相等说明没改变。desiredval 直接替换。
                currentval = interlocked.compareexchange(ref target, desiredval, startval);

            } while (startval != currentval); //不相等说明,target值已经被其他线程改动。自旋继续。
            return desiredval;
        }

三:readerwriterlockslim

假如我们有份缓存数据a,如果每次都不管任何操作lock一下,那么我的这份缓存a就永远只能单线程读写了, 这在web高并发下是不能忍受的。

那有没有一种办法我只在写入时进入独占锁呢,读操作时不限制线程数量呢?答案就是我们的readerwriterlockslim主角,读写锁。

readerwriterlockslim 其中一种锁enterupgradeablereadlock最关键  即可升级锁。 

它呢允许你先进入读锁,发现缓存a不一样了, 再进入写锁,写入后退回读锁模式。

ps: 这里注意下net 3.5之前有个readerwriterlock 性能较差。推荐使用升级版的 readerwriterlockslim 。

复制代码 代码如下:

//实例一个读写锁
 readerwriterlockslim cachelock = new readerwriterlockslim(lockrecursionpolicy.supportsrecursion);

上面实例一个读写锁,这里注意的是构造函数的枚举。

lockrecursionpolicy.norecursion 不支持,发现递归会抛异常。

lockrecursionpolicy.supportsrecursion  即支持递归模式,线程锁中继续在使用锁。

复制代码 代码如下:

cachelock.enterreadlock();
            //do
                cachelock.enterreadlock();
                //do
                cachelock.exitreadlock();
            cachelock.exitreadlock();

这种模式极易容易死锁,比如读锁里面使用写锁。

复制代码 代码如下:

cachelock.enterreadlock();
            //do
              cachelock.enterwritelock();
              //do
              cachelock.exitwritelock();
            cachelock.exitreadlock();

下面是直接拿msdn的缓存例子了,加了简单注释。

复制代码 代码如下:

public class synchronizedcache
    {
        private readerwriterlockslim cachelock = new readerwriterlockslim();
        private dictionary<int, string> innercache = new dictionary<int, string>();

        public string read(int key)
        {
            //进入读锁,允许其他所有的读线程,写入线程被阻塞。
            cachelock.enterreadlock();
            try
            {
                return innercache[key];
            }
            finally
            {
                cachelock.exitreadlock();
            }
        }

        public void add(int key, string value)
        {
            //进入写锁,其他所有访问操作的线程都被阻塞。即写独占锁。
            cachelock.enterwritelock();
            try
            {
                innercache.add(key, value);
            }
            finally
            {
                cachelock.exitwritelock();
            }
        }

        public bool addwithtimeout(int key, string value, int timeout)
        {
            //超时设置,如果在超时时间内,其他写锁还不释放,就放弃操作。
            if (cachelock.tryenterwritelock(timeout))
            {
                try
                {
                    innercache.add(key, value);
                }
                finally
                {
                    cachelock.exitwritelock();
                }
                return true;
            }
            else
            {
                return false;
            }
        }

        public addorupdatestatus addorupdate(int key, string value)
        {
            //进入升级锁。 同时只能有一个可升级锁线程。写锁,升级锁都被阻塞,但允许其他读取数据的线程。
            cachelock.enterupgradeablereadlock();
            try
            {
                string result = null;
                if (innercache.trygetvalue(key, out result))
                {
                    if (result == value)
                    {
                        return addorupdatestatus.unchanged;
                    }
                    else
                    {
                        //升级成写锁,其他所有线程都被阻塞。
                        cachelock.enterwritelock();
                        try
                        {
                            innercache[key] = value;
                        }
                        finally
                        {
                            //退出写锁,允许其他读线程。
                            cachelock.exitwritelock();
                        }
                        return addorupdatestatus.updated;
                    }
                }
                else
                {
                    cachelock.enterwritelock();
                    try
                    {
                        innercache.add(key, value);
                    }
                    finally
                    {
                        cachelock.exitwritelock();
                    }
                    return addorupdatestatus.added;
                }
            }
            finally
            {
                //退出升级锁。
                cachelock.exitupgradeablereadlock();
            }
        }

        public enum addorupdatestatus
        {
            added,
            updated,
            unchanged
        };
    }

四:总结

多线程实际开发当中,往往测试没问题,一到生产环境,并发高了就容易出问题, 一定注意。

本文参考clr via c#。