带有 get/set 的 C# 线程安全

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/505515/
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-04 06:01:42  来源:igfitidea点击:

C# thread safety with get/set

c#lockingpropertiesthread-safety

提问by mmr

This is a detail question for C#.

这是 C# 的详细问题。

Suppose I've got a class with an object, and that object is protected by a lock:

假设我有一个带有对象的类,并且该对象受锁保护:

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         return property;
    }
    set { 
         property = value; 
    }
}

I want a polling thread to be able to query that property. I also want the thread to update properties of that object occasionally, and sometimes the user can update that property, and the user wants to be able to see that property.

我希望轮询线程能够查询该属性。我还希望线程偶尔更新该对象的属性,有时用户可以更新该属性,而用户希望能够看到该属性。

Will the following code properly lock the data?

以下代码会正确锁定数据吗?

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         lock (mLock){
             return property;
         }
    }
    set { 
         lock (mLock){
              property = value; 
         }
    }
}

By 'properly', what I mean is, if I want to call

通过“适当”,我的意思是,如果我想打电话

MyProperty.Field1 = 2;

or whatever, will the field be locked while I do the update? Is the setting that's done by the equals operator inside the scope of the 'get' function, or will the 'get' function (and hence the lock) finish first, and then the setting, and then 'set' gets called, thus bypassing the lock?

或者其他什么,在我进行更新时该字段会被锁定吗?是由“get”函数范围内的等号运算符完成的设置,还是“get”函数(以及锁定)首先完成,然后设置,然后调用“set”,从而绕过锁?

Edit: Since this apparently won't do the trick, what will? Do I need to do something like:

编辑:既然这显然不会奏效,那会怎样?我是否需要执行以下操作:

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         MyObject tmp = null;
         lock (mLock){
             tmp = property.Clone();
         }
         return tmp;
    }
    set { 
         lock (mLock){
              property = value; 
         }
    }
}

which more or less just makes sure that I only have access to a copy, meaning that if I were to have two threads call a 'get' at the same time, they would each start with the same value of Field1 (right?). Is there a way to do read and write locking on a property that makes sense? Or should I just constrain myself to locking on sections of functions rather than the data itself?

这或多或少只是确保我只能访问一个副本,这意味着如果我要让两个线程同时调用“get”,它们都会以相同的 Field1 值开始(对吗?)。有没有办法对有意义的属性进行读写锁定?或者我应该限制自己锁定功能部分而不是数据本身?

Just so that this example makes sense: MyObject is a device driver that returns status asynchronously. I send it commands via a serial port, and then the device responds to those commands in its own sweet time. Right now, I have a thread that polls it for its status ("Are you still there? Can you accept commands?"), a thread that waits for responses on the serial port ("Just got status string 2, everything's all good"), and then the UI thread which takes in other commands ("User wants you to do this thing.") and posts the responses from the driver ("I've just done the thing, now update the UI with that"). That's why I want to lock on the object itself, rather than the fields of the object; that would be a huge number of locks, a, and b, not every device of this class has the same behavior, just general behavior, so I'd have to code lots of individual dialogs if I individualized the locks.

只是为了让这个例子有意义:MyObject 是一个异步返回状态的设备驱动程序。我通过串行端口向它发送命令,然后设备在它自己的最佳时间响应这些命令。现在,我有一个线程轮询它的状态(“你还在吗?你能接受命令吗?”),一个等待串行端口响应的线程(“刚刚得到状态字符串 2,一切都很好” ),然后是接收其他命令的 UI 线程(“用户希望你做这件事。”)并发布来自驱动程序的响应(“我刚刚完成了这件事,现在用它更新 UI”)。这就是为什么我要锁定对象本身,而不是对象的字段;那将是大量的锁,a 和 b,并不是此类的每个设备都具有相同的行为,

采纳答案by LukeH

No, your code won't lock access to the members of the object returned from MyProperty. It only locks MyPropertyitself.

不,您的代码不会锁定对从MyProperty. 它只会锁定MyProperty自己。

Your example usage is really two operations rolled into one, roughly equivalent to this:

您的示例用法实际上是将两个操作合二为一,大致相当于:

// object is locked and then immediately released in the MyProperty getter
MyObject o = MyProperty;

// this assignment isn't covered by a lock
o.Field1 = 2;

// the MyProperty setter is never even called in this example

In a nutshell - if two threads access MyPropertysimultaneously, the getter will briefly block the second thread until it returns the object to the first thread, butit'll then return the object to the second thread as well. Both threads will then have full, unlocked access to the object.

简而言之——如果两个线程MyProperty同时访问,getter 将暂时阻塞第二个线程,直到将对象返回给第一个线程,它随后也会将对象返回给第二个线程。然后,两个线程都将拥有对该对象的完全、未锁定的访问权限。

EDIT in response to further details in the question

编辑以回应问题中的更多细节

I'm still not 100% certain what you're trying to achieve, but if you just want atomic access to the object then couldn't you have the calling code lock against the object itself?

