【问题标题】:Lock and Mutex are showing different resultsLock 和 Mutex 显示不同的结果
【发布时间】:2020-12-30 18:32:42
【问题描述】:

我正在尝试一些与 C# 线程中的锁和互斥锁相关的概念。但是,如果发现使用 Mutex 给了我正确的结果,而使用 lock 则不一致。

使用lock 构造:

class BankAccount
{
  private int balance;
  public object padlock = new object();
  public int Balance { get => balance; private set => balance = value; }

  public void Deposit(int amount)
  {
    lock ( padlock )
    {
      balance += amount;
    }
  }

  public void Withdraw(int amount)
  {
    lock ( padlock )
    {
      balance -= amount;
    }
  }

  public void Transfer(BankAccount where, int amount)
  {
    lock ( padlock )
    {
      balance = balance - amount;
      where.Balance = where.Balance + amount;
    }
  }
}

static void Main(string[] args)
{
  var ba1 = new BankAccount();
  var ba2 = new BankAccount();

  var task = Task.Factory.StartNew(() =>
  {
    for ( int j = 0; j < 1000; ++j )
      ba1.Deposit(100);
  });

  var task1 = Task.Factory.StartNew(() =>
  {
    for ( int j = 0; j < 1000; ++j )
      ba2.Deposit(100);
  });

  var task2 = Task.Factory.StartNew(() =>
  {
    for ( int j = 0; j < 1000; ++j )
      ba1.Transfer(ba2, 100);
  });

  Task.WaitAll(task, task1, task2);
  Console.WriteLine($"Final balance is {ba1.Balance}.");
  Console.WriteLine($"Final balance is {ba2.Balance}.");
  Console.ReadLine();
}

代码为 ba2 提供了不正确的余额,而 ba1 设置为 0。 即使每个操作都被lock 语句包围,情况也是如此。它不能正常工作。

使用Mutex 构造:

class BankAccount
{
  private int balance;

  public int Balance { get => balance; private set => balance = value; }

  public void Deposit(int amount)
  {
    balance += amount;
  }

  public void Withdraw(int amount)
  {
    balance -= amount;
  }

  public void Transfer(BankAccount where, int amount)
  {
    balance = balance - amount;
    where.Balance = where.Balance + amount;
  }
}

static void Main(string[] args)
{
  var ba1 = new BankAccount();
  var ba2 = new BankAccount();

  var mutex1 = new Mutex();
  var mutex2 = new Mutex();

  var task = Task.Factory.StartNew(() =>
  {
    for ( int j = 0; j < 1000; ++j )
    {
      var lockTaken = mutex1.WaitOne();

      try
      {
        ba1.Deposit(100);
      }
      finally
      {
        if ( lockTaken )
        {
          mutex1.ReleaseMutex();
        }
      }
    }
  });

  var task1 = Task.Factory.StartNew(() =>
  {
    for ( int j = 0; j < 1000; ++j )
    {
      var lockTaken = mutex2.WaitOne();

      try
      {
        ba2.Deposit(100);
      }
      finally
      {
        if ( lockTaken )
        {
          mutex2.ReleaseMutex();
        }
      }
    }
  });

  var task2 = Task.Factory.StartNew(() =>
  {
    for ( int j = 0; j < 1000; ++j )
    {
      bool haveLock = Mutex.WaitAll(new[] { mutex1, mutex2 });
      try
      {
        ba1.Transfer(ba2, 100);
      }
      finally
      {
        if ( haveLock )
        {
          mutex1.ReleaseMutex();
          mutex2.ReleaseMutex();
        }
      }
    }
  });

  Task.WaitAll(task, task1, task2);
  Console.WriteLine($"Final balance is {ba1.Balance}.");
  Console.WriteLine($"Final balance is {ba2.Balance}.");
  Console.ReadLine();
}

通过这种方法,我每次运行时都能获得正确的余额。

我无法弄清楚为什么第一种方法不能正常工作。我是否遗漏了与 lock 语句相关的内容?

