多线程编程(一)c#中的多线程同步

       一直想找机会好好把各种语言的多线程技术整理下,因为做所以最近查了点资料。第一篇是关于C#的多线程同步的。网上查了一些关于C#多线程同步,大部分的资料估计是转载的,内容一样,不过找到一个系列的写的很详细。我本来是想直接转帖,但是阅读了后,发现还是有一些容易引起误解的内容,因此在总结了我自己的一些经验后,我还是决定自己写一篇。另外我也会把我找到的资料转帖出来,方便比对阅读


一.预备知识

线程冲突:这里不详细解释了,只要有个概念就可以,详细的可以百度。所谓线程冲突,简单的讲,就是在多个不同线程竞争同一资源时造成的问题,比如死锁,脏数据,数据异常等等。在线程冲突的起因在于资源竞争,因此引起了下一个概念:线程安全

线程安全:线程安全是针对线程访问的资源而言。也就是该资源在被多线程访问时,会不会有引起线程冲突的隐患。这里我要说明的是,我以前一直认为线程安全的资源,本身就应该具备了多线程安全访问的能力,即不需要我们再写额外的代码来实现线程安全。但是这次在整理时,我发现,不知道是不是这个概念已经扩展了,还是我以前的认知有误,似乎具备线程安全的资源,也可能是需要自己调用一些资源本身已实现的接口来实现线程安全的。只不过你不负责实现线程安全功能,但你负责调用线程安全功能。

比如:MSDN中对List<T>类的说明,该类在全局静态下是线程安全的。但实际,不是说你生命一个静态List<T>,然后多线程访问就安全了。相反,如果你不调用它的SyncRoot来实现同步的话,它根本就是不安全的。

不过这不重要,我们只要知道,所谓的安全就是不能引发线程冲突。至于是我们自己实现,还是已经实现好了,这个只是文字概念上划分的问题。

线程同步:为了实现线程安全,我们需要编写线程同步的代码,来执行操作,来访问资源,不同的语言,都有类似的线程同步的实现方法。比如,互斥锁,信号量,临界区等


二.线程同步

C#中线程同步有一种实现方法:lock,monitor,Mutex,EventWaitHandle,Semaphore


(一)lock

具体的lock用法,我就不写了,MSDN或者看我的引用连接中,介绍的很详细了,我只总结一些重点。

1.lock只能锁定引用类型,不能锁定值类型

2.不推荐使用lock(this),原因在于,使用lock(this)的,必定是在类内部的程序,那么假设类外部的代码使用了lock(instance),即锁定了你的类实例,那么就可能产生线程死锁。

3.不能使用lock(typeof(T)),这个比lock(this)更夸张,这个锁定的引用是类对象,所有实例只有一个类对象,也就是任何访问到该类的代码,都会造成死锁。

4.不能使用lock("字符串"),字符串在程序中也会存在一个变量中被引用,而C#会把相同的字符串指向同一个暂存变量的引用,因此,死锁就是这么来的。

5.在早期的.Net集合中,集合类有几个来帮助实现同步的属性,方法,现在已经移动到System.Collections中,一个叫ICollection的接口类中,你可以使用默认的,或是自己实现相关的接口来覆盖它们

6.MSDN中大部分的集合类,都有一句这样的说明“此类型的公共静态(在 Visual Basic 中为 Shared)成员是线程安全的。但不能保证任何实例成员是线程安全的”。对于这句话,我的理解是参考我前面对线程安全的解释。当你声明集合类作为一个公共静态实例,你仍需自己调用ICollection的相关属性方法,来达到线程安全。如果你不调用,虽然集合类提供了线程安全的机制,但仍旧不起作用,也就是不安全的。至于当你声明集合类为成员变量时,那么显然,即便你调用了它的接口,它还可能是不安全的。(这点其实我觉得MS说的多余,对于成员变量的实例,本来竞争的就不是同一资源,完全和同步安全还差的很远)

7.使用lock比使用Monitor要好,原因在于,lock实际是使用Monitor来实现的,但是在MS的代码中,lock使用了finally关键字,确保了代码能退出锁定的临界区。这个具体可以在介绍monitor时再介绍。


(二)monitor

