62

我有一个 Queue 对象,我需要确保它是线程安全的。使用这样的锁定对象会更好:

lock(myLockObject)
{
//do stuff with the queue
}

还是建议像这样使用 Queue.Synchronized :

Queue.Synchronized(myQueue).whatever_i_want_to_do();

通过阅读 MSDN 文档,它说我应该使用 Queue.Synchronized 使其成为线程安全的,但随后它给出了一个使用锁定对象的示例。来自 MSDN 文章:

为了保证 Queue 的线程安全,所有的操作都只能通过这个 wrapper 来完成。

通过集合进行枚举本质上不是线程安全的过程。即使一个集合被同步,其他线程仍然可以修改该集合,这会导致枚举器抛出异常。为了保证枚举过程中的线程安全,您可以在整个枚举过程中锁定集合,也可以捕获其他线程更改导致的异常。

如果调用 Synchronized() 不能确保线程安全,那有什么意义呢?我在这里错过了什么吗?

4

5 回答 5

50

就个人而言,我总是更喜欢锁定。这意味着可以决定粒度。如果您只依赖 Synchronized 包装器,则每个单独的操作都是同步的,但如果您需要做不止一件事(例如遍历整个集合),则无论如何都需要锁定。为了简单起见,我更喜欢只记住一件事——适当地锁定!

编辑:如评论中所述,如果您可以使用更高级别的抽象,那就太好了。如果您确实使用了锁定,请小心使用它 - 记录您希望在哪里锁定的内容,并在尽可能短的时间内获取/释放锁定(更多的是为了正确性而不是性能)。避免在持有锁时调用未知代码,避免嵌套锁等。

在 .NET 4 中,对更高级别的抽象(包括无锁代码)提供了更多支持无论哪种方式,我仍然不建议使用同步包装器。

于 2008-12-03T21:25:12.517 回答
35

旧集合库中的方法存在一个主要问题Synchronized,因为它们同步的粒度级别太低(每个方法而不是每个工作单元)。

有一个带有同步队列的经典竞争条件,如下所示,您可以在其中检查Count出队是否安全,但随后该Dequeue方法会引发异常,指示队列为空。发生这种情况是因为每个单独的操作都是线程安全的,但是Count当您查询它和使用该值时,它的值可能会发生变化。

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();
}

您可以使用手动锁定整个工作单元(即检查计数并将项目出列)安全地编写此代码,如下所示:

object item;
lock (queue)
{
    if (queue.Count > 0)
    {
        item = queue.Dequeue();
    }
}

因此,由于您无法安全地将任何内容从同步队列中出列,因此我不会打扰它,只会使用手动锁定。

.NET 4.0 应该有一大堆正确实现的线程安全集合,但不幸的是,这还有将近一年的时间。

于 2008-12-03T21:49:54.730 回答
16

对“线程安全集合”的需求与以原子方式对集合执行多个操作的需求之间经常存在紧张关系。

因此 Synchronized() 为您提供了一个集合,如果多个线程同时向其中添加项目,它不会自行破坏,但它不会神奇地为您提供一个知道在枚举期间没有其他人必须触摸它的集合。

除了枚举之外,诸如“这个项目已经在队列中了吗?不,那我会添加它”之类的常见操作也需要比队列更宽的同步。

于 2008-12-03T21:19:41.217 回答
7

这样我们就不需要锁定队列来发现它是空的。

object item;
if (queue.Count > 0)
{
  lock (queue)
  {
    if (queue.Count > 0)
    {
       item = queue.Dequeue();
    }
  }
}
于 2008-12-03T22:29:24.900 回答
1

我似乎很清楚,使用 lock(...) {...} 锁是正确的答案。

为了保证 Queue 的线程安全,所有的操作都只能通过这个 wrapper 来完成。

如果其他线程在不使用 .Synchronized() 的情况下访问队列,那么您将陷入困境 - 除非您的所有队列访问都被锁定。

于 2008-12-03T21:17:14.107 回答