12 Возможности библиотеки Task Parallel Library. Проблема гонки данных

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

Проблема гонки данных возникает при следующих условиях:
  1. Несколько потоков работают с разделяемым ресурсом.
  2. Конечный результат зависит от очередности выполнения командных последовательностей разных потоков.

Для иллюстрации проблемы гонки данных рассмотрим следующий фрагмент. Два потока выводят на экран значение общей переменной Msg.

// Код потока №1
(1) Msg = “I’m thread one”;
(2) Console.WriteLine(“Thread #1: “ + Msg);


// Код потока №2

(3) Msg = “I’m thread two”;
(4) Console.WriteLine(“Thread #2: “ + Msg);

Переменная Msg является общей – изменение переменной в одном потоке будут видны в другом потоке. При параллельной работе потоков вывод определяется конкретной последовательностью выполнения операторов.

Если операторы первого потока выполняются до операторов второго потока, т.е. при последовательности (1) – (2) – (3) – (4), то мы получаем:

Thread #1: I’m thread one 
Thread #2: I’m thread two

Если же в выполнение операторов одного потока вмешаются операторы другого потока, например, при последовательности (1) – (3) – (2) – (4), то получим следующее:

Thread #1: I’m thread two 
Thread #2: I’m thread two

Проблема гонки данных возникает не только при выполнении нескольких операторов, но и при выполнении одного оператора. Рассмотрим следующий случай. Оба потока выполняют один оператор над общей переменной x типа int:

x = x + 5

Данный оператор предполагает выполнение следующих действий:
  • загрузить значение операндов в регистры процессора
  • осуществить суммирование
  • записать результат по адресу переменной x

При выполнении командных последовательностей одного потока на одном исполнительном устройстве в работу может вмешаться другой поток. Конечный результат зависит от очередности выполнения командных последовательностей. Если потоки осуществляют суммирование последовательно, то получаем конечный результат: 10. Если потоки осуществляют суммирование одновременно, то раннее изменение будет затерто поздним:


Еще одной «ловушкой» в многопоточной обработке является работа с динамическими структурами данных (списки, словари). Добавление и удаление элементов в динамические структуры данных осуществляется с помощью одного метода:

list.Add(“New element”);
dic.RemoveKey(“keyOne”);

Реализация методов включает несколько действий. Например, при добавлении элемента в список необходимо записать элемент по текущему индексу (указателю) и сдвинуть индекс на следующую позицию. Для корректного осуществления добавления элемента одним потоком необходимо, чтобы другие потоки дождались завершения манипуляций со списком.

// Добавление элемента в массив по текущему индексу
data[current_index] = new_value;
current_index++;

Для решения проблемы гонки данных необходимо обеспечить взаимно исключительный доступ к тем командным последовательностям, в которых осуществляется работа с разделяемым ресурсом. Взаимная исключительность означает, что в каждый момент времени с ресурсом работает только один поток, другие потоки блокируются в ожидании завершения работы первого потока. Фрагмент кода, к которому должен быть обеспечен взаимно исключительный доступ, называется критической секцией.

Проблема гонки данных не всегда возникает при работе с общей переменной. Например, в следующем фрагменте два потока перед завершением осуществляют изменение разделяемой переменной, а третий поток читает это значение.

// Общая переменная
bool b = false;
//Поток №1
void f1()
{
DoSomeWork1();
b = true;
}
//Поток №2
void f2()
{
DoSomeWork2();
b = true;
}
//Поток №3
void f3()
{
while(!b) ;
DoSomeWork3();
}

В этом примере третий поток в цикле «ожидает» завершения хотя бы одного потока. Проблемы гонки данных не возникает, несмотря на работу трех потоков с общей переменной. Порядок выполнения потоков не влияет на конечный результат. Изменения, вносимые потоками, не противоречат друг другу.