我仍然不能 100% 确定您要实现的目标,但是如果您只想对对象进行原子访问,那么您不能对对象本身进行调用代码锁定吗?

// quick and dirty example
// there's almost certainly a better/cleaner way to do this
lock (MyProperty)
{
    // other threads can't lock the object while you're in here
    MyProperty.Field1 = 2;
    // do more stuff if you like, the object is all yours
}
// now the object is up-for-grabs again

Not ideal, but so long as all access to the object is contained in lock (MyProperty)sections then this approach will be thread-safe.

不理想,但只要对对象的所有访问都包含在lock (MyProperty)部分中,那么这种方法就是线程安全的。

回答by SoapBox

In the code example you posted, a get is never preformed.

在您发布的代码示例中,从未执行过 get。

In a more complicated example:

在一个更复杂的例子中:

MyProperty.Field1 = MyProperty.doSomething() + 2;

And of course assuming you did a:

当然,假设你做了一个:

lock (mLock) 
{
    // stuff...
}

In doSomething()then all of the lock calls would notbe sufficient to guarantee synchronization over the entire object. As soon as the doSomething()function returns, the lock is lost, then the addition is done, and then the assignment happens, which locks again.

doSomething()那么所有的锁通话会不会足以在整个对象的保证同步。一旦doSomething()函数返回,锁定丢失,则除了完成,然后分配情况,这再次锁定。

Or, to write it another way you can pretend like the locks are not done amutomatically, and rewrite this more like "machine code" with one operation per line, and it becomes obvious:

或者,以另一种方式编写它,您可以假装锁不是自动完成的,并将其重写为每行一个操作的“机器代码”,这变得很明显:

lock (mLock) 
{
    val = doSomething()
}
val = val + 2
lock (mLock)
{
    MyProperty.Field1 = val
}

回答by OJ.

The beauty of multithreading is that you don't know which order things will happen in. If you set something on one thread, it might happen first, it might happen after the get.

多线程的美妙之处在于你不知道事情会以什么顺序发生。如果你在一个线程上设置一些东西,它可能先发生,也可能在 get 之后发生。

The code you've posted with lock the member while it's being read and written. If you want to handle the case where the value is updated, perhaps you should look into other forms of synchronisation, such as events. (Check out the auto/manual versions). Then you can tell your "polling" thread that the value has changed and it's ready to be reread.

您发布的代码会在读取和写入成员时锁定该成员。如果您想处理值更新的情况,也许您应该研究其他形式的同步,例如events。(查看自动/手动版本)。然后您可以告诉您的“轮询”线程该值已更改并且已准备好重新读取。

回答by headsling

The lock scope in your example is in the incorrect place - it needs to be at the scope of the 'MyObject' class's property rather than it's container.

您的示例中的锁定范围位于不正确的位置 - 它需要在 'MyObject' 类的属性范围内,而不是它的容器。

If the MyObject my object class is simply used to contain data that one thread wants to write to, and another (the UI thread) to read from then you might not need a setter at all and construct it once.

如果 MyObject my object 类仅用于包含一个线程想要写入的数据,而另一个(UI 线程)要从中读取数据,那么您可能根本不需要 setter 并构造它一次。

Also consider if placing locks at the property level is the write level of lock granularity; if more than one property might be written to in order to represent the state of a transaction (eg: total orders and total weight) then it might be better to have the lock at the MyObject level (i.e. lock( myObject.SyncRoot ) ... )

还要考虑在property级别放锁是不是锁粒度的写级别;如果可能写入多个属性以表示交易状态(例如:总订单和总重量),那么最好在 MyObject 级别拥有锁(即 lock( myObject.SyncRoot ) .. .)

回答by jdigital

In your edited version, you are still not providing a threadsafe way to update MyObject. Any changes to the object's properties will need to be done inside a synchronized/locked block.

在您编辑的版本中,您仍然没有提供线程安全的方式来更新 MyObject。对对象属性的任何更改都需要在同步/锁定块内完成。

You can write individual setters to handle this, but you've indicated that this will be difficult because of the large number fields. If indeed the case (and you haven't provided enough information yet to assess this), one alternative is to write a setter that uses reflection; this would allow you to pass in a string representing the field name, and you could dynamically look up the field name and update the value. This would allow you to have a single setter that would work on any number of fields. This isn't as easy or as efficient but it would allow you to deal with a large number of classes and fields.

您可以编写单独的 setter 来处理此问题,但您已经指出,由于字段数量众多,这将很困难。如果确实如此(并且您还没有提供足够的信息来评估这一点),另一种选择是编写一个使用反射的 setter;这将允许您传入表示字段名称的字符串,并且您可以动态查找字段名称并更新值。这将允许您拥有一个可以处理任意数量字段的 setter。这并不那么容易或高效,但它可以让您处理大量的类和字段。

回答by Hans Passant

Concurrent programming would be pretty easy if your approach could work. But it doesn't, the iceberg that sinks that Titanic is, for example, the client of your class doing this:

如果您的方法可行,并发编程将非常容易。但事实并非如此,例如,沉没泰坦尼克号的冰山就是您班级的客户这样做:

