• C#线程同步技术(一) lock 语句


    开篇语:

    上班以后,烦恼少了,至少是没有什么好烦的了,只要负责好自己的工作就可以了,因此也有更多的时间去探索自己喜欢的程序。买回来的书已经看了一半,DEMO也敲了不少,昨晚终于在这里开BLOG,记录一些读书笔记。以我自己的经验来看,写笔记、测试、体会是加深理解的利器,往往起到事半功倍的效果。这几天在看任务、线程和同步的部分,就用这个练练笔,记录一些学习的心得。

    一、一个小测试

    本文讨论的是线程同步的技术,假定你已经理解相关概念。如果未接触过,或者理解得不多,且看下面的小例子:

            public class SharedState
            {
                public int State { get; set; }
            }
    
            public class Job
            {
                private SharedState _state;
                public Job(SharedState state)
                {
                    this._state = state;
                }
    
                public void DoTheJob()
                {
                    for (int i = 0; i < 5000; i++)
                    {
                        this._state.State++;
                    }
                }
            }

    这里定义了两个类:SharedObject 用于保存线程之间的共享数据,有一个数据成员 State,Job类拥有一个 SharedObject 类型的成员,DoTheJob()方法中进行5000次循环累加成员的 State 的属性。

    下面的测试方法中,我们会新建20个任务,执行DoTheJob()方法:

            public static void MultiThreadTest()
            {
                var size = 20;
                var tasks = new Task[size];
                SharedState state = new SharedState();
                for (int i = 0; i < size; i++)
                {
                    tasks[i] = Task.Factory.StartNew(() =>
                    {
                        new Job(state).DoTheJob();
                    });
                }
                for (int i = 0; i < size; i++)
                {
                    tasks[i].Wait();
                }
                Console.WriteLine(state.State);
            }

    按照同步执行的习惯去理解的话,你可能会认为输出的结果会是:5000*20 = 100000,实际上,以上程序执行了3次的结果分别是:

    41841
    58509
    69589

    当然,这只是我的机器上的执行结果,在你的机器上会有不同的结果。这说明了一个问题:在多线程并行执行的环境下,共享的数据有可能被其他线程修改而导致出现非预期结果。

    二、C#用于多个线程同步的技术

    如果需要在线程中共享数据,就需要使用同步技术,C#可以用于多线程同步的技术有:

    1. lock 语句
    2. Interlocked 类
    3. Monitor 类
    4. SpinLock 结构
    5. WaitHandle 类
    6. Mutex 类
    7. Semaphore 类
    8. Event 类
    9. Barrier 类
    10. ReaderWriterLockSlim 类

    1、lock 语句

    lock 语句只能锁定引用类型,锁定值类型只能锁定一个副本,并无意义(实际上,对值类型使用了 lock 语句,编译器会给出一个错误)

    使用 lock 语句可以将类的实例成员设置为线程安全的,一次只有一个线程能访问相同实例的相同成员。结合几个例子理解这句话:

                /*
                 * 将 DoTheJob() 方法进行以下改造是否达到我们的目的了?
                 * 答案是否定的
                 * 改造后继续测试依然没有输出我们期待的 100000
                 * 这里的 lock 只对使用相同实例的线程起作用
                 * tasks[] 中每个任务都调用不同的实例,所以它们都能同时调用 DoTheJob()方法
                 */
    
                public void DoTheJob()
                {
                    lock (this)
                    {
                        for (int i = 0; i < 5000; i++)
                        {
                            this._state.State++;
                        }
                    }
                }

    即使上面的改造并不成功,本着对比加深理解的目的,这里提一提 lock(this) 和 lock(obj) 的区别,以下的改造和以上的改造有何不同?

                private object syncObj = new object();
                public void DoTheJob()
                {
                    lock (syncObj)
                    {
                        for (int i = 0; i < 5000; i++)
                        {
                            this._state.State++;
                        }
                    }
                }

    从功能上看,lock(this) 锁定了整个实例,导致锁定期间 Job 实例的成员都不能被其他访问,而不仅仅是 DoTheJob() 不能被其他线程访问。而 lock(syncObj)只会导致 DoTheJob() 不能被其他线程访问,但实例的其他成员依然可以被访问。以下的例子可以更清楚的说明这一点。

    lock(this)

            public class LockThis
            {
                private bool _deadLock = true;
                public void DeadLocked()
                {
                    lock (this)
                    {
                        while (_deadLock)
                        {
                            Console.WriteLine("OMG! I am locked!");
                            Thread.Sleep(1000);
                        }
                        Console.WriteLine("DeadLocked() End.");
                    }
                }
    
                public void DontLockMe()
                {
                    _deadLock = false;
                }
            }
    
            /*
             * lockThis 实例企图在死锁 DeadLocked() 发生5秒后
             * 通过 DontLockMe() 接触死锁
             * 但并不成功!
             * 因为死锁中 lock(this) 锁定了整个实例
             * 导致外层也有可能用同步方式访问该实例时,连非同步方法 DontLockMe() 也不能调用
             */
    
            public static void LockThisMethod()
            {
                LockThis lockThis = new LockThis();
                Task.Factory.StartNew(lockThis.DeadLocked);
                Thread.Sleep(5000);
                lock (lockThis)
                {
                    lockThis.DontLockMe();
                }
            }

    lock(syncObj)

            public class LockObject
            {
                private bool _deadLock = true;
                private object _syncObj = new object();
                public void DeadLocked()
                {
                    lock (_syncObj)
                    {
                        while (_deadLock)
                        {
                            Console.WriteLine("OMG! I am locked!");
                            Thread.Sleep(1000);
                        }
                        Console.WriteLine("DeadLocked() End.");
                    }
                }
    
                public void DontLockMe()
                {
                    _deadLock = false;
                }
            }
    
            /*
             * lockObject 实例企图在死锁 DeadLocked() 发生5秒后
             * 通过 DontLockMe() 接触死锁
             * 成功了!
             * 因为死锁中 lock(_syncObj) 只锁定了 DeadLocked() 方法
             * 即使外层也有用同步方式访问该实例时,非同步方法 DontLockMe() 也可以被调用
             */
    
            public static void LockObjectMethod()
            {
                LockObject lockObject = new LockObject();
                Task.Factory.StartNew(lockObject.DeadLocked);
                Thread.Sleep(5000);
                lock (lockObject)
                {
                    lockObject.DontLockMe();
                }
            }

    总结:因为类的对象也可以用于外部的同步访问( 上面的 lock(lockThis) 和 lock(lockObject) 就模拟了这种访问 ),而且我们不能在类自身中控制这种访问,所以应该尽量使用 lock(obj) 的方式,可以比较精确的控制需要同步的范围。

    说着说着好像说远了,只顾说 lock(this) 和 lock(obj) 的区别,我们要的 100000 还没出来呢 :)俺的缺点就一般不怎么扯,一扯就扯得挺远 :)

    好吧,继续。可能有很多看官早就想爆料,说这TM不简单的,一二三给个出 100000 的代码出来,其实这个俺也知道,只是这不是俺的目的。学习最怕的是知其然,而不知其所以然。我们不仅要知道正确的方式,更需要知道错误的方式,更更重要的是,需要知道它为什么正确,又为什么是错误的。

    再看这种,可能真的有朋友会这么做哦~ 如果不对,不对的点又在哪呢?

            public class SharedState
            {
                private object syncObj = new object();
                private int _state;
                public int State 
                {
                    get
                    {
                        lock (syncObj)
                        {
                            return _state;
                        }
                    }
                    set
                    {
                        lock (syncObj)
                        {
                            _state = value;
                        }
                    }
                }
            }

    貌似是可以的,直接对共享状态控制同步,读和写都同步了,应该没问题了

    很可惜,结果是 100000 依然没有出来 :(

    误区就是:对同步的过程理解错了,读和写之间 syncObj 并没有被锁定,依然有线程可以在这个期间获得值。

    夜已渐深了,看到这里很多人都会有自己的答案了。下面就列出两种正确的实现方法:

    1)一种是对 SharedState 进行改造,作为一种原子操作提供递增方式,将DoTheJob()中递增的代码改为调用 IncrementState() 方法

            public class SharedState
            {
                private object syncObj = new object();
                private int _state;
                public int State
                {
                    get { return _state; }
                }
    
                public int IncrementState()
                {
                    lock (syncObj)
                    {
                        return ++_state;
                    }
                }
            }

    2)另一种是不改动 SharedState 类,使用正确的 locker ,将 lock 语句放在合适的地方

            public class SharedState
            {
                public int State { get; set; }
            }
    
            public class Job
            {
                private SharedState _state;
                public Job(SharedState state)
                {
                    this._state = state;
                }
    
                public void DoTheJob()
                {
                    for (int i = 0; i < 5000; i++)
                    {
                        lock (_state)
                        {
                            _state.State++;
                        }
                    }
                }
            }

    关于 lock 语句使用暂时介绍到这里,最后需要体会的:

    在一个地方使用 lock 语句并不意味着,访问对象的线程都在等待,必须对每个访问共享状态的线程,都显式的使用同步功能。

    如何理解并验证这句话?把 lock(this) 无法解除死锁那段代码中去掉外层的 lock(lockThis) 运行看看就知道了 :)

    虽然任务Task线程里使用了lock(this)锁定实例,但是外层主线程并无使用同步功能,因此自然可以掉到 DontLockMe() 方法成功解锁!

    敲码的时间总是过得很快,要洗洗睡了,明天继续总结 Interlocked 类。

  • 相关阅读:
    恢复安装数据库的SCOTT用户的数据:
    SQL Server DATEDIFF() 函数
    MS SQL 日期格式转换
    cxGrid增加一栏显示checkBox的设置方法
    介绍JSP程序动态网站环境搭建的详细步骤
    SQL Server CONVERT() 函数
    最新JSP环境配置方法
    JSP语法(Jsp技术大全)1
    SQL字符串处理函数大全
    JSP数据库连接
  • 原文地址:https://www.cnblogs.com/cnhxz/p/3859535.html
Copyright © 2020-2023  润新知