关于c#:为什么lock(this)不好?

Why is lock(this) {…} bad?

MSDN Documentation says

1
2
3
4
5
6
7
8
9
10
public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

Is"a problem if the instance can be accessed publicly".我在想为什么?是因为锁会比需要长吗?然而,是否有更多的不明原因?


在lock语句中使用this是不好的形式,因为通常情况下,您无法控制还有谁可能锁定该对象。

为了正确地规划并行操作,应该特别注意考虑可能的死锁情况,并且具有未知数量的锁入口点会阻碍这一点。例如,任何引用对象的人都可以在对象设计器/创建者不知道的情况下锁定它。这会增加多线程解决方案的复杂性,并可能影响它们的正确性。

私有字段通常是一个更好的选项,因为编译器将对其强制执行访问限制,并且它将封装锁定机制。使用this违反了封装,将部分锁定实现公开。也不清楚您是否会获取this上的锁,除非它已被记录在案。即便如此,依靠文档来防止问题也是次优的。

最后,有一种常见的误解,即lock(this)实际上修改了作为参数传递的对象,并且在某种程度上使它成为只读的或不可访问的。这是错误的。作为参数传递给lock的对象仅用作键。如果该钥匙上已持有锁,则无法进行锁;否则,允许进行锁。

这就是为什么在lock语句中使用字符串作为键是不好的,因为它们是不可变的,并且在应用程序的各个部分之间是共享/可访问的。您应该使用一个私有变量来代替,一个Object实例可以做得很好。

以下面的C代码为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name ="Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name ="Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name ="Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name ="Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

控制台输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string"Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.


因为如果人们可以访问您的对象实例(即:您的this指针),那么他们也可以尝试锁定同一个对象。现在,他们可能不知道您在内部锁定了this,因此这可能会导致问题(可能是死锁)。

除此之外,这也是不好的做法,因为它锁定的"太多"

例如,您可能有一个List的成员变量,而实际需要锁定的唯一一件事就是该成员变量。如果您将整个对象锁定在函数中,那么其他调用这些函数的东西将在等待锁定时被阻塞。如果这些函数不需要访问成员列表,那么您将导致其他代码等待,毫无理由地减慢应用程序的速度。


查看msdn主题线程同步(C编程指南)

Generally, it is best to avoid locking
on a public type, or on object
instances beyond the control of your
application. For example, lock(this)
can be problematic if the instance can
be accessed publicly, because code
beyond your control may lock on the
object as well. This could create
deadlock situations where two or more
threads wait for the release of the
same object. Locking on a public
data type, as opposed to an object,
can cause problems for the same
reason. Locking on literal strings is
especially risky because literal
strings are interned by the common
language runtime (CLR). This means
that there is one instance of any
given string literal for the entire
program, the exact same object
represents the literal in all running
application domains, on all threads.
As a result, a lock placed on a string
with the same contents anywhere in the
application process locks all
instances of that string in the
application. As a result, it is best
to lock a private or protected member
that is not interned. Some classes
provide members specifically for
locking. The Array type, for example,
provides SyncRoot. Many collection
types provide a SyncRoot member as
well.


我知道这是一条古老的线索,但因为人们仍然可以仰视它并依赖它,所以指出lock(typeof(SomeObject))明显比lock(this)差是很重要的。说了这句话之后,真诚地向艾伦致敬,因为他指出江户十一〔二〕是不好的做法。

System.Type的一个实例是其中最通用的粗粒度对象之一。至少,System.Type的实例对AppDomain是全局的,并且.NET可以在AppDomain中运行多个程序。这意味着,如果两个完全不同的程序都试图在同一类型实例上获得同步锁,那么它们可能会相互干扰,甚至造成死锁。

因此,lock(this)并不是特别健壮的形式,它会引起问题,并且应该根据所引用的所有原因总是扬眉吐气。尽管我个人更愿意看到模式的改变,但是有广泛使用的、相对受尊重的、明显稳定的代码,比如log4net,它广泛使用锁(这个)模式。

但是lock(typeof(SomeObject))打开了一个全新的增强型蠕虫罐头。

为了它的价值。


…同样的参数也适用于这个结构:

1
lock(typeof(SomeObject))


想象一下,你的办公室里有一个技术熟练的秘书,这是这个部门的共同资源。有一次,你冲向他们,因为你有一个任务,只是希望你的另一个同事还没有认领他们。通常你只需要等待一小段时间。

因为关怀是共享的,所以您的经理决定客户也可以直接使用秘书。但这有一个副作用:当您为这个客户工作时,客户甚至可能会要求他们索赔,而且您还需要他们执行部分任务。死锁发生,因为声明不再是一个层次结构。这一点本来可以一起避免,因为首先不允许客户提出索赔。

如我们所见,lock(this)是坏的。一个外部对象可能会锁定该对象,由于您不控制谁在使用该类,任何人都可以锁定它…这就是上面描述的例子。同样,解决方法是限制物体的曝光。但是,如果您有一个privateprotectedinternal类,那么您就可以控制锁定对象的对象,因为您确信自己已经编写了代码。所以这里的信息是:不要把它暴露为public。另外,确保在类似场景中使用锁可以避免死锁。

