编码技巧 --- 同步锁对象的选定

这篇具有很好参考价值的文章主要介绍了编码技巧 --- 同步锁对象的选定。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言

在C#中,让线程同步有两种方式:

  • 锁(lock、Monitor)
  • 信号量(EventWaitHandle、Semaphore、Mutex)

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

如何选定

既然需要锁定的资源就是C#中的一个对象,我们就该仔细思考,到底什么样的对象能够成为一个锁对象(也叫同步对象)?

那么选择同步对象的时候,应当始终注意以下几点:

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

原因分析

接下来就探讨一下这五种情况。

注意事项1:需要锁定的对象在多个线程中是可见的,而且是同一个对象。

“可见的”这是显而易见的,如果对象不可见,就不能被锁定。

“同一个对象”,这也很容易理解,如果锁定的不是同一个对象,那又如何来同步两个对象呢?

虽然理解起来简单,但不见得我们在这上面就不会犯错误。

我们模拟一个必须使用到锁的场景:在遍历一个集合的过程中,同时在另外一个线程中删除集合中的某项。

下面这个例子中,如果没有 lock 语句,将会抛出异常System.InvalidOperationException:“Collection was modified; enumeration operation may not execute.”

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    AutoResetEvent autoResetEvent = new AutoRe

    List<string> strings = new List<string>() 

    private void btn_StartThreads_Click(object
    {
        object syncObj = new object();

        Thread t1 = new Thread(() =>
        {
            //确保等待t2开始之后才运行下面的代码
            autoResetEvent.WaitOne();

            lock (syncObj)
            {
                foreach (var item in strings)
                {
                    Thread.Sleep(1000);
                }
            }
        });
        t1.IsBackground = false;

        t1.Start();

        Thread t2 = new Thread(() =>
        {
            autoResetEvent.Set();

            Thread.Sleep(1000);

            lock (syncObj)
            {
                strings.RemoveAt(1);
            }

        });
        t2.IsBackground = false;

        t2.Start();
    }
}

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

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

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void btn_StartThreads_Click(object sender, EventArgs e)
    {
        SampleClass sampleClass1 = new SampleClass();
        SampleClass sampleClass2 = new SampleClass();
        sampleClass1.StartT1();
        sampleClass2.StartT2();
    }
}

public class SampleClass
{
    public static AutoResetEvent autoResetEvent = new AutoResetEvent(false);

    static List<string> strings = new List<string>() { "str1", "str2", "str3" };

    object syncObj = new object();

    public void StartT1()
    {
        Thread t1 = new Thread(() =>
        {
            //确保等待t2开始之后才运行下面的代码
            autoResetEvent.WaitOne();

            lock (syncObj)
            {
                foreach (var item in strings)
                {
                    Thread.Sleep(1000);
                }
            }
        });
        t1.IsBackground = false;

        t1.Start();
    }
    public void StartT2()
    {
        Thread t2 = new Thread(() =>
        {
            autoResetEvent.Set();

            Thread.Sleep(1000);

            lock (syncObj)
            {
                strings.RemoveAt(1);
            }

        });
        t2.IsBackground = false;

        t2.Start();
    }
}

该例子运行起来就会抛出异常System.InvalidOperationException:“Collection was modified; enumeration operation may not execute.”

查看类型 SampleClass 的方法 StartT1StartT2 ,方法内部锁定的是 SampleClass 的实例变量 syncObj

实例变量意味着,每创建一个 SampleClass 的实例都会生成一个 syncObj 对象。

在本例中,调用者一共创建了两个 SampleClass 实例,继而分别调用:

samplel.StartTl();
sample2.StartT2();

也就是说,以上代码锁定的是两个不同的 syncObj ,这等于完全没有达到两个线程锁定同一个对象的目的。

要修正以上错误,只要将 syncObj 变成 static 就可以了。

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

第二个注意事项:在非静态方法中,静态变量不应作为同步对象。

上文说到,要修正第一个注意事项中的示例问题,需要将 syncObj 变成 static。这似乎和本注意事项有矛盾。事实上,第一个注意事项中的示例代码仅仅出于演示的目的,在实际应用中,我们非常不建议编写此类代码。

