建议73:避免锁定不恰当的同步对象

在C#中,让线程同步的另一种编码方式就是使用线程锁。线程锁的原理,就是锁住一个资源,使得应用程序在此刻只有一个线程访问该资源。通俗地讲,就是让多线程变成单线程。在C#中,可以将被锁定的资源理解成new出来的普通CLR对象。

既然需要锁定的资源就是C#中的一个对象,我们就该仔细思考,到底什么样的对象能够成为一个锁对象(也叫同步对象)?在选择同步对象的时候,应当始终注意以下几点:

1)同步对象在需要同步的多个线程中是可见的同一个对象。
2)在非静态方法中,静态变量不应作为同步对象。
3)值类型对象不能作为同步对象。
4)避免将字符串作为同步对象。
5)降低同步对象的可见性。

下面分别详细介绍以上五个注意事项。

第一个注意事项:需要锁定的对象在多个线程中是可见的,而且是同一个对象。“可见的”这是显而易见的,如果对象不可见,就不能被锁定。“同一个对象”,这也很容易理解,如果锁定的不是同一个对象,那又如何来同步两个对象呢?虽然理解起来简单,但不见得我们在这上面就不会犯错误。为了帮助大家理解本建议的内容,我们先模拟一个必须使用到锁的场景:在遍历一个集合的过程中,同时在另外一个线程中删除集合中的某项。下面这个例子中,如果没有lock语句,将会抛出异常InvalidOperationException:“集合已修改;可能无法执行枚举”:

public partial class FormMain : Form
{
public FormMain()
{
InitializeComponent();
} AutoResetEvent autoSet = new AutoResetEvent(false);
List<string> tempList = new List<string>() { "init0", "init1", "init2" }; private void buttonStartThreads_Click(object sender, EventArgs e)
{
object syncObj = new object(); Thread t1 = new Thread(() =>
{
//确保等待t2开始之后才运行下面的代码
autoSet.WaitOne();
lock (syncObj)
{
foreach (var item in tempList)
{
Thread.Sleep();
}
}
});
t1.IsBackground = true;
t1.Start(); Thread t2 = new Thread(() =>
{
//通知t1可以执行代码
autoSet.Set();
//沉睡1秒是为了确保删除操作在t1的迭代过程中
Thread.Sleep();
lock (syncObj)
{
tempList.RemoveAt();
}
});
t2.IsBackground = true;
t2.Start();
}
}

这是一个Winform窗体应用程序,需要演示的功能在按钮的单击事件中。对象syncObj对于线程t1和t2来说,在CLR中肯定是同一个对象。所以,上面的示例运行是没有问题的。

现在,我们将此示例重构。将实际的工作代码移到一个类型SampleClass中,该示例要在多个SampleClass实例间操作一个静态字段,如下所示:

private void buttonStartThreads_Click(object sender, EventArgs e)
{
SampleClass sample1 = new SampleClass();
SampleClass sample2 = new SampleClass();
sample1.StartT1();
sample2.StartT2();
} class SampleClass
{
public static List<string> TempList = new List<string>() { "init0",
"init1", "init2" };
static AutoResetEvent autoSet = new AutoResetEvent(false);
object syncObj = new object(); public void StartT1()
{
Thread t1 = new Thread(() =>
{
//确保等待t2开始之后才运行下面的代码
autoSet.WaitOne();
lock (syncObj)
{
foreach (var item in TempList)
{
Thread.Sleep();
}
}
});
t1.IsBackground = true;
t1.Start();
} public void StartT2()
{
Thread t2 = new Thread(() =>
{
//通知t1可以执行代码
autoSet.Set();
//沉睡1秒是为了确保删除操作在t1的迭代过程中
Thread.Sleep();
lock (syncObj)
{
TempList.RemoveAt();
}
});
t2.IsBackground = true;
t2.Start();
}
}

该示例运行起来会抛出异常InvalidOperationException:

“集合已修改;可能无法执行枚举。”

查看类型SampleClass的方法StartT1和StartT2,方法内部锁定的是SampleClass的实例变量syncObject。实例变量意味着,每创建一个SampleClass的实例都会生成一个syncObject对象。在本例中,调用者一共创建了两个SampleClass实例,继而分别调用:

sample1.StartT1();  
sample2.StartT2();
也就是说,以上代码锁定的是两个不同的syncObject,这等于完全没有达到两个线程锁定同一个对象的目的。要修正以上错误,只要将syncObject变成static就可以了。

另外,思考一下lock(this),我们同样不建议在代码中编写这样的代码。如果两个对象的实例分别执行了锁定的代码,实际锁定的也就会是两个对象,完全不能达到同步的目的。

第二个注意事项:在非静态方法中,静态变量不应作为同步对象。也许有读者会问,前面曾说到,要修正第一个注意事项中的示例问题,需要将syncObject变成static。这似乎和本注意事项有矛盾。事实上,第一个注意事项中的示例代码仅仅出于演示的目的,在实际应用中,我们强烈建议不要编写此类代码。在编写多线程代码时,要遵循这样的一个原则:

