Решение 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
}
Получается, что, хоть этот подход и решает проблему детализации, он подвержен взаимоблокировкам. Ниже будет показано, как исправить этот недостаток.