王朝网络
分享
 
 
 

C#多线程实践——锁和线程安全

王朝学院·作者佚名  2016-05-20  
宽屏版  字体: |||超大  

C#多线程实践——锁和线程安全锁实现互斥的访问,用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:

class ThreadUnsafe { static int val1, val2; static void Go() { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; }}

这不是线程安全的:如果Go方法被两个线程同时调用,可能会得到在某个线程中除数为零的错误,因为val2可能被一个线程设置为零,而另一个线程刚好执行到if和Console.WriteLine语句。

下面用c#中的lock来修正这个问题:

class ThreadSafe { static object locker = new object(); static int val1, val2; static void Go() { lock (locker) { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } } }

在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。因为一个线程的访问不能与另一个重叠,互斥锁有时被称之对由锁所保护的内容强迫串行化访问。在这个例子中,保护了Go方法的逻辑,以及val1 和val2字段的逻辑。一个等候竞争锁的线程被阻止将在ThreadState上为WaitSleepJoin状态。稍后将讨论一个线程通过另一个线程调用Interrupt或Abort方法来强制地被释放。这是用于结束工作线程一个相当高效率的技术。C#的lock 语句实际上是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法:

Monitor.Enter (locker); try { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0;}finally { Monitor.Exit (locker); }

在同一个对象上,在调用第一个Monitor.Ente之前却先调用了Monitor.Exit将引发异常。Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false。TryEnter也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。

选择同步对象

任何对所有有关系的线程都可见的对象都可以作为同步对象,但要满足一个硬性规定:它必须是引用类型。建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。满足这些规则,则同步对象可以兼对象和保护两种作用。比如下面List :

