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

187

有人能解释一下以下几种方式的区别吗:

  • lock (someobject) {}
  • 使用 Mutex
  • 使用 Semaphore
  • 使用 Monitor
  • 使用其他 .Net 同步类

我就是搞不懂,前两种方式好像是一样的?


1
这个链接帮了我很大的忙:http://www.albahari.com/threading/ - Raphael
7个回答

151

这是一个很好的问题。我可能会错,让我试试。我的原始答案第二次修订。现在我更加理解了。感谢你让我读一下 :)

lock(obj)

  • 是CLR中的一个构造用于线程同步。确保只有一个线程可以拥有对象的锁并进入代码块。其他线程必须等待当前所有者通过退出代码块来放弃锁。此外,建议您在类的私有成员对象上进行锁定。

监视器

  • lock(obj)在内部使用Monitor实现。 您应该优先选择lock(obj),因为它可以防止您忘记清理程序等失误。如果您愿意,它还可以“傻瓜式”地使用Monitor构造。
    通常情况下,使用监视器比使用互斥量更好,因为监视器专为.NET Framework设计,因此能更好地利用资源。

使用锁或监视器可用于防止线程敏感代码的同时执行,但这些结构不允许一个线程向另一个线程通信事件。这需要同步事件,即具有两个状态(已标记和未标记)的对象,可用于激活和挂起线程。 Mutex、Semaphores是操作系统级别的概念。例如,使用命名互斥体可以在多个(托管的)exe之间进行同步(确保应用程序在机器上只运行一个实例)。

互斥体:

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

信号量(伤脑筋)。

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

需要阅读的页面-线程同步(C#)


19
你声称“Monitor”不允许通信是不正确的;你仍然可以使用“Monitor”来执行“Pulse”等操作。 - Marc Gravell
4
请查看Semaphores的另一种描述 - https://dev59.com/fnVD5IYBdhLWcg3wRpaX#40473。把信号量想象成夜店里的保安。夜店里只允许有限数量的人进入,如果夜店已经满了,则不允许有人再进去,但只要有一个人离开,另一个人就可以进去。 - Alex Klaus

31

关于"使用其他 .Net 同步类",以下是你应该了解的一些其他同步类:

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


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

15

正如ECMA所述,并且正如您可以从反射方法中观察到的那样,lock语句基本上等效于

object obj = x;
System.Threading.Monitor.Enter(obj);
try {
   …
}
finally {
   System.Threading.Monitor.Exit(obj);
}

从上面的例子中,我们可以看到监视器可以锁定对象。

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

信号量就像是被强化了的互斥量,它们允许并发访问,提供一个最大并发访问计数。一旦达到限制,信号量将开始阻塞任何对资源的进一步访问,直到其中一个调用者释放信号量。


5
这种语法糖在C#4中略有改变。 请查看http://blogs.msdn.com/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx。 - Peter Gfader

14
我为DotGNU编写了与线程相关的类和CLR支持,并有一些想法...
除非您需要跨进程锁,否则应始终避免使用Mutex和Semaphores。 .NET中的这些类是Win32 Mutex和Semaphores的包装器,比较重(它们需要切换到内核,这很昂贵-特别是如果您的锁没有争用)。
正如其他人提到的那样,C# lock语句是Monitor.Enter和Monitor.Exit的编译器魔法(存在于try / finally中)。
监视器具有简单但强大的信号/等待机制,而Mutexes没有通过Monitor.Pulse / Monitor.Wait方法。 Win32等效物将是通过CreateEvent实际上也存在于.NET中作为WaitHandles的事件对象。 Pulse / Wait模型类似于Unix的pthread_signal和pthread_wait,但速度更快,因为在未争用情况下可以完全是用户模式操作。
Monitor.Pulse/Wait很容易使用。在一个线程中,我们锁定一个对象,检查标志/状态/属性,如果不是我们期望的,调用Monitor.Wait会释放锁并等待直到发送脉冲。当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


9

如果您要对任何使用字符串ID标识的共享Mutex进行锁定,请注意它将默认为“Local \”mutex,并且在终端服务器环境中不会共享。

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


5

2
避免使用锁,而是使用监视器?为什么呢? - mafu
因为你需要自己注意同步。 - Peter Gfader
哦,现在我明白了,你的意思是要避免所有提到的三个想法。听起来像是你会使用 Monitor,但不使用 lock/Mutex。 - mafu
永远不要使用System.Collections.Concurrent。它们是竞态条件的主要来源,也会阻塞调用者线程。 - Alexander Danilov

-4
在大多数情况下,您不应该使用锁(=监视器)或互斥体/信号量。它们都会阻塞等待线程的时间进行同步操作。因此,它们仅适用于非常小的操作。
而且,您绝对不应该使用System.Collections.Concurrent类-它们不支持与多个集合的事务,并且还使用阻塞同步。
令人惊讶的是,.NET没有有效的非阻塞同步机制。
我在C#上实现了GCD(Objc/Swift世界)的串行队列-一种非常轻量级的、不阻塞的同步工具,它使用线程池,并带有测试。
这是大多数情况下同步任何内容的最佳方法-从数据库访问(你好sqlite)到业务逻辑。

你的回答无非是对一个名字平淡无奇的第三方库(https://github.com/Gentlee/SerialQueue)的推广,而且是一个糟糕的推广。没有使用案例,没有代码示例,也没有与当前已经建立起来的技术在性能和易用性方面的比较。只有大胆的宣称和空洞的论据。在我看来,这个回答只值得被删除。 - undefined
@TheodorZoulias,实际上这里有一个代码示例,并且有足够的解释来与其他方法进行比较,并说明为什么它更好。只是你对多线程的理解显然存在重大缺乏。我可以写一篇关于这个问题的完整文章,但是我没有意愿和时间。因此,这个回答只是给那些真正寻求真相并具有一些基础知识的人,如果你无法理解,请忽略它。 - undefined
亚历山大,你要求反馈,你得到了一些。你的回应没有给我任何理由考虑撤销我的负评。我试着阅读了你库中的文档和示例,但对我来说一无所获。我不会再尝试了,因为这看起来是浪费我的时间。祝你好运,希望能找到那些会为你的回答找到价值并给予赞同的C#多线程专家。 - undefined
1
@Theodor Zoulias,我主要是在询问关于我回答中技术部分的有效论点,指出那些给我点踩的人只是不理解它,而你完全证实了这一点。对此表示感谢 :) - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接