Нередко в потоках используются некоторые разделяемые ресурсы, общие для всей программы. Это могут быть общие переменные, файлы, другие ресурсы. Например:
int x = 0; // запускаем пять потоков for (int i = 1; i < 6; i++) { Thread myThread = new(Print); myThread.Name = $"Поток {i}"; // устанавливаем имя для каждого потока myThread.Start(); } void Print() { x = 1; for (int i = 1; i < 6; i++) { Console.WriteLine($"{Thread.CurrentThread.Name}: {x}"); x++; Thread.Sleep(100); } }
Здесь у нас запускаются пять потоков, которые вызывают метод Print и которые работают с общей переменной x. И мы предполагаем, что метод выведет все значения x от 1 до 5. И так для каждого потока. Однако в реальности в процессе работы будет происходить переключение между потоками, и значение переменной x становится непредсказуемым. Например, в моем случае я получил следующий консольный вывод (он может в каждом конкретном случае различаться):
Поток 1: 1 Поток 5: 1 Поток 4: 1 Поток 2: 1 Поток 3: 1 Поток 1: 6 Поток 5: 7 Поток 3: 7 Поток 2: 7 Поток 4: 9 Поток 1: 11 Поток 4: 11 Поток 2: 11 Поток 3: 14 Поток 5: 11 Поток 1: 16 Поток 2: 16 Поток 3: 16 Поток 5: 18 Поток 4: 16 Поток 1: 21 Поток 5: 21 Поток 3: 21 Поток 2: 21 Поток 4: 21
Решение проблемы состоит в том, чтобы синхронизировать потоки и ограничить доступ к разделяемым ресурсам на время их использования каким-нибудь потоком. Для этого используется ключевое слово lock. Оператор lock определяет блок кода, внутри которого весь код блокируется и становится недоступным для других потоков до завершения работы текущего потока. Остальный потоки помещаются в очередь ожидания и ждут, пока текущий поток не освободит данный блок кода. В итоге с помощью lock мы можем переделать предыдущий пример следующим образом:
int x = 0; object locker = new(); // объект-заглушка // запускаем пять потоков for (int i = 1; i < 6; i++) { Thread myThread = new(Print); myThread.Name = $"Поток {i}"; myThread.Start(); } void Print() { lock (locker) { x = 1; for (int i = 1; i < 6; i++) { Console.WriteLine($"{Thread.CurrentThread.Name}: {x}"); x++; Thread.Sleep(100); } } }
Для блокировки с ключевым словом lock используется объект-заглушка, в данном случае это переменная locker
. Обычно это переменная типа object. И когда выполнение доходит
до оператора lock, объект locker блокируется, и на время его блокировки монопольный доступ к блоку кода имеет только один поток. После окончания работы блока кода,
объект locker освобождается и становится доступным для других потоков.
В этом случае консольный вывод будет более упорядоченным:
Поток 1: 1 Поток 1: 2 Поток 1: 3 Поток 1: 4 Поток 1: 5 Поток 5: 1 Поток 5: 2 Поток 5: 3 Поток 5: 4 Поток 5: 5 Поток 3: 1 Поток 3: 2 Поток 3: 3 Поток 3: 4 Поток 3: 5 Поток 2: 1 Поток 2: 2 Поток 2: 3 Поток 2: 4 Поток 2: 5 Поток 4: 1 Поток 4: 2 Поток 4: 3 Поток 4: 4 Поток 4: 5