C# 在多线程场景中正确锁定 List<T> ?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1362995/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 15:40:17  来源:igfitidea点击:

Properly locking a List<T> in MultiThreaded Scenarios?

c#.netmultithreading

提问by Michael Stum

Okay, I just can't get my head around multi-threading scenarios properly. Sorry for asking a similar question again, I'm just seeing many different "facts" around the internet.

好吧,我就是无法正确理解多线程场景。很抱歉再次提出类似问题,我只是在互联网上看到了许多不同的“事实”。

public static class MyClass {
    private static List<string> _myList = new List<string>;
    private static bool _record;

    public static void StartRecording()
    {
        _myList.Clear();
        _record = true;
    }

    public static IEnumerable<string> StopRecording()
    {
        _record = false;
        // Return a Read-Only copy of the list data
        var result = new List<string>(_myList).AsReadOnly();
        _myList.Clear();
        return result;
    }

    public static void DoSomething()
    {
        if(_record) _myList.Add("Test");
        // More, but unrelated actions
    }
}

The idea is that if Recording is activated, calls to DoSomething() get recorded in an internal List, and returned when StopRecording() is called.

这个想法是,如果 Recording 被激活,对 DoSomething() 的调用会被记录在一个内部 List 中,并在调用 StopRecording() 时返回。

My specification is this:

我的规格是这样的:

  • StartRecording is not considered Thread-Safe. The user should call this while no other Thread is calling DoSomething(). But if it somehow could be, that would be great.
  • StopRecording is also not officially thread-safe. Again, it would be great if it could be, but that is not a requirement.
  • DoSomething has to be thread-safe
  • StartRecording 不被认为是线程安全的。用户应该在没有其他线程调用 DoSomething() 时调用它。但如果可以的话,那就太好了。
  • StopRecording 也不是正式的线程安全的。再说一次,如果可以的话就太好了,但这不是必需的。
  • DoSomething 必须是线程安全的

The usual way seems to be:

通常的方法似乎是:

    public static void DoSomething()
    {
        object _lock = new object();
        lock(_lock){
            if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }

Alternatively, declaring a static variable:

或者,声明一个静态变量:

    private static object _lock;

    public static void DoSomething()
    {
        lock(_lock){
            if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }

However, this answersays that this does not prevent other code from accessing it.

但是,此答案表示这不会阻止其他代码访问它。

So I wonder

所以我想知道

  • How would I properly lock a list?
  • Should I create the lock object in my function or as a static class variable?
  • Can I wrap the functionality of Start and StopRecording in a lock-block as well?
  • StopRecording() does two things: Set a boolean variable to false (to prevent DoSomething() from adding more stuff) and then copying the list to return a copy of the data to the caller). I assume that _record = false; is atomic and will be in effect immediately? So normally I wouldn't have to worry about Multi-Threading here at all, unless some other Thread calls StartRecording() again?
  • 我将如何正确锁定列表?
  • 我应该在我的函数中创建锁对象还是作为静态类变量?
  • 我也可以将 Start 和 StopRecording 的功能包装在一个锁块中吗?
  • StopRecording() 做两件事:将布尔变量设置为 false(以防止 DoSomething() 添加更多内容),然后复制列表以将数据副本返回给调用者)。我假设 _record = false; 是原子的,会立即生效吗?所以通常我根本不必担心多线程,除非其他线程再次调用 StartRecording()?

At the end of the day, I am looking for a way to express "Okay, this list is mine now, all other threads have to wait until I am done with it".

归根结底,我正在寻找一种表达“好吧,这个列表现在是我的,所有其他线程必须等到我完成它”的方法。

采纳答案by Henk Holterman

I will lock on the _myList itself here since it is private, but using a separate variable is more common. To improve on a few points:

我将在此处锁定 _myList 本身,因为它是私有的,但使用单独的变量更为常见。改进几点:

public static class MyClass 
{
    private static List<string> _myList = new List<string>;
    private static bool _record; 

    public static void StartRecording()
    {
        lock(_myList)   // lock on the list
        {
           _myList.Clear();
           _record = true;
        }
    }

    public static IEnumerable<string> StopRecording()
    {
        lock(_myList)
        {
          _record = false;
          // Return a Read-Only copy of the list data
          var result = new List<string>(_myList).AsReadOnly();
          _myList.Clear();
          return result;
        }
    }

