2016-05-18 4 views
1

В моем коде у меня есть метод, который обрабатывает перевод денег с одной учетной записи на другую. Очевидно, что этот метод должен обеспечивать выполнение только одной транзакции для каждой учетной записи в любой момент выполнения программы. Я выполнил эту проверку с помощью 2 вложенных блокировок (Account является DB объект aith уникальный целочисленный идентификатор):Одновременное блокирование нескольких объектов

public void Transfer(Account from, Account to, decimal amount) 
{ 
    lock(GetLockObject(from)) 
    { 
     lock(GetLockObject(to)) 
     { 
      DoTransfer(from, to, amount); 
     } 
    } 
} 

private static Dictionary<int, object> lockObjects = new Dictionary<int, object>(); 
private static GetLockObject(Accont acc) 
{ 
    lock (lockObjects) 
    { 
     var lockObject = lockObjects[acc.Id]; 
     if (lockObject == null) 
     { 
      lockObject = new object(); 
      lockObjects[accId] = lockObject; 
     } 
     return lockObject; 
    } 
} 

Это, кажется, работает хорошо, но у меня есть 2 проблемы:

  • Код может вызвать тупик, если метод вызывается одновременно с теми же 2 учетными записями, что и его аргументы: Transfer(acc1, acc2, 10);Transfer(acc2, acc1, 10); Как мне избежать взаимоблокировки здесь?
  • Можно ли добиться того же результата лучше (с точки зрения производительности, надежности и т. Д.), Чем использовать вложенные блокировки?

ответ

1

Что касается вашего первого пункта: ознакомьтесь с порядком размещения учетных записей (например, с помощью идентификатора), затем заблокируйте один с меньшим идентификатором, второй с большим идентификатором.

Что касается вашего второго вопроса, есть варианты. Например, если вы уверены, что вам нужно заблокировать учетную запись to, и если вы можете переносить небольшое окно несогласованности, когда balance(from) + balance(to) меньше, чем когда был введен метод Transfer (меньше на amount), то вы можете сделать это:

public void Transfer(Account from, Account to, decimal amount) { 
    bool withdrawalSuccessful = false; 
    lock(GetLockObject(from)) { 
     withdrawalSuccessful = Withdraw(from, amount); 
    } 
    if (withdrawalSuccessful) { 
     lock(GetLockObject(to)) { 
      Deposit(to, amount) 
     } 
    } 
} 

Или вы можете полностью избавиться от блокировки «до»: вместо представления остатка на счете в виде одного номера храните коллекцию «депозитов» и «снятий» (будь то простой список или записи в таблице БД или что-то еще). Затем вы можете просто добавить депозиты в «до» без блокировки.

Или сделайте это не «депозиты» и «снятия средств», а просто некоторые суммы денег вместе с информацией владельца и некоторым идентификатором. Затем, чтобы сделать передачу, вы:

  1. замок «от»
  2. проверки, что сумма денег из кортежей, принадлежащих к from является >= amount
  3. разделить некоторые из его кортежей, скажем (id0, M, from) где M >= amount (если нет такой кортеж затем объединить некоторые из существующих кортежей первых) в [(id1, amount, from), (id2, M-amount, from)], а затем атомарно изменить (id1, amount, from) -> (id1, amount, to) ("UPDATE money_tuples SET owner='<to>' WHERE id='<id1>'" или просто "tuple.owner = to;"),
  4. наконец разблокировать «от».

Отличие от предыдущего варианта заключается в том, что вы используете («атомный») «ОБНОВЛЕНИЕ» или присваивание переменной вместо добавления двух объектов для разделения коллекций.