class ThreadSafe { List <string> list = new List <string>(); void Test() { lock (list) { list.Add ("Item 1"); ...

一个专门字段(如在例子中的locker)是常用的方式 , 因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:

lock (this) { ... }

或:

lock (typeof (Widget)) { ... } // 保护访问静态

的方式是不好的,因为存在可以在公共范围访问这些对象的潜在风险。

锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x) 而被阻止。

嵌套锁定

线程可以重复锁定相同的对象,可以通过多次调用Monitor.Enter或lock语句来实现。当对应编号的Monitor.Exit被调用或最外面的lock语句完成后,对象那一刻即被解锁。这就允许最简单的语法实现一个方法的锁调用另一个锁:

static object x = new object();static void Main() { lock (x) { Console.WriteLine ("I have the lock"); Nest(); Console.WriteLine ("I still have the lock"); } //在这锁被释放}static void Nest(){ lock (x) { ... } // 释放了锁?没有完全释放!}

线程只能在最开始的锁或最外面的锁时被阻止。

何时进行锁定

作为一项基本规则,任何和多线程有关的会进行读和写的字段都应当加锁。甚至是极平常的事情——单一字段的赋值操作,都必须考虑到同步问题。在下面的例子中Increment和Assign 都不是线程安全的:

class ThreadUnsafe { static int x; static void Increment() { x++; } static void Assign() { x = 123; }}

下面是Increment 和 Assign 线程安全的版本:

class ThreadUnsafe{ static object locker = new object(); static int x; static void Increment() { lock (locker) x++; } static void Assign() { lock (locker) x = 123; }}

作为加锁的另一个选择,在一些简单的情况下,也可以使用非阻止同步,将在后面讨论即使像这样的语句需要同步的原因。

锁和原子操作

如果有很多变量在一些锁中总是进行读和写的操作,那么你可以称之为原子操作。我们假设x 和 y不停地读和赋值,他们在锁内通过locker锁定:

lock (locker) { if (x != 0) y /= x; }

你可以认为x 和 y 通过原子的方式访问,因为代码段没有被其它的线程分开 或 抢占,别的线程改变x 和 y是无效的输出,你永远不会得到除数为零的错误,保证了x 和 y总是被相同的排他锁访问。

性能考量

锁本身是非常快的,一个锁在没有堵塞的情况下一般只需几十纳秒(十亿分之一秒)。如果发生堵塞,任务切换带来的开销接近于数微秒(百万分之一秒)的范围内,尽管在线程重组实际的安排时间之前它可能花费数毫秒(千分之一秒)。相反,该使用锁而没使用的会带来更长的时间开销。如果发生了死锁和竞争锁,锁就会带来反作用,由于太多的代码被放置到锁语句中了,引起其它线程不必要的被阻止。死锁是两线程彼此等待被锁定的内容,导致两者都无法继续下去。争用锁是两个线程任一个都可以锁定某个内容,如果“错误”的线程获取了锁,则导致程序错误。

对于同步对象非常容易出现死锁的情况,比较好的处理方式是设计较少的锁。在一个可信的情况下涉及比较多阻止的话,可以考虑增加锁的粒度。

线程安全

线程安全的代码是指在面对任何多线程情况下,代码都没有不确定的因素。线程安全首先完成锁,然后减少在线程间交互的可能性。

一个线程安全的方法,在任何情况下可以可重入式调用。引用类型很少是线程安全的,原因如下:

完全线程安全的开发是重要的,尤其是一个类型有很多字段(在任意多线程上下文中每个字段都有潜在的交互作用)的情况下。线程安全带来性能损失(要付出的,在某种程度上无论与否类型是否被用于多线程)。一个线程安全类型不一定能使程序使用线程安全,有时参与工作后者可使前者变得冗余。 为了处理一个特定的多线程情况,线程安全经常只在需要实现的地方来实现。不过也有特殊情况,通过牺牲锁的粒度包含大段的代码甚至在排他锁中访问全局对象来迫使在更高的级别上实现串行化访问,实现庞大复杂的类安全地运行在多线程环境中。这种用法让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于在保护对非线程安全对象的所有的属性、方法和字段的访问上。或者通过最小化共享数据来最小化线程交互,多用于“弱状态”的中间层程序和web服务器实现引用类型的线程安全。虽然多个客户端请求同时到达,但每个请求来自它自己的线程(比如asp.net,Web服务器或者远程体系结构),它们调用的方法是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,因此类不能够在每个请求间持久保留数据。线程交互仅限于可以被选择创建的静态字段,一般用于在内存里缓存常用数据和提供认证和审核这样的基础设施服务。

线程安全与.NET Framework类型

锁可用于将非线程安全的代码转换成线程安全的代码。在.NET framework实现中,几乎所有非基本类型的实例都不是线程安全的。将非基本类型用于多线程代码中,就需要给访问的对象进行锁保护。以下示例中两个线程同时为相同的List增加条目,然后枚举它:

class ThreadSafe{ static List <string> list = new List <string>(); static void Main() { new Thread (AddItems).Start(); new Thread (AddItems).Start(); } static void AddItems() { for (int i = 0; i < 100; i++) lock (list)list.Add ("Item " + list.Count); string[] items; lock (list) items = list.ToArray(); foreach (string s in items) Console.WriteLine (s); }}

在这种情况下锁定list对象本身,也许是一个不错的方式。枚举.NET的集合也不是线程安全的,在枚举的时候另一个线程改动list的话,会抛出异常。为了不直接锁定枚举过程,我们首先将项目复制到数组中,避免因为在枚举过程中有潜在的耗时而固定住锁。

一个有趣的假设:如果List实际上为线程安全的,要增加一个项目到我们假象的线程安全的list里,如下:

if (!myList.Contains (newItem)) myList.Add (newItem);

无论list是否为线程安全的,这个语句显然不是,也就是说完全线程安全的通用集合类是基本不存在的。.net4.0中,微软提供了一组线程安全的并行集合类,但他们都经过特殊处理,在访问方式做了限定。上面的语句要实现线程安全,整个if语句必须放到一个锁中,用来保护在判断有无和增加新的之间的抢占。类似的锁需要用于任何我们需要修改list的地方,比如下面的语句:

myList.Clear();

换言之,我们必须锁定差不多所有非线程安全的集合类们。内置线程安全,显而易见是浪费时间!由于这些理由,.NET framework中静态成员是线程安全的,而一个实例成员则不是。从而在写自定义类型时,也不要尝试去创建一个线程安全的自定义组件!当写公用组件的时候,单独小心处理静态成员是一个好的编码习惯。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
>>返回首页<<
推荐阅读
 
 
频道精选
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
© 2005- 王朝网络 版权所有