多线程概念和锁定在c#

时间:2012-04-14 09:46:27

标签: c# multithreading locking

我读过关于锁的内容,虽然根本没有理解。 我的问题是为什么我们使用未使用的object并锁定它以及如何使线程安全的东西或者它如何帮助多线程?没有其他方法来制作线程安全的代码。

public class test {
    private object Lock { get; set; }

    ...
    lock (this.Lock) { ... }
    ...
}

抱歉,我的问题非常愚蠢,但我不明白,虽然我已多次使用过它。

6 个答案:

答案 0 :(得分:3)

在其他线程修改它时从一个线程访问一段数据被称为"数据争用条件" (或者只是"数据竞争")并且可能导致数据损坏。 (*)

锁只是一种避免数据竞争的机制。如果两个(或更多)并发线程锁定相同的锁对象,则它们不再是并发的,并且在锁定期间不再导致数据争用。从本质上讲,我们序列化访问共享数据。

诀窍是将你的锁保持为"宽"因为你必须以避免数据争夺,仍然是"缩小"因为你可以通过并发执行获得性能。这是一个很好的平衡,可以很容易地从任何方向打破,这就是多线程编程很难的原因。

一些指导原则:

  • 只要所有线程只读取数据而没有人会修改它,就不需要锁定。
  • 相反,如果至少有一个线程可能在某个时刻修改数据,那么访问相同数据的所有并发代码路径必须通过锁定正确序列化,即使是那些只读取数据的锁定。
    • 在一个代码路径中使用锁定但不在另一个代码路径中使用锁定将使数据在竞争条件下保持开放状态。
    • 此外,在一个代码路径中使用一个锁定对象,但在另一个(并发)代码路径中使用不同的锁定对象不会序列化这些代码路径,并使您对数据竞争完全开放。
    • 另一方面,如果两个并发代码路径访问不同的数据,则它们可以使用不同的锁定对象。 但是,只要有多个锁定对象,请注意deadlocks。死锁通常也是一种代码竞争条件" (和heisenbug,见下文)。
  • 锁定对象不需要(并且通常不是)与您要保护的数据相同。不幸的是,没有语言设施可以让你宣布"哪些数据受哪个锁定对象的保护,因此您必须非常谨慎地记录您的锁定约定"对于可能维护你的代码的其他人,以及你自己(因为即使在很短的时间之后你忘记锁定约定的一些角落和缝隙)。
  • 通常最好尽可能多地保护锁定物体免受外界伤害。毕竟,你正在将它用于非常敏感的锁定任务,并且你不希望它以不可预见的方式被外部演员锁定。这就是为什么使用this或公共字段作为锁定对象通常是个坏主意。
  • lock关键字只是Monitor.EnterMonitor.Exit的更方便的语法。
  • 锁定对象可以是.NET中的任何对象, value objectsMonitor.Enter的调用中将装箱,这意味着线程不会共享相同的锁对象,使数据不受保护。因此,仅将引用类型用作锁定对象。
  • 对于进程间通信,您可以使用全局互斥锁,可以通过将非空name传递给Mutex Constructor来创建。全局互斥体提供与常规"本地"基本相同的功能。锁定,除了它们可以在不同的进程之间共享。
  • 除了锁之外还有其他同步机制,例如信号量,条件变量,消息队列或atomic operations。混合不同的同步机制时要小心。
  • 锁也表现为memory barriers,这在现代多核,多缓存CPU中越来越重要。这是部分之所以你需要锁定读取数据而不仅仅是写作。

(*)它被称为" race"因为并发线程是"赛车"对共享数据执行操作以及赢得该比赛的人确定操作的结果。因此,结果取决于执行的时间,这在现代抢占式多任务操作系统上基本上是随机的。更糟糕的是,通过调试器等工具观察程序执行的简单行为很容易修改时序,这使得它们成为"heisenbugs"(即观察到的现象仅通过观察行为而改变)。

答案 1 :(得分:1)

lock语句引入了互斥的概念。任何时候只有一个线程可以获取给定对象的锁定。这可以防止线程同时访问共享数据结构,从而破坏它们。

如果其他线程已经持有锁,则lock语句将阻塞,直到它能够在允许其块执行之前获取其参数的独占锁。

请注意,lock唯一能做的就是控制代码块的输入。访问该类的成员与锁完全无关。由类本身决定是否通过使用lock或其他同步原语来协调必须同步的访问。另请注意,可能无需同步对部分或全部成员的访问。例如,如果要维护计数器,可以使用Interlocked类而不锁定。


锁定的替代方法是无锁数据结构,它在存在多个线程时表现正常。必须非常谨慎地设计无锁数据结构的操作,通常在无锁原语(如比较和交换(CAS))的帮助下进行。