类型的静态方法应当保证线程安全,非静态方法不需实现线程安全。

FCL中的绝大部分类都遵循了这个原则。像上一个示例中,如果将syncObject变成static,就相当于让非静态方法具备了线程安全性,这带来的一个问题是,如果应用程序中该类型存在多个实例,在遇到这个锁的时候,它们都会产生同步,而这可能不是开发者所愿意看到的。第二个注意事项实际也可以归纳到第一个注意事项中。

第三个注意事项:值类型对象不能作为同步对象。值类型在传递到另一个线程的时候,会创建一个副本,这相当于每个线程锁定的也是两个对象。因此,值类型对象不能作为同步对象。

第四个注意事项:锁定字符串是完全没有必要的,而且相当危险。这整个过程看上去和值类型正好相反。字符串在CLR中会被暂存到内存里,如果有两个变量被分配了相同内容的字符串,那么这两个引用会被指向同一块内存。所以,如果有两个地方同时使用了lock(“abc”),那么它们实际锁定的是同一个对象,这会导致整个应用程序被阻滞。

第五个注意事项:降低同步对象的可见性。可见范围最广的一种同步对象是typeof(SampleClass)。typeof方法所返回的结果(也就是类型的type)是SampleClass的所有实例所共有的,即:所有实例的type都指向typeof方法的结果。这样一来,如果我们lock(typeof(SampleClass)),当前应用程序中所有SampleClass的实例线程将会全部被同步。这样编码完全没有必要,而且这样的同步对象太开放了。

一般来说,同步对象也不应该是一个公共变量或属性。在FCL的早期版本中,一些常用的集合类型(如ArrayList)提供了公共属性SyncRoot,让我们锁定以便进行一些线程安全的操作。所以你一定会觉得我们刚才的结论不正确。其实不然,ArrayList操作的大部分应用场景不涉及多线程同步,所以它的方法更多的是单线程应用场景。线程同步是一个非常耗时(低效)的操作。若ArrayList的所有非静态方法都要考虑线程安全,那么ArrayList完全可以将这个SyncRoot变成静态私有的。现在它将SyncRoot变为公开的,是让调用者自己去决定操作是否需要线程安全。我们在编写代码时,除非有这样的要求,否则就应该始终考虑降低同步对象的可见性,将同步对象藏起来,只开放给自己或自己的子类就够了(需要开放给子类的情况其实也不多)。

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

编写高质量代码改善C#程序的157个建议——建议73:避免锁定不恰当的同步对象的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  10. 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法

    建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...

随机推荐

  1. Web App、Hybrid App与Native App的设计差异

    目前主流应用程序大体分为三类:Web App.Hybrid App. Native App. 一.Web App.Hybrid App.Native App 纵向对比 首先,我们来看看什么是 Web ...

  2. python开发_function annotations

    在看python的API的时候,发现了一个有趣的东东,即:python的方法(函数)注解(Function Annotation) 原文: 4.7.7. Function Annotations Fu ...

  3. Linux启动流程【转载】

    探讨操作系统接管硬件以后发生的事情,也就是操作系统的启动流程. 这个部分比较有意思.因为在BIOS阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多:但是,一旦进入操作系统,程序员几乎可以定制 ...

  4. vue设置title和ioc图标

    vue设置ioc图标和title 1.ioc图标设置 在根目录中的index.html中引入代码: <link rel="shortcut icon" type=" ...

  5. Spring技术内幕之Spring Data JPA-自定义Repository实现

    1.自定义Repository方法接口,让接口的实现类来继承这个中间接口而不是Repository接口 package com.data.jpa.dao; import java.io.Seriali ...

  6. Redis作为缓存服务器

    1.ICache的Redis实现没有放在'Framework.Cache/Logic'中.如果是以前,我会认为这样不好.我会这样做,'Framework.Cache'项目引用Redis项目或直接从Nu ...

  7. Python的索引迭代

    Python中,迭代永远是取出元素本身,而非元素的索引. 对于有序集合,元素确实是有索引的.有的时候,我们确实想在 for 循环中拿到索引,怎么办? 方法是使用 enumerate() 函数: > ...

  8. javaScript字符串操作

    JS自带函数concat将两个或多个字符的文本组合起来,返回一个新的字符串.var a = "hello";var b = ",world";var c = a ...

  9. Request.IsAuthenticated

    Original question that the answer below refers to: I have a forms based application that is giving m ...

  10. MySQL数据库篇之索引原理与慢查询优化之二

    接上篇 7️⃣  正确使用索引 一.索引未命中 并不是说我们创建了索引就一定会加快查询速度,若想利用索引达到预想的提高查询速度的效果, 我们在添加索引时,必须遵循以下问题: #1 范围问题,或者说条件 ...