与此完全相反的是锁定在整个应用程序域中共享的资源——最坏的情况。这就像把你的秘书放在外面,让外面的每个人都认领他们。结果是彻底的混乱——或者从源代码的角度来说:这是个坏主意;扔掉它重新开始。那我们该怎么做呢?

正如这里大多数人指出的,类型在应用程序域中是共享的。但我们可以使用更好的东西:字符串。原因是字符串被合并。换句话说:如果在一个应用程序域中有两个内容相同的字符串,那么它们有可能拥有完全相同的指针。因为指针被用作锁定键,所以您基本上得到的是"准备未定义的行为"的同义词。

同样,您不应该锁定wcf对象、httpcontext.current、thread.current、singletons(一般情况下)等。避免所有这些的最简单方法是什么?private [static] object myLock = new object();


如果锁定共享资源,则锁定此指针可能会很糟糕。共享资源可以是计算机上的静态变量或文件,也就是类中所有用户共享的资源。原因是每次实例化类时,此指针将包含对内存中某个位置的不同引用。因此,在类的一次实例中锁定它与在类的另一个实例中锁定它不同。

查看此代码以了解我的意思。在控制台应用程序的主程序中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

创建如下所示的新类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal : " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  : " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is : " + balance +" and you tried to withdraw" + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random rand = new Random();
        Withdraw(rand.Next(1, 100) * 100);
    }
}

这是一个锁定程序的运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

下面是对mylock的程序锁定运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000

微软的性能架构师Rico Mariani在http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects上写了一篇非常好的文章。NET运行时

Excerpt:

The basic problem here is that you don't own the type object, and you
don't know who else could access it. In general, it's a very bad idea
to rely on locking an object you didn't create and don't know who else
might be accessing. Doing so invites deadlock. The safest way is to
only lock private objects.


这里有一个更简单的例子(取自这里的问题34),当类的使用者也试图锁定对象时,为什么锁(这个)是坏的,并且可能导致死锁。下面,只有三个线程中的一个可以继续,另外两个是死锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

为了解决这个问题,这个人使用了thread.trymonitor(超时)而不是lock:

1
2
3
4
5
6
7
8
9
            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks


这里还有一些很好的讨论:这是互斥的正确使用吗?


请参考下面的链接,解释为什么锁(这个)不是一个好主意。

http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx

因此,解决方案是向类中添加一个私有对象,例如lock object,并将代码区域放在lock语句中,如下所示:

1
2
3
4
lock (lockObject)
{
...
}


下面是一些更简单的示例代码(IMO):(将在LinqPad中使用,引用以下命名空间:System.net和System.Threading.Tasks)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Main()
{
    ClassTest test = new ClassTest();
    lock(test)
    {
        Parallel.Invoke (
            () => test.DoWorkUsingThisLock(1),
            () => test.DoWorkUsingThisLock(2)
        );
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine("Before ClassTest.DoWorkUsingThisLock" + i);
        lock(this)
        {
            Console.WriteLine("ClassTest.DoWorkUsingThisLock" + i);
            Thread.Sleep(1000);
        }
        Console.WriteLine("ClassTest.DoWorkUsingThisLock Done" + i);
    }
}


因为任何可以看到类实例的代码块也可以锁定该引用。您希望隐藏(封装)您的锁定对象,以便只有需要引用它的代码才能引用它。关键字this引用当前类实例,因此任何数量的事物都可以引用它,并可以使用它来进行线程同步。

显然,这是不好的,因为其他一些代码块可以使用类实例来锁定,并且可能会阻止代码获得及时的锁定,或者可能会造成其他线程同步问题。最佳情况:没有其他方法使用对类的引用来锁定。中间的情况:某些东西使用对类的引用来进行锁定,这会导致性能问题。最坏的情况是:某些东西使用类的引用来进行锁定,这会导致非常糟糕、非常微妙、非常难以调试的问题。


抱歉,伙计们,我不同意这样的论点,锁定这个可能会导致死锁。你混淆了两件事:僵局和饥饿。

  • 如果不中断其中一个线程,就无法取消死锁,因此进入死锁后就无法退出。
  • 其中一个线程完成其工作后,饥饿将自动结束。

这是一张图片,说明了不同之处。

结论如果线程不足不是您的问题,您仍然可以安全地使用lock(this)。您还必须记住,当使用lock(this)的饥饿线程在锁定对象的锁中结束时,最终将以永久饥饿结束;)


您可以建立一个规则,说明类可以拥有锁定"this"的代码,或者类中的代码实例化的任何对象。所以,如果不遵循这个模式,这只是个问题。

如果您希望保护自己不受不遵循此模式的代码的影响,那么接受的答案是正确的。但是如果遵循这个模式,那就不是问题了。

锁的优点是效率。如果您有一个简单的"值对象",它保存一个值,该怎么办?它只是一个包装器,它被实例化数百万次。通过只为锁定而创建一个私有同步对象,您基本上已经将对象的大小增加了一倍,分配的数量也增加了一倍。当性能很重要时,这是一个优势。

当您不关心分配数量或内存占用时,出于其他答案中指出的原因,最好避免锁定(这)。


如果可以公开访问该实例,则会出现问题,因为可能有其他请求正在使用同一对象实例。最好使用私有/静态变量。