原文:改善C#程序的建议8:避免锁定不恰当的同步对象

在C#中让线程同步的另一种编码方式就是使用线程锁。所谓线程锁,就是锁住一个资源,使得应用程序只能在此刻有一个线程访问该资源。可以用下面这句不是那么贴切的话来理解线程锁的作用:锁,就是让多线程变成单线程。在C#中,可以将被锁定的资源理解成new出来的普通对象。

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

    q同步对象在需要同步的多个线程中是可见的、同一个对象;

    q非静态方法中,静态变量不应作为同步对象;

    q值类型对象不能作为同步对象;

    q避免将字符串作为同步对象。

    q降低同步对象的可见性。

第一点,需要锁定的对象在多个线程中是可见的、同一个对象

“可见的”这是显而易见的,如果对象不可见,就不能被锁定。“同一个对象”,这理解起来也很好理解,如果锁定的不是同一个对象,那又如何来同步两个对象呢?可是,不见得我们在这上面不会犯错误。为了阐述本建议,我们先模拟一个必须使用到锁的场景:在遍历一个集合的过程中,同时在另外一个线程中删除集合中的某项。下面的这个例子中,如果没有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(SampeClass)),当前应用程序中的所有SampleClass的实例的线程,将会全部被同步。这样编码是完全没有必要的,这样的同步对象太开放了。

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

之前内容:
改善C#程序的建议7:正确停止线程改善C#程序的建议6:在线程同步中使用信号量改善C#程序的建议5:引用类型赋值为null与加速垃圾回收改善C#程序的建议4:C#中标准Dispose模式的实现改善C#程序的建议3:在C#中选择正确的集合进行编码改善C#程序的建议2:C#中dynamic的正确用法改善C#程序的建议1:非用ICloneable不可的理由

改善C#程序的建议8:避免锁定不恰当的同步对象的更多相关文章

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

    建议73:避免锁定不恰当的同步对象 在C#中,让线程同步的另一种编码方式就是使用线程锁.线程锁的原理,就是锁住一个资源,使得应用程序在此刻只有一个线程访问该资源.通俗地讲,就是让多线程变成单线程.在C ...

  2. [No0000180]改善C#程序的建议8:避免锁定不恰当的同步对象

    在C#中让线程同步的另一种编码方式就是使用线程锁.所谓线程锁,就是锁住一个资源,使得应用程序只能在此刻有一个线程访问该资源.可以用下面这句不是那么贴切的话来理解线程锁的作用:锁,就是让多线程变成单线程 ...

  3. 改善C#程序的建议10:用Parallel简化Task

    在命名空间System.Threading.Tasks下,有一个静态类Parallel简化了在同步状态下的Task的操作.Parallel主要提供了3个有用的方法:For.ForEach.Invoke ...

  4. 改善C#程序的建议6:在线程同步中使用信号量

    原文:改善C#程序的建议6:在线程同步中使用信号量 所谓线程同步,就是多个线程之间在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定.C#中对象的类型分为引用类型和值类型.CLR在这两 ...

  5. [转]改善C#程序的建议4:C#中标准Dispose模式的实现

    需要明确一下C#程序(或者说.NET)中的资源.简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类: 托管资源:由CLR管理分配和释放的资源,即由CLR里new出来的对象: 非托管资源:不 ...

  6. 改善C#程序的建议7:正确停止线程

    原文:改善C#程序的建议7:正确停止线程 开发者总尝试对自己的代码有更多的控制.“让那个还在工作的线程马上停止下来”就是诸多要求中的一种.然而事与愿违,这里面至少存在两个问题: 第一个问题是:正如线程 ...

  7. 改善C#程序的建议1:非用ICloneable不可的理由

    原文:改善C#程序的建议1:非用ICloneable不可的理由 好吧,我承认,这是一个反标题,实际的情况是:我找不到一个非用ICloneable不可的理由.事实上,接口ICloneable还会带来误解 ...

  8. 编写高质量代码--改善python程序的建议(六)

    原文发表在我的博客主页,转载请注明出处! 建议二十八:区别对待可变对象和不可变对象 python中一切皆对象,每一个对象都有一个唯一的标识符(id()).类型(type())以及值,对象根据其值能否修 ...

  9. 编写高质量代码--改善python程序的建议(八)

    原文发表在我的博客主页,转载请注明出处! 建议四十一:一般情况下使用ElementTree解析XML python中解析XML文件最广为人知的两个模块是xml.dom.minidom和xml.sax, ...

随机推荐

  1. AJAX代码格式

    var request; //XMLHttpRequest的创建 function createRequest(url){ if(window.XMLHttpRequest){ request = n ...

  2. 学习鸟哥的Linux私房菜笔记(6)——过滤器、输入输出及管道

    一.过滤器 Linux中的应用工具分为三种: 交互工具 过滤器 编辑器 能够接受数据,过滤再输出的工具,称之为过滤器 对过滤器和进程,存在着输入源与输出对象 二.输入.输出.重定向 输入:过滤器的数据 ...

  3. 【codeforces 777D】Cloud of Hashtags

    [题目链接]:http://codeforces.com/contest/777/problem/D [题意] 让你通过删除字符串的末尾字符的方式; 使得所给的字符串以字典序升序排序; 不能交换字符串 ...

  4. Java堆/栈/常量池以及String的详细详解(转)------经典易懂系统

    一:在JAVA中,有六个不同的地方可以存储数据: 1. 寄存器(register). 这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部.但是寄存器的数量极其有限,所以寄存器由编译器根据 ...

  5. sqoop 1.4.7 安装配置/连接测试

    环境: hadoop2.7.7 mysql 8 zk 3.4.10 hive 3 1.上传并解压tar包后进入conf目录 拷贝sqoop-env-template.sh并重命名为sqoop-env. ...

  6. Springboot系列:@SpringBootApplication注解

    在使用 Springboot 框架进行开发的时候,通常我们会在 main 函数上添加 @SpringBootApplication 注解,今天为大家解析一下 @SpringBootApplicatio ...

  7. 视频和音频播放的演示最简单的例子9:SDL2广播PCM

    ===================================================== 最简单的视频和音频播放的演示样品系列列表: 最简单的视音频播放演示样例1:总述 最简单的视音 ...

  8. windows 7 64位下配置mysql64位免安装版

    1.官方网站下载mysql-noinstall-5.1.51-winx64.zip 2.解压到E:\Program Files\MySQL.(路径自己指定) 3.在E:\Program Files\M ...

  9. Cython 的学习

    开发效率极高的 Python 一直因执行效率过低为人所诟病,Cython 由此诞生,特性介于 Python 和 C 语言之间. Cython 学习 1. Cython 是什么? 它是一个用来快速生成 ...

  10. Uniform synchronization between multiple kernels running on single computer systems

    The present invention allocates resources in a multi-operating system computing system, thereby avoi ...