не хватает одного параметра: static void Main(string[] args) |
Потоки. Гонка данных и другие проблемы
Критические секции. Блокировка. Оператор lock
Критической секцией кода многопоточного приложения будем называть тот фрагмент кода, в котором ведется работа с общим ресурсом потоков. Чтобы решить проблему "гонки данных", достаточно, чтобы в критической секции кода одновременно мог работать только один поток, другие потоки должны стоять в очереди, ожидая, когда поток, выполняющий критическую секцию, закончит свою работу. После чего начать выполнять критическую секцию сможет другой поток. Такая схема работы называется "блокировкой" потоков. Пока один поток работает в критической секции, работа всех других потоков, претендующих на общий ресурс, блокируется. Существуют различные механизмы блокировки.
Рассмотрим простейший механизм блокировки, основанный на использовании оператора языка С# - оператора lock. Пусть в нашем приложении существует несколько критических секций, использующих один и тот же ресурс. Разные потоки могут входить в разные секции. Тем не менее, необходимо все их блокировать за исключением той, где работает активный поток, уже успевший захватить ресурс. Решение проблемы состоит в том, что создается некоторый объект, видимый во всех критических секциях. Обычно это объект универсального типа object с именем, например, locker. Затем каждая критическая секция закрывается оператором lock с ключом locker. Синтаксически конструкция блокировки выглядит так:
lock (locker) { < Критическая секция> }
Семантика конструкции такова. Будем полагать, что объект locker может находиться в двух состояниях - "открыт" или "закрыт". В исходном состоянии объект открыт. Когда некоторый поток достигает критической секции с ключевым объектом locker, то, если объект открыт, поток начинает выполнять критическую секцию, предварительно переключая объект locker в состояние "закрыт". При завершении критической секции объект locker переводится в состояние "открыт". Когда поток достигает критической секции с закрытым объектом locker, то он становится в очередь на выполнение критической секции. Как только locker открывается, первый по очереди поток входит в критическую секцию, блокируя ресурс для других потоков из очереди.
Такой способ блокировки позволяет справиться с проблемой "гонки данных" и написать потокобезопасное приложение для работы с банковскими счетами.
Безопасная работа с банковскими счетами
Прочитав вышеприведенный текст, программисты Последовательнов и Параллельнов поняли свою ошибку и достаточно быстро внесли в свой проект необходимые изменения. Прежде чем ознакомиться с их решением, попробуйте ответить на вопрос, как следует модифицировать существующее решение:
- Внести изменения в класс Account;
- Создать новый класс Account_new;
- Изменить логику работы клиентов с банковским счетом;
- Иной вариант решения проблемы.
Первый вариант нарушает один из основных принципов ООП - никогда не вносите изменения в уже работающий класс при появлении новых потребностей. Класс Account хорошо справляется со своими обязанностями при последовательном программировании, когда нет потоков и параллельных вычислений.
Создавать новый класс нецелесообразно, поскольку в существующем классе много полезного и не хочется повторять уже выполненную работу. Это опять-таки нарушает принципы ООП.
Логику работы клиентов менять не следует. Гонка данных возникает в критических секциях, связанных с методами класса Account.
Правильным решением проблемы является создание класса Safe_Account, наследующего от класса Account. В нашей задаче общим ресурсом является объект account, представляющий банковский счет, с которым работают несколько клиентов. Критические секции возникают во всех методах класса, которые изменяют значения полей объекта. Эти методы и требуют блокировки и соответственно переопределения в классе потомке. Вот как выглядит класс Safe_Account:
class Safe_Account: Account { static object Locker = new object(); public Safe_Account(double Init): base (Init) { } /// <summary> /// Положить на счет /// </summary> /// <param name="s"> добавляемая сумма</param> public override void Add(double s) { lock (Locker) { if (s > 0) { sum += s; error = false; message = " Операция начисления прошла успешно"; positive += s; } else { error = true; message = "При пополнении сумма должна быть положительной"; } } } /// <summary> /// Снять со счета /// </summary> /// <param name="s"> снимаемая сумма</param> public override void Sub(double s) { lock (Locker) { if (s < 0) { error = true; message = "При снятии сумма должна быть положительной"; } else if (sum >= s) { sum -= s; error = false; message = " Операция снятия прошла успешно"; negative += s; } else { error = true; message = "На счете нет запрашиваемой суммы"; } } } }
В классе вводится статический объект Locker, который используется для блокировки критических секций, представленных методами Add и Sub - пополнения и снятия денег со счета.
При работе с клиентами теперь необходимо использовать объект класса Safe_Account:
static void Test_Safe() { account = new Safe_Account(Init); Go(); }
Теперь работа с семейным счетом выполняется корректно. Вот как выглядят результаты сеанса работы после обновления:
Как видите, теперь все хорошо. Гонка данных устранена. Денег снято ровно столько, сколько положено.