    public static void DoSomething()
    {
        lock(_myList)
        {
          if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }
}

Note that this code uses lock(_myList)to synchronize access to both _myList and_record. And you need to sync all actions on those two.

请注意,此代码用于lock(_myList)同步对 _myList_record 的访问。并且您需要同步这两个上的所有操作。

And to agree with the other answers here, lock(_myList)does nothing to _myList, it just uses _myList as a token (presumably as key in a HashSet). All methods must play fair by asking permission using the same token. A method on another thread can still use _myList without locking first, but with unpredictable results.

并且同意这里的其他答案,lock(_myList)对 _myList 没有任何作用,它只是使用 _myList 作为标记(大概是 HashSet 中的键)。所有方法都必须通过使用相同的令牌请求许可来公平竞争。另一个线程上的方法仍然可以使用 _myList 而无需先锁定,但会产生不可预测的结果。

We can use any token so we often create one specially:

我们可以使用任何令牌,因此我们经常专门创建一个:

private static object _listLock = new object();

And then use lock(_listLock)instead of lock(_myList)everywhere.

然后使用lock(_listLock)而不是lock(_myList)无处不在。

This technique would have been advisable if myList had been public, and it would have been absolutely necessary if you had re-created myList instead of calling Clear().

如果 myList 是公开的,则这种技术是可取的,如果您重新创建 myList 而不是调用 Clear(),则绝对有必要。

回答by Jon Skeet

Creating a new lock in DoSomething()would certainlybe wrong - it would be pointless, as each call to DoSomething()would use a different lock. You should use the second form, but with an initializer:

在创建新锁DoSomething()肯定是错的-这将是毫无意义的,因为每次调用DoSomething()会使用不同的锁。您应该使用第二种形式,但使用初始化程序:

private static object _lock = new object();

It's true that locking doesn't stop anything else from accessing your list, but unless you're exposing the list directly, that doesn't matter: nothing else will be accessing the list anyway.

确实,锁定并不会阻止其他任何东西访问您的列表,但除非您直接公开该列表,否则这无关紧要:无论如何,没有其他任何东西会访问该列表。

Yes, you can wrap Start/StopRecording in locks in the same way.

是的,您可以以相同的方式将 Start/StopRecording 包装在锁中。

Yes, setting a Boolean variable is atomic, but that doesn't make it thread-safe. If you only access the variable within the same lock, you're fine in terms of both atomicity andvolatility though. Otherwise you might see "stale" values - e.g. you set the value to truein one thread, and another thread could use a cached value when reading it.

是的,设置布尔变量是原子的,但这并不能使它成为线程安全的。如果您只访问同一个锁中的变量,那么在原子性易变性方面都很好。否则,您可能会看到“陈旧”值——例如,您true在一个线程中将该值设置为,而另一个线程在读取它时可能会使用缓存值。

回答by Quintin Robinson

You may be misinterpreting the this answer, what is actually being stated is that they lockstatement is not actually locking the object in question from being modified, rather it is preventing any other code using that object as a locking source from executing.

您可能误解了这个答案,实际上是说他们的lock声明实际上并没有锁定有问题的对象被修改,而是阻止任何其他使用该对象作为锁定源的代码执行。

What this really means is that when you use the same instance as the locking object the code inside the lock block should not get executed.

这真正意味着当您使用与锁定对象相同的实例时,不应执行锁定块内的代码。

In essence you are not really attempting to "lock" your list, you are attempting to have a common instance that can be used as a reference point for when you want to modify your list, when this is in use or "locked" you want to prevent other code from executing that would potentially modify the list.

本质上,您并没有真正尝试“锁定”您的列表,而是尝试拥有一个通用实例,该实例可用作您何时想要修改列表、何时使用或“锁定”您想要的列表的参考点以防止执行可能会修改列表的其他代码。

回答by Scott Weinstein

The first way is wrong, as each caller will lock on a different object. You could just lock on the list.

第一种方法是错误的,因为每个调用者都会锁定不同的对象。你可以锁定列表。

lock(_myList)
{
   _myList.Add(...)
}

回答by Matt Davis

There are a few ways to lock the list. You can lock on _myList directly providing _myList is never changed to reference a new list.

有几种方法可以锁定列表。您可以直接锁定 _myList,前提是 _myList 永远不会更改为引用新列表。

lock (_myList)
{
    // do something with the list...
}

You can create a locking object specifically for this purpose.

您可以专门为此创建一个锁定对象。

private static object _syncLock = new object();
lock (_syncLock)
{
    // do something with the list...
}

If the static collection implements the System.Collections.ICollection interface (List(T) does), you can also synchronize using the SyncRoot property.

如果静态集合实现 System.Collections.ICollection 接口(List(T) 实现),您还可以使用 SyncRoot 属性进行同步。

lock (((ICollection)_myList).SyncRoot)
{
    // do something with the list...
}

The main point to understand is that you want oneand only oneobject to use as your locking sentinal, which is why creating the locking sentinal inside the DoSomething() function won't work. As Jon said, each thread that calls DoSomething() will get its own object, so the lock on that object will succeed every time and grant immediate access to the list. By making the locking object static (via the list itself, a dedicated locking object, or the ICollection.SyncRoot property), it becomes shared across all threads and can effectively serialize access to your list.

最主要的一点要明白的是,你想要一个唯一一个对象作为您锁定前哨,这就是为什么创建锁定前哨内DoSomething的()函数将无法正常工作使用。正如 Jon 所说,每个调用 DoSomething() 的线程都将获得自己的对象,因此对该对象的锁定每次都会成功并立即授予对列表的访问权限。通过将锁定对象设为静态(通过列表本身、专用锁定对象或 ICollection.SyncRoot 属性),它会在所有线程之间共享,并且可以有效地序列化对列表的访问。