ChatGPT解决这个技术问题 Extra ChatGPT

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

有人可以解释以下之间的区别:

锁定(某个对象){}

使用互斥锁

使用信号量

使用监视器

使用其他 .Net 同步类

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

这个链接对我帮助很大:albahari.com/threading

G
Gishu

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

锁(对象)

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

监视器

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

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

互斥体:

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

Semaphores(伤脑筋)。

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

THE PAGE TO READ - Thread Synchronization (C#)


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

重新“使用其他 .Net 同步类”-您应该了解的其他一些:

ReaderWriterLock - 允许多个读取器或单个写入器(不是同时)

ReaderWriterLockSlim - 和上面一样,开销更低

ManualResetEvent - 打开时允许代码通过的门

AutoResetEvent - 同上,但打开后自动关闭

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


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

正如 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/…
t
tumtumtum

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

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

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

监视器有一个简单但强大的信号/等待机制,而 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


n
nvuono

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

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


P
Peter Gfader

如果可以的话,我会尽量避免使用“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。它们是竞争条件的主要来源,并且还会阻塞调用者线程。
A
Alexander Danilov

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

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

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

我在 C# 上实现了来自 GCD(Objc/Swift 世界)的 serial queue - 非常轻量级、非阻塞同步工具,它使用线程池和测试。

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