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 类。

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

时间: 2024-07-29 23:11:56

C#线程同步技术(一) lock 语句的相关文章

线程系列07,使用lock语句块或Interlocked类型方法保证自增变量的数据同步

假设多个线程共享一个静态变量,如果让每个线程都执行相同的方法每次让静态变量自增1,这样的做法线程安全吗?能保证自增变量数据同步吗?本篇体验使用lock语句块和Interlocked类型方法保证自增变量的数据同步. □ 线程不安全.数据不同步的做法 class Program { static int sum = 0; static void Main(string[] args) { Stopwatch watch = new Stopwatch(); watch.Start(); Parall

C#线程同步技术(二) Interlocked 类

接昨天谈及的线程同步问题,今天介绍一个比较简单的类,Interlocked.它提供了以线程安全的方式递增.递减.交换和读取值的方法. 它的特点是: 1.相对于其他线程同步技术,速度会快很多. 2.只能用于简单的同步问题. 比叫好理解,不再赘述,给一个我们常用的单例模式的 Interlocked 实现: class SourceManager { private SourceManager() { } private static SourceManager sourceManager; publ

【WIN32进阶之路】:线程同步技术纲要

前面博客讲了互斥量(MUTEX)和关键段(CRITICAL SECTION)的使用,想来总觉不妥,就如盲人摸象一般,窥其一脚而言象,难免以偏概全,追加一篇博客查遗补漏. win32下的线程同步技术分为用户模式下的线程同步和用内核对象进行线程同步两大类. 用户模式下的线程同步和用内核对象进行线程同步有以下的明显差异: 1.用户模式下的线程同步不需要进入操作系统核心,直接在用户模式就可以进行操作. 2.用内核对象进行线程同步需要进入操作系统核心,用户模式切换至核心模式大约花费1000个CPU周期.

Delphi中线程类TThread实现多线程编程(线程同步技术、Synchronize、WaitFor……)

接着上文介绍TThread. 现在开始说明 Synchronize和WaitFor 但是在介绍这两个函数之前,需要先介绍另外两个线程同步技术:事件和临界区 事件(Event)与Delphi中的事件有所不同.从本质上讲,Event其实就相当于一个全局的布尔变量.它有两个赋值操作:Set和ReSet,相当于把它设置为 True或False.而检查它的值是通过WaitFor操作进行.对应在Windows平台上,是三个API函数:SetEvent.ResetEvent.WaitForSignalObje

C++windows内核编程笔记day14 其他线程同步技术

线程同步技术: 原子锁 临界区(段) 互斥 事件 信号量(线程示例时已经使用过) 可等候定时器 使用范围:原子锁<临界区<互斥 效率:    原子锁>临界区(用户态)>互斥(内核态) 一般用临界区. //等候多个信号 DWORD WaitForMultipleObjects( DWORD nCount,             // number of handles in array CONST HANDLE *lpHandles,  // object-handle array

python笔记10-多线程之线程同步(锁lock)

前言 关于吃火锅的场景,小伙伴并不陌生,吃火锅的时候a同学往锅里下鱼丸,b同学同时去吃掉鱼丸,有可能会导致吃到生的鱼丸.为了避免这种情况,在下鱼丸的过程中,先锁定操作,让吃火锅的小伙伴停一会,等鱼丸熟了再开吃,那么python如何模拟这种场景呢? 未锁定 1.如果多个线程同时操作某个数据,会出现不可预料的结果.比如以下场景:当小伙伴a在往火锅里面添加鱼丸的时候,小伙伴b在同时吃掉鱼丸,这很有可能导致刚下锅的鱼丸被夹出来了(没有熟),或者还没下锅,就去夹鱼丸(夹不到). # coding=utf-

Delphi 线程同步技术(转)

上次跟大家分享了线程的标准代码,其实在线程的使用中最重要的是线程的同步问题,如果你在使用线程后,发现你的界面经常被卡死,或者无法显示出来,显示混乱,你的使用的变量值老是不按预想的变化,结果往往出乎意料,那么你很有可能是忽略了线程同步的问题. 当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源.例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数.当然,在把整个文件调入内存之前,统计它的计数是没有意义的.但是,由于每个操作都有自己的 线程,操作系统

JAVA 并发编程-线程同步通信技术(Lock和Condition)(十)

在之前的博客中已经介绍过线程同步通信技术<JAVA 并发编程-传统线程同步通信技术(四)>,上篇是使用的synchronized,wait,notify来实现,今天我们使用的是Lock和Condition,下面我们结合两者对比来学习. 简单的Lock锁应用: /** * 简单Lock的应用 * @author hejingyuan * */ public class LockTest { public static void main(String[] args) { new LockTest

Visual C++线程同步技术剖析:临界区,事件,信号量,互斥量

转自: 使线程同步 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解.正常情况下对这种处理结果的了解应当在其处理任务完成后进行. 如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解.例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题.如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数