C# 中各种线程同步选项有啥区别?

Posted

技术标签:

【中文标题】C# 中各种线程同步选项有啥区别?【英文标题】:What are the differences between various threading synchronization options in C#?C# 中各种线程同步选项有什么区别? 【发布时间】:2010-09-23 00:05:24 【问题描述】:

谁能解释一下两者的区别:

锁定(某个对象) 使用互斥锁 使用信号量 使用监视器 使用其他 .Net 同步类

我就是想不通。在我看来,前两个是一样的?

【问题讨论】:

这个链接对我帮助很大:albahari.com/threading 【参考方案1】:

在大多数情况下,您不应使用锁(=监视器)或互斥体/信号量。它们都在同步操作时阻塞等待线程。所以它们只适用于非常小的操作。

而且你绝对不应该使用 System.Collections.Concurrent 类 - 它们不支持具有多个集合的事务,并且还使用阻塞同步。

令人惊讶的是 .NET 没有有效的非阻塞同步机制。

我在 C# 上实现了来自 GCD (Objc/Swiftworld) 的 serial queue - 非常轻量级,不阻塞使用线程池的同步工具,带有测试。

在大多数情况下,这是同步任何东西的最佳方式 - 从数据库访问 (hello sqlite) 到业务逻辑。

【讨论】:

【参考方案2】:

我在 DotGNU 中为线程提供了类和 CLR 支持,我有一些想法...

除非您需要跨进程锁,否则应始终避免使用互斥锁和信号量。 .NET 中的这些类是 Win32 Mutex 和 Semaphores 的包装器,并且重量相当大(它们需要将上下文切换到内核中,这很昂贵 - 特别是如果您的锁没有处于争用状态)。

正如其他人提到的,C# lock 语句是 Monitor.Enter 和 Monitor.Exit 的编译器魔法(存在于 try/finally 中)。

Monitor 有一个简单但强大的信号/等待机制,而 Mutexes 通过 Monitor.Pulse/Monitor.Wait 方法没有这种机制。 Win32 等价物将是通过 CreateEvent 的事件对象,它实际上也作为 WaitHandles 存在于 .NET 中。 Pulse/Wait 模型类似于 Unix 的 pthread_signal 和 pthread_wait,但速度更快,因为它们在非竞争情况下可以完全是用户模式操作。

Monitor.Pulse/Wait 易于使用。在一个线程中,我们锁定一个对象,检查一个标志/状态/属性,如果它不是我们所期望的,调用 Monitor.Wait 它将释放锁定并等待直到发送一个脉冲。当等待返回时,我们循环返回并再次检查标志/状态/属性。在另一个线程中,每当我们更改标志/状态/属性时,我们都会锁定对象,然后调用 PulseAll 来唤醒所有正在侦听的线程。

通常我们希望我们的类是线程安全的,所以我们在代码中加了锁。但是,通常情况下,我们的类只会被一个线程使用。这意味着锁不必要地减慢了我们的代码……这就是 CLR 中的巧妙优化可以帮助提高性能的地方。

我不确定 Microsoft 的锁实现,但在 DotGNU 和 Mono 中,锁状态标志存储在每个对象的标头中。 .NET(和 Java)中的每个对象都可以成为锁,因此每个对象都需要在其标头中支持这一点。在 DotGNU 实现中,有一个标志允许您为每个用作锁的对象使用全局哈希表——这有利于消除每个对象的 4 字节开销。这对内存来说不是很好(尤其是对于线程不重的嵌入式系统),但会影响性能。

Mono 和 DotGNU 都有效地使用互斥锁来执行锁定/等待,但使用自旋锁样式 compare-and-exchange 操作来消除实际执行硬锁的需要,除非确实有必要:

您可以在此处查看如何实施监视器的示例:

http://cvs.savannah.gnu.org/viewvc/dotgnu-pnet/pnet/engine/lib_monitor.c?revision=1.7&view=markup

【讨论】:

【参考方案3】:

如果可以的话,我会尽量避免使用“lock()”、“Mutex”和“Monitor”......

查看 .NET 4 中的新命名空间 System.Collections.Concurrent 它有一些不错的线程安全集合类

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

并发字典太棒了!我不再需要手动锁定!

【讨论】:

避免锁定但使用监视器?为什么? @mafutrct 因为你需要自己处理同步。 哦,现在我明白了,您的意思是要避免所有提到的三个想法。听起来你会使用 Monitor 但不使用 lock/Mutex。 永远不要使用 System.Collections.Concurrent。它们是竞争条件的主要来源,也阻塞调用者线程。【参考方案4】:

锁定您使用字符串 ID 标识的任何共享互斥锁的另一个警告是,它将默认为“本地\”互斥锁,并且不会在终端服务器环境中的会话之间共享。

在您的字符串标识符前加上“Global\”,以确保对共享系统资源的访问得到适当控制。在我意识到这一点之前,我刚刚遇到了一大堆与在 SYSTEM 帐户下运行的服务同步通信的问题。

【讨论】:

【参考方案5】:

正如 ECMA 中所述,您可以从 Reflected 方法中观察到 lock 语句基本上等同于

object obj = x;
System.Threading.Monitor.Enter(obj);
try 
   …

