59

这是 C# 的详细问题。

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

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

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

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

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

'适当地',我的意思是,如果我想打电话

MyProperty.Field1 = 2;

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

编辑:既然这显然不会奏效,那会怎样?我是否需要做类似的事情:

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

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

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

4

9 回答 9

45

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

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

// 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

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

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

我仍然不能 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

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

于 2009-02-03T00:30:21.283 回答
17

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

objectRef.MyProperty += 1;

读取-修改-写入竞赛非常明显,还有更糟糕的竞赛。除了使其不可变之外,您绝对无法做任何事情来使您的属性成为线程安全的。需要解决头痛问题的是您的客户。被迫将这种责任委派给最不可能做对的程序员是并发编程的致命弱点。

于 2009-02-03T01:36:29.713 回答
5

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

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

我还建议使用事件而不是使用线程来轮询设备状态。使用轮询机制,您将在每次线程查询设备时锁定。使用事件机制,一旦状态发生变化,对象会通知任何监听者。此时,您的“轮询”线程(将不再轮询)将唤醒并获得新状态。这将更有效率。

举个例子...

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

您的“投票”线程看起来像这样。

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();
于 2009-02-03T02:05:53.697 回答
2

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

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

还要考虑在属性级别加锁是否是锁粒度的写级别;如果可能会写入多个属性以表示事务的状态(例如:总订单和总重量),那么最好在 MyObject 级别使用锁(即 lock( myObject.SyncRoot ) .. . )

于 2009-02-03T00:46:45.460 回答
1

在您发布的代码示例中,永远不会执行 get 。

在一个更复杂的例子中:

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

当然,假设你做了一个:

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

那时doSomething()所有的锁调用都不足以保证整个对象的同步。一旦doSomething()函数返回,锁就丢失了,然后加法完成,然后分配发生,再次锁定。

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

lock (mLock) 
{
    val = doSomething()
}
val = val + 2
lock (mLock)
{
    MyProperty.Field1 = val
}
于 2009-02-03T00:07:30.940 回答
1

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

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

于 2009-02-03T00:24:04.383 回答
0

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

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

于 2009-02-03T01:23:52.533 回答
0

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

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

于 2012-10-15T08:51:19.003 回答
-2

C# 锁是否不会遇到与其他语言相同的锁定问题?

例如

var someObj = -1;

// Thread 1

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

// Thread 2

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

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

// Threads 1 & 2

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

只是要记住的事情。

于 2016-02-19T18:59:15.520 回答