【问题讨论】:

  • 基本上,除非我误读了某些内容,否则第一种方法不是线程安全的。第一种方法为Transfer 锁定ba1,而第二种方法同时锁定ba1ba2,有效地禁止在处理ba1.Transfer 时对ba2 进行更改
  • @CamiloTerevinto 第一种方法为Transfer 锁定ba1 并为Transfer 锁定ba2。这些操作作为单独的任务运行。无法理解您为什么只说 ba1 被锁定为 Transfer
  • 当您在第一种方法中调用ba1.Transfer(ba2, 100); 时,锁定是针对ba1.padlocknot 针对ba2.padlock(并且这个仍然未锁定)跨度>
  • @CamiloTerevinto 你的cmets对我来说很清楚。谢谢
  • 我可能会误解一些东西,所以我会等到有更多锁具经验的人回答:)

标签: c# multithreading locking task mutex


【解决方案1】:

主要问题在于这一行:

public int Balance { get => balance; private set => balance = value; }

您允许外部代码干预balance 字段,而不受padlock 的保护。您还允许乱序读取balance 字段,因为缺少memory barrier,或者更糟糕的是torn reads,以防您稍后将int 类型替换为更合适的decimal

第二个问题可以通过使用padlock保护读取来解决。

public int Balance { get => { lock (padlock) return balance; } }

至于Transfer方法,现在可以在不访问其他BankAccountsbalance的情况下实现,像这样:

public void Transfer(BankAccount where, int amount)
{
    Withdraw(amount);
    where.Deposit(amount);
}

这个Transfer 实现不是原子的,因为where.Deposit 方法中的异常可能导致amount 消失得无影无踪。也不会阻止其他线程读取两个BankAccounts Balances 的不一致值。这就是为什么人们通常使用配备ACID 属性的数据库来完成此类工作的原因。

【讨论】:

    【解决方案2】:

    这两个代码在我的机器上给出了相同的结果 VS2017 .NET Framework 4.7.2 并且工作正常。因此,您的系统可能有所不同。

    Final balance is 0.
    Final balance is 200000.
    

    Mutex 历史上和最初用于进程间同步。

    因此,在创建互斥锁的过程中,它永远不会自我锁定,除非它像问题中提供的代码那样被释放。

    使用操作系统互斥对象来同步线程是一种不好的做法,也是一种反模式。

    如果lockvolatile 有问题,请在进程中使用SemaphoreMonitor

    Mutex :“一种同步原语,也可用于进程间同步。”

    信号量:“限制可以同时访问资源或资源池的线程数。”

    监视器:“提供一种同步访问对象的机制。”

    lock : "lock 语句获取给定对象的互斥锁,执行语句块,然后释放锁。当持有锁时,持有锁的线程可以再次获取并释放锁。任何其他线程都被阻止获取锁并等待直到锁被释放。"

    volatile : "volatile 关键字表示一个字段可能被同时执行的多个线程修改。编译器、运行时系统甚至硬件可能会重新安排对内存位置的读取和写入以提高性能原因。声明为 volatile 的字段不受这些优化的影响。添加 volatile 修饰符可确保所有线程将按照执行顺序观察任何其他线程执行的 volatile 写入。不能保证单个总排序从所有执行线程中看到的 volatile 写入。”

    因此您可以尝试添加volatile:

    private volatile int balance;
    

    如果需要,您还可以将储物柜对象设置为静态以在实例之间共享:

    static private object padlock = new object();
    

    【讨论】:

    • 此答案与此答案中链接的文档背道而驰。如果是这种情况,而事实并非如此,则第二种方法将给出完全无效的答案。请注意,第一句是“也可以用于进程间同步的同步原语。” (强调我的)
    • @OlivierRogier 是的。即使在第一种方法中,将padlock 设为静态也能提供一致的输出。谢谢
    • @SwapnilGhodke 是的,通过拥有一个 padlock 实例,您可以确保 lock 也锁定其他线程
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-28
    • 1970-01-01
    • 2013-01-21
    相关资源
    最近更新 更多