Потоки одного процесса разделяют единое адресное пространство, что упрощает взаимодействие подзадач (потоков), но требует обеспечения согласованности доступа к общим структурам данных.
Проблема гонки данных возникает при следующих условиях:
- Несколько потоков работают с разделяемым ресурсом.
- Конечный результат зависит от очередности выполнения командных последовательностей разных потоков.
// Код потока №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();
}
В этом примере третий поток в цикле «ожидает» завершения хотя бы одного потока. Проблемы гонки данных не возникает, несмотря на работу трех потоков с общей переменной. Порядок выполнения потоков не влияет на конечный результат. Изменения, вносимые потоками, не противоречат друг другу.