在编写多线程代码时,要遵循这样的一个原则:

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

FCL中的绝大部分类都遵循了这个原则。

像上一个示例中,如果将 syncObj 变成 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个建议 / 陆敏技著.一北京:机械工业出版社,2011.9文章来源地址https://www.toymoban.com/news/detail-553279.html

到了这里,关于编码技巧 --- 同步锁对象的选定的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 进程线程知识之线程同步

    线程同步原因 互斥锁 信号量 条件变量 读写锁 1、线程同步原因 线程同步是为了 对共享资源的访问进行保护 ,目的是为了 解决数据一致性的问题 。 出现数据一致性问题本质在于进程中的多个线程对 共享资源的并发访问 (同时访问)。 为确保不会出现对共享资源的并发访

    2024年02月10日
    浏览(40)
  • Linux 线程和线程同步

     【操作系统】2.进程和线程 - imXuan - 博客园 (cnblogs.com) 线程:light weight process(LWP)轻量级的进程,在 Linux 中本质上仍然是一个进程 进程:有独立的地址空间,独立PCB,可以当作只有一个线程的进程。进程是计算机 资源分配的最小单位 线程:有独立的PCB,共享物理地址空间

    2024年02月05日
    浏览(41)
  • 基于物理层网络编码的相位同步算法matlab仿真

    目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 matlab2022a         基于物理层网络编码的相位同步算法是一种利用物理层网络编码技术来实现相位同步的算法。这种算法的原理是将两个或多个相位不同的信号进行叠加,产生

    2024年02月09日
    浏览(39)
  • 线程条件控制实现线程的同步

            前面讲了互斥锁,但是总感觉有些功能互斥锁有些不够用。    条件变量是线程另一可用的同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。 条件本身是由互斥量保护的。线程

    2024年02月05日
    浏览(44)
  • 基于多线程并发-线程同步-系统实现

    系统实现:相对于STL来说非标准的实现,Linux和Windows平台各自的实现。 线程同步:通过限制多个线程同时执行某段代码来保护资源的。 1、线程互斥量 pthread_mutex_t 的初始化 a、定义再初始化: pthread_mutex_init函数的第二个参数attr是定义互斥锁的属性,一般为NULL。成功初始化返

    2024年02月08日
    浏览(38)
  • Linux——线程3|线程互斥和同步

    我们上一篇提到过,多个线程执行下面代码可能会出错,具体原因可查看上一篇Linux博客。 为避免这种错误的出现,我们可采用加锁保护。 PTHREAD_MUTEX_INITIALIZER 用pthread_mutex_t定义一把锁。ptherad_mutex_init是对锁进行初始化的函数。如果这把锁是全局的并且是静态定义的,我们可

    2024年02月05日
    浏览(48)
  • Java多线程 - 线程安全和线程同步解决线程安全问题

    线程安全问题指的是: 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。 举例: 取钱模型演示 需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。 如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问

    2023年04月15日
    浏览(41)
  • verilog设计技巧 (1) :复位技术(同步复位、异步复位、异步复位同步释放)

    一、复位的类型和划分 通常,芯片的复位信号分为两大类, 全局复位 和 局部复位 ; 全局复位: 能够确保每个寄存器都处于可控的状态; 局部复位: 基于软件功能的需求而存在的独立复位,对于某一个模块的单独控制,建议使用局部复位; ( 等待完善 ) 二、同步复位 定义

    2023年04月21日
    浏览(40)
  • 【Windows线程开发】Windows线程同步技术

    我们在上一章节中讲解了关于Windows的线程基础,相信大家已经对线程有了基本的概念。这一章节中,我们来讲讲线程同步技术,包括加锁技术(原子锁和互斥体)和事件,信号量。 原子锁主要解决的问题是多线程在操作符方面的问题。 相关问题: 多个线程对同一个数据进行

    2024年02月04日
    浏览(55)
  • Linux--线程-条件控制实现线程的同步

    1.条件变量 条件变量是线程另一可用的同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。 条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之

    2024年02月05日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包