【问题标题】:What is non-thread-safety for?什么是非线程安全的?
【发布时间】:2011-01-07 19:05:37
【问题描述】:

有很多文章和讨论解释了为什么构建线程安全类是好的。据说如果多个线程访问例如同时一个领域,只能有一些不好的后果。那么,保持线程安全的代码有什么意义呢?我主要关注 .NET,但我认为主要原因与语言无关。

例如.NET 静态字段不是线程安全的。 如果它们默认是线程安全的,结果会是什么? (无需执行“手动”锁定)。使用(实际上默认为)非线程安全有什么好处?

我想到的一件事是性能(不过更多的是猜测)。相当直观的是,当一个函数或字段不需要是线程安全的时,它不应该是。然而,问题是:为了什么?线程安全只是您始终需要实现的额外代码量吗?在什么情况下我可以 100% 确定,例如一个字段不会同时被两个线程使用?

【问题讨论】:

    标签: c# .net multithreading language-agnostic thread-safety


    【解决方案1】:

    编写线程安全代码:

    1. 需要更熟练的开发人员
    2. 更难并且需要更多的编码工作
    3. 更难测试和调试
    4. 通常具有更大的性能成本

    但是!并不总是需要线程安全的代码。如果您可以确定只有一个线程可以访问某些代码,那么上面的列表就会变得巨大且不必要的开销。就像你们两个人,行李不多的时候去邻居城市租一辆面包车一样。

    【讨论】:

    • 但是你什么时候可以肯定地说 DLL 永远不会使用线程?客户呢,他们想尝试你的图书馆吗?是否应该使所有公共方法线程安全?关于面包车:有些人喜欢吹嘘。 :)
    • @rook:记住,如果它是一个好的库,你不必关心它是如何实现的:它不应该让你访问线程不安全的东西,除非它非常仔细地标记它们给你。
    • @rook:关于客户试验他们的库,除非它明确记录一个方法是线程安全的,否则可以安全地假设它不是。客户在使用您的类时承担维护线程安全的责任。
    • @rook 对。如果没有明确声明线程安全,则假定没有。问题是线程安全有不同级别:方法可以是线程/不安全的,而类是。当库是(您可以创建多个实例)时,类可以是安全的/不安全的,有时整个库不是线程安全的。但您还有其他选择,例如创建多个应用域。
    • @rook:理想情况下,库代码应该从不使用任何形式的全局变量/全局状态。 (这包括像“singletons”这样的委婉说法。)如果需要,您应该始终用锁保护它们,以防调用者被线程化。但通常这还不够,因为调用者可能有 2 个不同的组件(可能是库本身),每个组件都假定它们对库的内部状态具有独占控制权。所以只是不要在库中使用全局变量。期间。
    【解决方案2】:

    线程安全是有代价的——如果同时访问,您需要锁定可能导致问题的字段。

    在不使用线程但在每个 cpu 周期都很重要时需要高性能的应用程序中,没有理由拥有安全线程类。

    【讨论】:

    • 这些是什么问题?如果两个线程只对一个字段执行读取操作会发生什么?
    • @rook:如果两个线程对一个字段执行读操作,它们将读取该字段的值。现在如果两个线程都说同时修改字段,你可能会遇到问题。
    【解决方案3】:

    那么,保留非线程安全代码的意义何在?

    成本。就像您假设的那样,性能通常会受到影响。

    此外,编写线程安全代码更加困难和耗时。

    【讨论】:

      【解决方案4】:

      线程安全不是“是”或“否”的命题。 “线程安全”的含义取决于上下文;这是否意味着“并发读取安全,并发写入不安全”?这是否意味着应用程序可能会返回陈旧数据而不是崩溃?它可能意味着很多事情。

      不让类“线程安全”的主要原因是成本。如果该类型不会被多个线程访问,那么投入工作并增加维护成本是没有好处的。

      【讨论】:

        【解决方案5】:

        编写线程安全代码有时非常困难。例如,简单的延迟加载需要对 '== null' 和一个锁进行两次检查。真的很容易搞砸。

        [编辑]

        我并不是说线程延迟加载特别困难,而是“哦,我不记得先锁定它!”一旦你认为你已经完成了真正具有挑战性的锁定,那一刻就来得又快又艰难。

        【讨论】:

        • 它也很慢。 THigns 必须是易失的,因此编译器不会优化访问。易失性意味着缓存刷新。基本上 CPU 处理速度会慢很多。
        • @TomTom:当然,我只是给出我不一直并行执行的主要原因。除非他们注意到速度,否则您的普通消费者不会太担心速度,这对于当今的大多数应用程序来说是不太可能的。
        • 啊,不错 ;) 我希望我能拥有这种奢侈 - 处理大量数据或时间非常关键的数据会改变我的观点。
        • 是的,当我在一个研究机构工作时,我们绝对关心速度,因为几个额外的 CPU 周期意味着更多的处理时间。现在我构建应用程序,处理时间仅在它中断 UI 绘制时才重要。 :)
        【解决方案6】:

        在某些情况下“线程安全”没有意义。除了更高的开发人员技能和增加的时间(开发、测试和运行时都会受到影响)之外,还要考虑这一点。

        例如,List<T> 是一个常用的非线程安全类。如果我们要创建一个线程安全的等价物,我们将如何实现GetEnumerator?提示:没有好的解决方案。

        【讨论】:

        • 但是,仍然存在一些。这样的实现有什么问题?
        • 我不确定您所说的“仍然存在一些”是什么意思。拥有真正线程安全的数据结构的唯一方法是使它们不可变。
        • 其实“还是”就是要三振。我的意思是存在一些解决方案,它们似乎有效地处理了线程安全问题。也许他们不像你说的那样“好”。
        • @rook:我的意思是某些类(例如,List<T>)实际上不能成为线程安全的。您可以创建其他线程安全集合(例如ConcurrentQueue<T>),或者选择使其不可变,但您不能拥有与非线程安全List<T> 具有相同语义的线程安全List<T>
        • @StephenCleary:List<T> 上的索引操作不能是有意义的线程安全的,如果在确定想要的项目的索引和操作的时间之间有可能删除项目的话那个索引。但是,可以拥有List<T> 功能的有用线程安全子集,包括例如所有不删除项目的成员,或包括 AddRemoveRemoveAt(0)Item(0).GetGetEnumerator,但没有其他索引操作(缺少 Try 读取项目 0 的方法会令人厌烦,但并非不可克服)。
        【解决方案7】:

        把这个问题转过来。

        在编程的早期,没有线程安全代码,因为没有线程的概念。一个程序开始了,然后一步一步地进行到最后。事件?那是什么?线程?嗯?

        随着硬件变得更加强大,软件可以解决哪些类型的问题的概念变得更加富有想象力,开发人员也更加雄心勃勃,软件基础架构也变得更加复杂。它也变得更加头重脚轻。而今天,我们拥有一个复杂、强大、在某些情况下不必要地头重脚轻的软件生态系统,其中包括线程和“线程安全”。

        我意识到这个问题更多地针对应用程序开发人员,而不是固件开发人员,但纵观整个森林确实可以深入了解这棵树是如何演变的。

        【讨论】:

          【解决方案8】:

          那么,保留非线程安全代码的意义何在?

          通过允许不是线程安全的代码,您将其留给程序员来决定正确的隔离级别是什么。

          正如其他人所提到的,这可以降低复杂性并提高性能。

          Rico Mariani 写了两篇题为“Putting your synchronization at the correct level”和 Putting your synchronization at the correct level -- solution 有一个很好的例子。

          在文章中他有一个方法叫DoWork()。在其中他调用了其他类Read 两次Write 两次然后LogToSteam

          ReadWriteLogToSteam 都共享一个锁并且是线程安全的。这很好,除了因为DoWork 也是线程安全的,每个ReadWriteLogToSteam 中的所有同步工作完全是浪费时间。

          这都与命令式编程的本质有关。它的副作用导致需要这样做。

          但是,如果您有一个开发平台,可以将应用程序表示为没有依赖关系或副作用的纯函数,那么就可以创建无需开发人员干预即可管理线程的应用程序。

          【讨论】:

            【解决方案9】:

            So, what is the point of keeping non thread-safe code?

            经验法则是尽可能避免锁定。 Ideal 代码是可重入和线程安全的,没有任何锁定。但这将是乌托邦。

            回到现实,good 程序员尽其所能拥有部分锁定,而不是锁定整个上下文。一个例子是在各种例程中一次锁定几行代码,而不是在一个函数中锁定所有内容。

            因此,此外,必须重构代码以提出一种设计,如果不完全消除锁定,则可以最大限度地减少锁定。

            例如考虑一个foobar() 函数,该函数在每次调用时获取新数据,并在一种数据类型上使用switch() case 来更改树中的节点。大多数情况下可以避免锁定(如果不是完全),因为每个 case 语句都会触及树中的不同节点。这可能是一个更具体的例子,但我认为它阐述了我的观点。

            【讨论】:

              猜你喜欢
              • 2010-12-10
              • 1970-01-01
              • 2012-09-19
              • 1970-01-01
              • 2011-08-13
              • 2015-06-02
              相关资源
              最近更新 更多