在Montor.Enter锁中使用复杂对象有任何副作用吗?

时间:2016-07-21 23:10:21

标签: c# multithreading locking

我看到锁定的大多数代码示例都使用这样的模式:

private static int _counter = 0;

private static readonly object _sync = new object();

public void DoWork()
{
    int counter;
    lock (_sync)
    {
        counter = _counter++;
    }
    // etc ...
}

我的猜测是,Montor.Enter使用某种类型的引用指向生活在内存中的对象,以构建一个由什么线程锁定的内部字典。但是,不确定这是否正确。

我想知道在Monitor.Enter参数中是否存在使用更复杂对象的任何后果。例如,如果多个线程试图广播到WebSocket,则需要

  1. 排队请求并让一个线程负责发送,或
  2. 使用锁定来防止多个线程发送到同一个套接字。
  3. 假设WebSocket对象本身用于锁定:

    public async Task SendMessage(WebSocket socket, ArraySegment<byte> data)
    {
        lock (socket)
        {
            if (socket.State == WebSocketState.Open)
            {
                await socket.SendAsync(
                    data,
                    WebSocketMessageType.Text,
                    true,
                    CancellationToken.None);
            }
        }
    }
    

    如果Monitor.Enter只是使用一个指向内存中底层对象的引用指针,理论上它不会产生副作用,因为它是一个大而复杂的对象,而不是一个小小的新对象()。

    有没有人有这方面的数据?

    编辑:在下面的一些答案之后,我提出了另一种模式,扩展了WebSocket示例。任何进一步的反馈将不胜感激。

    1. 底层对象周围的薄包装允许创建用于锁定的私有只读对象。
    2. 锁内的异步方法是同步的。
    3. 请注意,此模式并未考虑仅允许单个线程访问WebSocket连接(通过队列系统)的建议 - 我主要尝试通过我的理解具有特定示例的锁定模式。

      public class SocketWrapper
      {
          private readonly object _sync = new object();
      
          public WebSocket Socket { get; private set; }
      
          public SocketWrapper(WebSocket socket)
          {
              this.Socket = socket;
          }
      
          public async Task SendMessage(ArraySegment<byte> data)
          {
              await Task.Yield();
      
              lock (this._sync)
              {
                  var t = await this.Socket.SendAsync(
                      data,
                      WebSocketMessageType.Text,
                      true,
                      CancellationToken.None);
                  t.Wait();
              }
          }
      }
      

3 个答案:

答案 0 :(得分:3)

锁机制使用对象的标头来锁定,对象的复杂程度并不重要,因为标头是机制所使用的。但是锁定的经验法则很好。

  1. 大部分时间只应锁定只读参考
  2. 为了清晰起见,为您的锁创建一个新的私有object,因为有人可能会锁定自己see this answer以获取更多信息
  3. 除非您的方法在程序级别锁定,否则不要使您的锁静态
  4. 您可以在MSDN上阅读有关lock关键字和Monitor.Enter的更多信息:

答案 1 :(得分:2)

这很好。 .NET使用一些Object头来有效地创建和使用spinlock,如果失败,它会使用一个信号量池。

在任何一种情况下,它都基于.NET中所有对象所具有的底层Object头。包含对象的复杂程度和简单程度无关紧要。

答案 2 :(得分:1)

  

我的猜测是,Montor.Enter使用某种类型的引用指向生活在内存中的对象,以构建一个由什么线程锁定的内部字典。但是,不确定这是否正确。

正如其他人所说,实际上每个.NET引用类型都内置了Monitor。任何线程都没有实际的“字典”(或任何其他集合)。

  

我想知道在Monitor.Enter参数中是否存在使用更复杂对象的任何后果。

使用任何引用类型都可以。然而...

  

多个线程试图广播到WebSocket

在这种情况下,排队是首选。特别是await中不能存在lock。通过使用异步兼容锁可以进行一种隐式排队,但这是另一个故事。

此外,不建议锁定参数。如果此示例是同步的,则仍然不建议:

// NOT recommended
public void SendMessage(WebSocket socket, ArraySegment<byte> data)
{
  lock (socket)
  ...
}

多年来已经制定了一些锁定指南:

  • 锁应该是私密的。由于任何代码都可以锁定,因此只要锁定任何其他代码都可以访问的实例,就可以打开死锁的可能性。请注意,隐私在此规则中很重要,因此lock(this)通常被理解为“不推荐”,但原因不是因为您“不应该锁定{{ 1}}“,而是因为”this不是私有的,你应该只锁定私有实例“。
  • 持有锁定时不要调用任意代码。这包括引发事件或调用回调。再一次,这开启了僵局的可能性。
  • this(在“关键部分”)中的代码应尽可能短。
  • 为了代码的可读性和可维护性,通常最好有一个明确的“互斥”对象(即lock)。
  • 应该记录“互斥锁”,以确定它正在保护的其他对象。
  • 避免需要多次锁定的代码。如果这是不可避免的,请建立并记录锁定层次结构,以便始终以相同的顺序获取锁定。

这些规则自然导致常见的互斥代码:

_sync