objectRef.MyProperty += 1;

The read-modify-write race is pretty obvious, there are worse ones. There is absolutely nothing you can do to make your property thread-safe, other than making it immutable. It is your client that needs to deal with the headache. Being forced to delegate that kind of responsibility to a programmer that is least likely to get it right is the Achilles-heel of concurrent programming.

读-修改-写竞争非常明显,还有更糟糕的竞争。除了使其不可变之外,您绝对无法使您的属性成为线程安全的。您的客户需要处理头痛。被迫将这种责任委托给最不可能做到正确的程序员是并发编程的致命弱点。

回答by Matt Davis

As others have pointed out, once you return the object from the getter, you lose control over who accesses the object and when. To do what you're wanting to do, you'll need to put a lock inside the object itself.

正如其他人指出的那样,一旦您从 getter 返回对象,您就无法控制谁访问该对象以及何时访问该对象。为了做你想做的事,你需要在对象本身内放一个锁。

Perhaps I don't understand the full picture, but based on your description, it doesn't sound like you'd necessarily need to have a lock for each individual field. If you have a set of fields are simply read and written via the getters and setters, you could probably get away with a single lock for these fields. There is obviously potential that you'll unnecessarily serialize the operation of your threads this way. But again, based on your description, it doesn't sound like you're aggressively accessing the object either.

也许我不明白全貌,但根据您的描述,听起来您不一定需要为每个单独的字段锁定。如果您有一组字段可以通过 getter 和 setter 简单地读取和写入,那么您可能会对这些字段使用一个锁。显然,您可能会以这种方式不必要地序列化线程的操作。但同样,根据您的描述,听起来您也没有积极访问该对象。

I would also suggest using an event instead of using a thread to poll the device status. With the polling mechanism, you're going to be hitting the lock each time the thread queries the device. With the event mechanism, once the status changes, the object would notify any listeners. At that point, your 'polling' thread (which would no longer be polling) would wake up and get the new status. This will be much more efficient.

我还建议使用事件而不是使用线程来轮询设备状态。使用轮询机制,您将在每次线程查询设备时锁定。通过事件机制,一旦状态发生变化,对象就会通知任何侦听器。那时,您的“轮询”线程(将不再进行轮询)将唤醒并获得新状态。这样效率会高很多。

As an example...

举个例子...

public class Status
{
    private int _code;
    private DateTime _lastUpdate;
    private object _sync = new object(); // single lock for both fields

    public int Code
    {
        get { lock (_sync) { return _code; } }
        set
        {
            lock (_sync) {
                _code = value;
            }

            // Notify listeners
            EventHandler handler = Changed;
            if (handler != null) {
                handler(this, null);
            }
        }
    }

    public DateTime LastUpdate
    {
        get { lock (_sync) { return _lastUpdate; } }
        set { lock (_sync) { _lastUpdate = value; } }
    }

    public event EventHandler Changed;
}

Your 'polling' thread would look something like this.

您的“轮询”线程看起来像这样。

Status status = new Status();
ManualResetEvent changedEvent = new ManualResetEvent(false);
Thread thread = new Thread(
    delegate() {
        status.Changed += delegate { changedEvent.Set(); };
        while (true) {
            changedEvent.WaitOne(Timeout.Infinite);
            int code = status.Code;
            DateTime lastUpdate = status.LastUpdate;
            changedEvent.Reset();
        }
    }
);
thread.Start();

回答by Ricky Helgesson

You have implemented a lock for getting/setting the object but you have not made the object thread safe, which is another story.

您已经实现了用于获取/设置对象的锁,但您没有使对象线程安全,这是另一回事。

I have written an article on immutable model classes in C# that might be interesting in this context: http://rickyhelgesson.wordpress.com/2012/07/17/mutable-or-immutable-in-a-parallel-world/

我写了一篇关于 C# 中不可变模型类的文章,在这种情况下可能会很有趣:http: //rickyhelgesson.wordpress.com/2012/07/17/mutable-or-immutable-in-a-parallel-world/

回答by IzzyCoding

Does C# locks not suffer from the same locking issues as other languages then?

那么,C# 锁不会像其他语言一样遭受相同的锁定问题吗?

E.G.

例如

var someObj = -1;

// Thread 1

if (someObj = -1)
    lock(someObj)
        someObj = 42;

// Thread 2

if (someObj = -1)
    lock(someObj)
        someObj = 24;

This could have the problem of both threads eventually getting their locks and changing the value. This could lead to some strange bugs. However you don't want to unnecessarily lock the object unless you need to. In this case you should consider the double checked locking.

这可能会导致两个线程最终都获得锁并更改值。这可能会导致一些奇怪的错误。但是,除非需要,否则您不想不必要地锁定对象。在这种情况下,您应该考虑双重检查锁定。

// Threads 1 & 2

if (someObj = -1)
    lock(someObj)
        if(someObj = -1)
            someObj = {newValue};

Just something to keep in mind.

只是要记住一些事情。