finally 
   System.Threading.Monitor.Exit(obj);

从上述示例中,我们看到监视器可以锁定对象。

互斥锁在您需要进程间同步时很有用,因为它们可以锁定字符串标识符。不同的进程可以使用相同的字符串标识符来获取锁。

信号量就像类固醇上的互斥锁,它们通过提供最大并发访问计数来允许并发访问。一旦达到限制,信号量就会开始阻止对资源的任何进一步访问,直到其中一个调用者释放信号量。

【讨论】:

这个语法糖在 C#4 中略有改变,查看blogs.msdn.com/ericlippert/archive/2009/03/06/…【参考方案6】:

关于“使用其他 .Net 同步类”- 您应该了解的其他一些内容:

ReaderWriterLock - 允许多个读取器或单个写入器(不能同时) ReaderWriterLockSlim - 和上面一样,开销更低 ManualResetEvent - 打开时允许代码通过的门 AutoResetEvent - 同上,但打开后自动关闭

CCR/TPL(Parallel Extensions CTP)中还有更多(低开销)锁定结构 - 但 IIRC,这些将在 .NET 4.0 中提供

【讨论】:

所以如果我想要一个简单的信号通信(比如完成一个异步操作) - 我应该 Monitor.Pulse 吗?还是使用 SemaphoreSlim 或 TaskCompletionSource? 使用 TaskCompletionSource 进行异步操作。基本上,停止考虑线程并开始考虑任务(工作单元)。线程是一个实现细节,并不相关。通过返回 TCS,您可以返回结果、错误或处理取消,并且可以轻松地与其他异步操作(例如异步等待或 ContinueWith)组合。【参考方案7】:

很好的问题。我可能错了..让我试试..我的原始答案的修订版#2..有了更多的理解。感谢您让我阅读:)

锁(obj)

是用于(对象内?)线程同步的 CLR 构造。确保只有一个线程可以获取对象锁的所有权并进入锁定的代码块。其他线程必须等到当前所有者通过退出代码块来放弃锁定。此外,建议您锁定类的私有成员对象。

监视器

lock(obj) 是在内部使用 Monitor 实现的。 您应该更喜欢 lock(obj),因为它可以防止您像忘记清理过程一样搞砸。如果您愿意,它是“防白痴”的 Monitor 构造。 使用 Monitor 通常比互斥锁更受欢迎,因为 Monitor 是专门为 .NET Framework 设计的,因此可以更好地利用资源。

使用锁或监视器对于防止同时执行对线程敏感的代码块很有用,但这些构造不允许一个线程将事件传递给另一个线程。这需要同步事件,它们是具有两种状态之一的对象,有信号和无信号,可用于激活和挂起线程。 互斥量、信号量是操作系统级别的概念。例如,使用命名互斥锁,您可以跨多个(托管)exe 进行同步(确保您的应用程序只有一个实例在机器上运行。)

互斥:

然而,与监视器不同的是,互斥锁可用于跨进程同步线程。当用于进程间同步时,互斥锁被称为命名互斥锁,因为它将在另一个应用程序中使用,因此不能通过全局或静态变量共享。必须给它一个名称,以便两个应用程序都可以访问同一个互斥对象。 相比之下,Mutex 类是 Win32 构造的包装器。虽然它比监视器更强大,但互斥体需要的互操作转换比 Monitor 类所需的计算成本更高。

Semaphores(伤脑筋)。

使用 Semaphore 类来控制对资源池的访问。线程通过调用WaitOne方法进入信号量,该方法继承自WaitHandle类,并通过调用Release方法释放信号量。 每次线程进入信号量时,信号量的计数都会减少,而当线程释放信号量时,计数会增加。当计数为零时,后续请求会阻塞,直到其他线程释放信号量。当所有线程都释放了信号量时,计数为创建信号量时指定的最大值。 一个线程可以多次进入信号量..Semaphore 类不强制 WaitOne 或 Release 上的线程标识..程序员有责任不搞砸。 信号量有两种类型:本地信号量和命名系统信号量。如果使用接受名称的构造函数创建 Semaphore 对象,则它与该名称的操作系统信号量相关联。 命名系统信号量在整个操作系统中都是可见的,并且可用于同步过程。 本地信号量仅存在于您的进程中。它可以被进程中任何引用本地信号量对象的线程使用。每个 Semaphore 对象都是一个单独的本地信号量。

THE PAGE TO READ - Thread Synchronization (C#)

【讨论】:

您声称Monitor不允许通信不正确;您仍然可以使用MonitorPulse 查看信号量的替代描述 - ***.com/a/40473/968003。将信号量视为夜总会的保镖。俱乐部允许有特定数量的人同时进入。如果俱乐部已满,则不允许任何人进入,但一旦一个人离开,另一个人可能会进入。

以上是关于C# 中各种线程同步选项有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

多进程和多线程有啥区别?

NSNotification是同步还是异步?和delegate相比有啥区别,效率呢?

list和vector有啥区别

C# 异步方法加await调用不就变成同步方法了吗

深入解析Python中的线程同步方法

C#多线程:深入了解线程同步lock,Monitor,Mutex,同步事件和等待句柄(中)