Решение 11 распространенных проблем в многопоточном коде - Неправильная детализация

ОГЛАВЛЕНИЕ

Неправильная детализация

Даже если доступ к общему состоянию выполняется с должной синхронизацией, получающееся поведение может всё же быть некорректным. Детализация должны быть достаточно велика, чтобы операции, которые должны представляться атомарными, включались в область. Существуют определенное противоречие между корректностью и уменьшением области, поскольку при уменьшении областей уменьшается время необходимого ожидания одновременного входа для остальных потоков.

Для примера взглянем на абстракцию банковского счета, показанную на рис. 1. Все вроде бы в порядке, и два метода объекта Deposit («Поместить») и Withdraw («Снять») кажутся корректными относительно одновременности. Некоторые банковские приложения могут использовать их, не волнуясь, что балансы станут неверными из-за одновременного доступа.

 Рис. 1. Банковский счет

class BankAccount {
  private decimal m_balance = 0.0M;
  private object m_balanceLock = new object();
  internal void Deposit(decimal delta) {
    lock (m_balanceLock) { m_balance += delta; }
  }
  internal void Withdraw(decimal delta) {
    lock (m_balanceLock) {
     if (m_balance < delta)
   throw new Exception("Insufficient funds");
     m_balance -= delta;
    }
  }
}

Но что если нам нужно добавить метод Transfer («Перевод»)? Наивный (и неверный) подход будет состоять в предположении, что, поскольку Deposit и Withdraw корректны в изоляции, их можно легко сочетать:

class BankAccount {
  internal static void Transfer(
   BankAccount a, BankAccount b, decimal delta) {
    Withdraw(a, delta);
    Deposit(b, delta);
  }
  // As before
}

Это неверно. На самом деле, между вызовами Withdraw и Deposit имеется период времени, в который деньги отсутствуют полностью.

Для правильной реализации потребуется получение блокировок как на a, так и на b заранее с последующим выполнением вызовов методов:

class BankAccount {
  internal static void Transfer(
   BankAccount a, BankAccount b, decimal delta) {
    lock (a.m_balanceLock) {
 lock (b.m_balanceLock) {
   Withdraw(a, delta);
   Deposit(b, delta);
 }
    }
  }
  // As before
}

Получается, что, хоть этот подход и решает проблему детализации, он подвержен взаимоблокировкам. Ниже будет показано, как исправить этот недостаток.