|
не хватает одного параметра: 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();
}Теперь работа с семейным счетом выполняется корректно. Вот как выглядят результаты сеанса работы после обновления:
Как видите, теперь все хорошо. Гонка данных устранена. Денег снято ровно столько, сколько положено.