1.monitor和lock一样,只能锁定引用,不能锁定值类型

2.TryEnter永远不会阻塞,当它尝试进入临界区,如果失败,则返回false,代码不会进入(依赖我们自己实现)。如果成功(也就意味着没有锁定),则返回true,代码进入(此时其他线程进入等待该线程释放锁)

3.wait与pulse,wait阻塞了调用线程,并将自己占用的锁释放,pulse只是唤醒线程,不保证获取到了所,另外,pulse只唤醒队列中第一个线程。pulseAll唤醒所有线程。

4.wait可以传递一个参数,设定超时时间,超时后,wait会返回,并且线程不再阻塞。这是线程进入就绪队列,但是不保证一定获取了锁。

5.对于wait的第四个参数,需要介绍到同步域的概念。同步域有别与事件等待句柄,或是lock这种互斥的临界区(其实作用和锁类似)。其区别在于锁定的对象。lock,monitor锁定的都是对象,但是同步域(由SynchronizationAttribute指定)是指定一个逻辑代码范围。同步域属性必须和同步上下文类一起使用。它的同步操作也是互斥的。

也就是说,当你声明一个类继承自同步上下文类对象,你必须给它指定同步域属性。然后在访问该类的任何成员时,都会进入一个互斥的同步域,即A进入了,B就需要等待。

如果此时你在这个类的内部调用wait(MSDN原话是“只有从非默认托管上下文内部调用 Wait 方法,exitContext 参数才有效”),就会造成A在调用wait之前就退出同步域,B得以继续(MSDN原文:“在调用该对象的任何成员时被阻止的线程便可以继续运行”),A在调用返回后重新进入等待,并获取同步域(这个时候传给wait的参数必须是true)。

6.Enter和Exit的调用次数必须一致,因为可以重复获取对象锁,如果不一致,会导致锁无法正确释放。


(三)Mutex

1.Mutex和lock,monitor的不同在于可以跨应用程序

2.开销比较大

3.Mutex的wait系列函数,和ReleaseMutex的调用次数必须对应一致。

4.指定名称的Mutex为全局Mutex,即可以跨进程(应用程序域)访问。没有指定名称是是局部Mutex


(四)EventWaitHandle

1.C#中提供了两个继承自EventWaitHandle的类,ManualResetEvent和AutoResetEvent,这两个类都用终止和非终止状态。当事件终止,则阻塞结束,可以理解为事件才是那个阻塞的对象。事件非终止,意味着进入阻塞。

2.Set函数是用来终止事件的。也就是用来结束阻塞的。不同的是,ManualResetEvent,顾名思义,你不手动调用Reset,它将在Set后一直保持终止状态,造成后续线程不在阻塞。AutoResetEvent也就是无需手动调用Reset,在终止后,会自动重置时间,让时间进入非终止状态,保持对后续线程的阻塞。也就是这时只有一个在等待队列的线程会被释放继续执行。如果没有等待线程,则保持终止状态,知道有线程请求等待。这时它会释放这个请求的线程,然后让后续线程继续等待。

3.AutoResetEvent更适合与要求一个线程执行完才执行另一个线程的情况


(五)Semaphore

1.你可以创建没有名称的“局部”信号量,也可以创建命名的“全局”信号量用于跨应用程序域的同步。

2.你可以用WaitOne()请求一个资源。

3.你需要使用try/finally结构调用“Close()”,确保信号量资源在使用后被正确释放。

4.Semaphore和Mutex不同,不是一个互斥的操作,所以可以用于资源的并发访问。

5.Semaphore允许指定同时访问资源的数目,超过数目的线程,将等待知道有线程释放为止。

6.释放信号量的线程不必是请求它的线程,并且可以指定释放信号量的数量。


总结:

应该还有几个C#的多线程同步技术,比如同步域,连锁等没有介绍,同时只是把我觉得一些看不明白的东西加以自己的理解解释,所以可能有不恰当或是缺少例子,有空补吧!


参考:

C#线程同步(1)- 临界区&Lock

C#线程同步(2)- 临界区&Monitor

C#线程同步(3)- 互斥量 Mutex

C#线程同步(4)- 通知&EventWaitHandle一家



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值