Что такое потокобезопасность C#

Что такое потокобезопасность? Как потоки могут влиять друг на друга таким образом, что они сломаются? Как обезопасить себя от подобных поломок?


Ответы (1 шт):

Автор решения: aepot

Вот пример.

static void Main(string[] args)
{
    int count = 0;
    var task1 = Task.Run(() => {
        for (int i = 0; i < 500; i++)
        {
            Thread.Sleep(1);
            count++;
        }
    });
    var task2 = Task.Run(() => {
        for (int i = 0; i < 500; i++)
        {
            Thread.Sleep(1);
            count++;
        }
    });
    Task.WaitAll(task1, task2);
    Console.WriteLine(count);
    Console.ReadKey();
}

Какой вывод в консоль ожидается? 1000, верно?

Запускаю

1000

Вроде все ок, ну-ка еще разок

998

Опа. Вот потоки и поругались, а в чем собственно дело.

Непредсказуемость результата выполнения многопоточного кода означает, что код не является потокобезопасным.

А в том что count++ это на самом деле count = count + 1, то есть процессор должен сначала прочитать значение переменной, затем прибавить к нему единицу, затем записать в эту переменную обратно. То есть не одно действие, а несколько.

Представьте, что действия двумя потоками выполняются во времени в следующем порядке.

  • Поток 1 читает значение 10 и прибавляет к нему 1, получает 11
  • Поток 2 читает значение 10 и прибавляет к нему 1, получает 11
  • Поток 1 записывает значение 11 в переменную
  • Поток 2 записывает значение 11 в переменную

В итоге единицу добавляли 2 раза, должно получиться 12, а получается 11.

Исправить просто - использовать потокобезопасный Increment.

static void Main(string[] args)
{
    int count = 0;
    var task1 = Task.Run(() => {
        for (int i = 0; i < 500; i++)
        {
            Thread.Sleep(1);
            Interlocked.Increment(ref count);
        }
    });
    var task2 = Task.Run(() => {
        for (int i = 0; i < 500; i++)
        {
            Thread.Sleep(1);
            Interlocked.Increment(ref count);
        }
    });
    Task.WaitAll(task1, task2);
    Console.WriteLine(count);
    Console.ReadKey();
}

Вывод

1000

И 1000 всегда.

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

→ Ссылка