Опубликован: 23.04.2013 | Доступ: свободный | Студентов: 854 / 184 | Длительность: 12:54:00
Лекция 6:

Потоки. Гонка данных и другие проблемы

< Лекция 5 || Лекция 6: 12345 || Лекция 7 >

Критические секции. Блокировка. Оператор 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();
        }

Теперь работа с семейным счетом выполняется корректно. Вот как выглядят результаты сеанса работы после обновления:

Безопасная работа с семейным счетом

Рис. 5.3. Безопасная работа с семейным счетом

Как видите, теперь все хорошо. Гонка данных устранена. Денег снято ровно столько, сколько положено.

< Лекция 5 || Лекция 6: 12345 || Лекция 7 >
Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?