我有一个队列对象,我需要确保它是线程安全的。最好使用这样的锁对象:
1 2 3 4
| lock(myLockObject)
{
//do stuff with the queue
} |
还是建议使用queue.synchronized,如下所示:
1
| Queue.Synchronized(myQueue).whatever_i_want_to_do(); |
号
从读取msdn文档来看,它说我应该使用queue.synchronized使其线程安全,但是它给出了一个使用锁对象的示例。来自msdn文章:
To guarantee the thread safety of the
Queue, all operations must be done
through this wrapper only.
Enumerating through a collection is
intrinsically not a thread-safe
procedure. Even when a collection is
synchronized, other threads can still
modify the collection, which causes
the enumerator to throw an exception.
To guarantee thread safety during
enumeration, you can either lock the
collection during the entire
enumeration or catch the exceptions
resulting from changes made by other
threads.
号
如果调用synchronized()不能确保线程安全,那么它的意义是什么?我是不是错过了什么?
就我个人而言,我总是喜欢锁门。这意味着您可以决定粒度。如果您仅仅依赖于同步包装器,那么每个单独的操作都是同步的,但是如果您需要执行多个操作(例如,对整个集合进行迭代),则无论如何都需要锁定。为了简单起见,我更喜欢只记住一件事——适当地锁定!
编辑:正如评论中提到的,如果你可以使用更高级的抽象,那就太好了。如果您确实使用锁,那么要小心使用它——记录您希望在哪里被锁上的内容,并尽可能短的时间内获取/释放锁(更多的是为了正确性而不是性能)。在持有锁时避免调用未知代码,避免嵌套锁等。
在.NET4中,对更高级别的抽象(包括无锁代码)有更多的支持。不管怎样,我还是不建议使用同步包装器。
- 也许可以添加一些关于无锁结构的内容?他们不保证有新的答复。
- @乔纳森:在这种特殊情况下,不知道有什么关于无锁结构的说法。我一般不会费心去尝试解除锁定——这涉及到很多头疼,而且我不记得曾经处于同步瓶颈的情况。
- 最好只使用线程安全容器,而不是在每次需要时自己实现线程安全。
- @Elazarleibovich:但是线程安全容器并不是真正的线程安全的。它隔离了单个操作,但是当需要复合操作时,您要做什么?此外,我经常发现,如果我需要对集合做一些事情,我还需要对类中的其他字段之一做一些事情。当然,这假设您不打算在API中公开集合本身——这将是一个坏主意。
- @琼斯基特:问题是说"我需要我的队列是线程安全的"。我认为您的思路中的一个正确答案是"不要使用线程安全队列,您可能不需要它,使用私有队列,并在使用它的操作之前锁定"。不管怎样,正如JCIP所说,我认为锁常常是并发性问题的错误答案。您可以通过线程安全消息队列和工作线程等解决许多情况。
- @Elazarleibovich:这取决于队列的用途。您似乎在假设发布者/消费者场景。这些天,我肯定会使用.NET 4并发队列来完成这类工作——但请记住,这要追溯到2008年,而OP实际上并不清楚这个队列的用途。我坚持不使用同步包装器的方法。根据具体情况,除了锁定之外,可能还有其他方法——但同步打包机很少是最佳选择,IMO。
- @Jonskeet,我不假设有发行者订户。我想说的是,许多问题都可以重新设计以适应工作队列/映射减少/任何可以使用高级并发模式且不使用锁定的模式,即使它们最初不是发布服务器订阅服务器问题。如果是这样的话,您最好使用这种方法,因为锁定对于难以检测的bug具有很好的潜力,即使是顶级并发专家也会使用锁定来处理bug。所以我至少会在回答中提到这一点。(不是我发明的,JCIP发明的)
- ElazarLeibovich:你一直提到JCIP,但这不是一个Java问题——在我写答案的时候,JavaUTIL并发中的高级抽象只是在.NET中不存在。现在.NET 4简化了很多…我正在编辑,但我认为你在评论中没有考虑到这个问题和答案的上下文。
- @琼斯基特,谢谢。最后一个评论,我真的认为JCIP包含许多以Java为中心的奇特,但它也包含与几乎所有的编程语言相关的建议。这不是我第一次听到"嘿,它是JCIP,我们不做Java,所以它是无关的",我真的认为这不是真的。你对上下文的评论是有效和正确的,我很抱歉没有注意到日期。有时,构建您所需要的高层次抽象是没有意义的,因为它的工作太多了,只需使用现有的东西就可以完成工作。谢谢,我道歉。
- @伊拉扎雷波维奇:不用担心,希望编辑最后能给出一个更好的答案。编辑是否涵盖了你所追求的大部分内容?我同意JCIP建议通常与其他平台相关,但前提是它建议您使用至少在您的平台上可用的东西:)我自己实现高层抽象的问题是,很难正确地获得——为一般情况设计一个高层抽象比做起来难。你需要的只是一个特定的案例。如果很清楚抽象应该是什么,那就太好了:)
- @Jonskeet编辑报道了,谢谢。实现一个通用的性能并发框架是很困难的,我不确定实现框架来解决您的问题,比用原始锁解决它要困难。看看golang.org,它有一个单独的阻塞队列和一个线程池,可以得到大量的可表示性(当然,select很难实现,但queue+线程池仍然非常有用)。实现这些并不难,您可以从书中复制实现。
- 了解这是一个较旧的答案,并发集合会使大部分问题变得毫无意义——但是锁定大量代码只意味着应用程序实际上没有进行并行处理。如果您需要不断地锁定以更新对象字段,那么您的设计可能会遇到更大的问题——尝试使用不可变的数据对其进行返工,这样您就有了一个更为并行的、线程安全的解决方案。
- @马蒂森:有时候你真的想对整个集合执行一系列的代码,同时仍然确信没有什么东西会改变集合。是的,这可以减少并行性。我完全赞成不变性和它带来的许多好处(不仅仅是线程安全方面),但这个答案的目的是在选择使用更高级别的锁还是使用"同步集合"(这通常是错误的IMO方法-如果不小心使用,至少是危险的)。
- 同步集合可能是一种错误的方法,但我认为这比简单地使用锁来让代码工作得不好要好得多。它现在隐藏了一个坏的设计。不过,总的来说,语义——幸运的是,有了更好的集合,最简单的路径就是更好的路径。
- @马蒂森:当你说这是一个糟糕的设计时,你做了很多假设。在某些情况下,它是一个完全合理的设计,实际上几乎不会影响性能。这不像并行收集也没有权衡。
- 过度使用锁作用域是一个糟糕的设计,因为此时您基本上是在运行一个单线程应用程序,但是由于管理锁的复杂性。客观上,这总是一个糟糕的设计。现在,什么是"过度使用"是值得争论的,但是您的设计应该总是尽可能减少对锁的需求。
- @马西森:是的-我的观点是,你假设我的答案是"过度使用"锁定范围,而我们没有足够的证据来证明OP实际上在做什么。您所谓的"过度使用"似乎是"在一个锁中运行整个应用程序"(否则它不会"基本上运行一个单线程应用程序"——只是因为存在一些争用并不意味着永久争用)。
- 我的想法不是我的,这是一个简单的事实,随着锁量的增加,应用程序的性能与单线程应用程序相同。因此,从性能角度来看,最小化所需锁量的设计更好。我不明白你怎么会认为这是有争议的,但对每个人都是有争议的。
- @马蒂森:我反对的是你从我的回答到我鼓励所有事情都要一直锁着。但如果你认为我的回答没有帮助,那就去投反对票,加上你自己的答案。我已经做完了。
旧集合库中的Synchronized方法存在一个主要问题,因为它们在太低的粒度级别(每个方法而不是每个工作单元)进行同步。
有一个典型的带有同步队列的竞争条件,如下所示,检查Count是否可以安全地出列,但Dequeue方法抛出一个异常,指示队列为空。这是因为每个单独的操作都是线程安全的,但是Count的值可以在查询和使用值之间进行更改。
1 2 3 4 5 6 7
| object item;
if (queue.Count > 0)
{
// at this point another thread dequeues the last item, and then
// the next line will throw an InvalidOperationException...
item = queue.Dequeue();
} |
您可以在整个工作单元周围使用手动锁安全地编写此命令(即检查计数并将项目排出列),如下所示:
1 2 3 4 5 6 7 8
| object item;
lock (queue)
{
if (queue.Count > 0)
{
item = queue.Dequeue();
}
} |
号
因此,由于您不能安全地从同步队列中出列任何内容,所以我不必为此费心,只需使用手动锁定。
.NET4.0应该有一大堆正确实现的线程安全集合,但不幸的是,这还需要将近一年的时间。
- +1表示粒度。
- +1例如显示粒度
- 当然,现在已经存在ConcurrentQueue——这个问题是用TryDequeue()解决的,所以你不能犯这个错误。
"线程安全集合"的需求和以原子方式对集合执行多个操作的需求之间常常存在紧张关系。
因此,synchronized()为您提供了一个集合,如果多个线程同时向其添加项,它不会自动崩溃,但它不会神奇地为您提供一个集合,该集合知道在枚举期间,没有其他人必须接触它。
除了枚举之外,常见的操作如"这个项目已经在队列中了吗?"不,然后我再补充一句"还需要同步,这比队列更宽。"
这样我们就不需要锁定队列就可以发现它是空的。
1 2 3 4 5 6 7 8 9 10 11
| object item;
if (queue.Count > 0)
{
lock (queue)
{
if (queue.Count > 0)
{
item = queue.Dequeue();
}
}
} |
在我看来,使用锁(…)…锁是正确的答案。
To guarantee the thread safety of the Queue, all operations must be done through this wrapper only.
号
如果其他线程在不使用.synchronized()的情况下访问队列,那么除非您的所有队列访问都被锁定,否则您将陷入困境。
- 本文讨论的包装器是synchronized()包装器,而不是lock()包装器。
- 对-但在一个线程中使用它并不能使其他线程兼容。关键是,如果你只依赖于.synch(),你就可以更容易地把自己搞得一团糟。