此类技术的一般主题是尝试以原子方式对数据结构执行操作,并检测操作何时因其他线程的并发操作而失败,然后重试。这种情况适用于容易出现故障的轻负载系统,但随着故障率的上升和重试成为主导负载,可能会产生失控行为。通过退出重试率可以改善这个问题,有效地限制负载。


更复杂的替代方案是软件事务内存。与CAS不同,STM将失败和重试的概念概括为任意复杂的内存操作。简单来说,您启动一​​个事务,执行所有操作,最后提交。系统检测操作是否由于其他线程执行的冲突操作而无法成功,这些线程将当前线程击败到冲头。在这种情况下,STM可能要么彻底失败,要求应用程序采取纠正措施,要么在更复杂的实现中,它可以自动返回到事务的开始并再试一次。

答案 2 :(得分:1)

当你有不同的线程同时访问同一个变量/资源时,他们可能会过度写入这个变量/资源,你可能会得到意想不到的结果。 Lock将确保只有一个线程可以按时评估变量并保持线程将排队以访问此变量/资源,直到锁定被释放

假设我们有帐户的余额变量。 两个不同的线程读取其值为100 假设第一个线程像100 + 50一样添加50并保存它并且余额将为150 由于第二个线程已经读取100并且意味着同时。假设它减去50像100-50,但请注意这里是第一个线程已经使余额150,所以第二个线程应该是150-50这可能会导致严重的问题。

所以锁定确保当线程上想要更改某些资源状态时它会锁定它并在提交更改后离开

答案 3 :(得分:1)

是的,确实有另一种方式:

using System.Runtime.CompilerServices;

class Test
{
    private object Lock { get; set; }

    [MethodImpl(MethodImplOptions.Synchronized)]
    public void Foo()
    {
        // Now this instance is locked
    }
}

虽然它看起来更“自然”,但它并不常用,因为对象以这种方式锁定本身,因此其他代码无法冒险锁定此对象 - 它可能会导致死锁。

因此,您通常会创建一个引用对象的(延迟初始化的)私有字段,并将该对象用作锁。这将保证没有其他人可以锁定与您相同的对象。


关于发动机罩下发生的事情的更多细节:

当您“锁定某个对象”时,您锁定对象本身。相反,您在整个程序中使用该对象作为内存中保证唯一的地址。当您“锁定”时,运行时获取对象的地址,使用它来查找另一个表(对您隐藏)中的实际锁,并使用 对象为“”锁定“(也称为”关键部分“)。

所以,对你而言,一个对象只是一个代理/符号 - 它本身并没有做任何事情;它只是作为一个独特的指标,永远不会与同一程序中的另一个有效对象发生冲突。

答案 4 :(得分:1)

Lock对象就像一个进入单个房间,每次只能有一个客人进入。 房间可以是您的数据,来宾可以是您的功能

  • 定义数据(房间)
  • 添加门(锁定对象)
  • 邀请嘉宾(职能)
  • 使用lock指令关闭/打开门,每次只允许一位客人进入房间。

为什么我们需要这个?如果你同时在一个文件中写一个数据(只是一个例子,可以是1000个其他的)你将需要同步你的函数访问(关闭/打开客人门)到写文件,所以任何函数都会追加到最后该文件(假设这是该例子的要求)

这自然不仅是同步线程的方式,还有更多:

  • 监视器
  • 等待hadlers ......

查看链接以获取每个链接的完整信息和说明

Thread Synchronization

答案 5 :(得分:1)

对于那些只熟悉C#中的lock关键字的人来说,这种混淆非常典型。你是对的,lock语句中使用的对象实际上只是一个定义关键部分的标记。该对象绝不会受到多线程访问本身的任何保护。

这种方式的工作方式是CLR在对象头(类型句柄)中保留一个称为同步块的4字节(32位系统)部分。同步块只不过是存储实际临界区信息的数组的索引。当您使用lock关键字时,CLR将相应地修改此同步块值。

这种方案有利有弊。优点是它为定义关键部分提供了相当优雅的解决方案。一个明显的缺点是每个对象实例都包含同步块,并且大多数实例从不使用它,所以在大多数情况下它似乎是浪费空间。另一个缺点是可以使用盒装值类型,这几乎总是错误的,肯定会导致混淆。

我还记得在.NET首次发布的时候,有很多关于lock关键字对该语言是好还是坏的喋喋不休。普遍的共识(至少我记得它)是因为using关键字可以很容易地被使用,所以它很糟糕。实际上,使用using关键字的解决方案实际上会更有意义,因为它可以在不需要同步块的情况下完成。 c#设计团队甚至记录在案,说如果他们有第二次机会lock关键字永远不会成为语言。 1


1 我能找到的唯一参考资料是Jon Skeet的网站here

相关问题