关于多线程:在C#中,最好使用Queue.Synchronized或lock()来保证线程安全吗?

In C# would it be better to use Queue.Synchronized or lock() for thread safety?

我有一个队列对象,我需要确保它是线程安全的。最好使用这样的锁对象:

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中,对更高级别的抽象(包括无锁代码)有更多的支持。不管怎样,我还是不建议使用同步包装器。


旧集合库中的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应该有一大堆正确实现的线程安全集合,但不幸的是,这还需要将近一年的时间。


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

因此,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()的情况下访问队列,那么除非您的所有队列访问都被锁定,否则您将陷入困境。