Полное руководство по языку программирования С# 14 и платформе .NET 10 / Многопоточность

Класс Lock и синхронизация

Начальный ⏱ 5 мин чтения 👁 2 🕓 обновлено 11.08.2026

Начиная с версии .NET 9 во фреймворк был добавлен новый API для синхронизации в виде класса System.Threading.Lock. Класс Lock служит для синхронизации доступа потоков к некоторой критической секции для предотвращения гонки за ресурсы между потоками. Есть разные варианты применения Lock. Рассмотрим их.

С оператором lock

Самый простой способ использования Lock- с оператором lock:


int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    lock(_lockObj)  // начало критической секции
    {
        x = 1;
        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}

Данный пример похож на обычную синхронизацию с помощью оператора lock, когда в качестве заглушки используется произвольный объект. Здесь мы сначала определяем некоторый общий ресурс, к которому надо синхронизировать доступ - переменную x, и объект Lock синхронизации. Для примера здесь запускаем 5 потоков, каждый из которых обращается к некоторой общей секции в методе Print, где происходит доступ и изменение переменной x. Когда один поток захватывает объект _lockObj и входит критическую секцию, остальные потоки ждут освобождения _lockObj. После выхода из критической секции поток освобождает объект _lockObj для других потоков. В итоге мы получим синхронизированный доступ потоков следующего рода:


Поток 1: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5

ПО консольному выводу мы видим, что у нас получился синхронизированный доступ к переменной x.

Метод Enter()

С помощью вызова метода Enter() у объекта Lock можно обозначить начало критической секции, в которую опять же может войти только один поток. Остальные потоки опять же ожидают выхода этого потока из критической секции. Для выхода из этой секции (ее завершения) у объекта Lock вызывается метод Exit():


int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    _lockObj.Enter();;  // начало критической секции
    try
    {
        x = 1;
        for (int i = 1; i < 6; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  
    finally { _lockObj.Exit(); } // завершение критической секции
}

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

TryEnter()

Метод TryEnter() также осуществляет вход в критическую секцию. Этот метод возвращает булевое значение - true или false. Если метод возвращает true, текущий поток является единственным потоком, который удерживает блокировку. Если метод возвращает false, то поток не ждет освобождения блокировки. Например:


int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}


void Print()
{
    if (_lockObj.TryEnter())  // начало критической секции
    {
        try
        {
            x = 1;
            for (int i = 1; i < 6; i++)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
                x++;
                Thread.Sleep(100);
            }
        }  
        finally { _lockObj.Exit(); } // завершение критической секции
    }
}

Консольный вывод:


Поток 1: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5

Мы видим, что действия критической секции были выполнены только одним потоком. Потому что у нас потоки запускаются одновременно. Первый поток получил блокировку (метод _lockObj.TryEnter() возвратил true) и этот поток стал выполнять действия из критической секции. Поскольку блокировка стала занята, для других потоков метод _lockObj.TryEnter() возвратил false, и они не стали выполнять действия из критической секции и не стали ждать освобождения блокировки.

EnterScope()

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


int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}


void Print()
{
    using (_lockObj.EnterScope())  // начало критической секции
    {
        x = 1;
        for (int i = 1; i < 6; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}

Причем в данном случае не надо вызывать метод Exit(), выход из критической секции и освобождение блокировки происходит автоматически. Консольный вывод:


Поток 1: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5

Проверь себя

📋 Тест по теме «Класс Lock и синхронизация» Тест по теме

Практические задания

Лёгкое Ручная проверка Заполните пропуск в коде (1)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

int x = _____;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    lock(_lockObj)  // начало критической секции
    {
        x = 1;
        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (2)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Поток _____: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (3)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

int x = 0;  // некоторый общий ресурс
Lock _____ = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    _lockObj.Enter();;  // начало критической секции
    try
    {
        x = 1;
        for (int i = 1; i < 6; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  
    finally { _lockObj.Exit(); } // завершение критической секции
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (4)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

int x = 0;  // некоторый общий ресурс
Lock _lockObj = _____(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}


void Print()
{
    if (_lockObj.TryEnter())  // начало критической секции
    {
        try
        {
            x = 1;
            for (int i = 1; i < 6; i++)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
                x++;
                Thread.Sleep(100);
            }
        }  
        finally { _lockObj.Exit(); } // завершение критической секции
    }
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (5)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Поток _____: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (1)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread._____ = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    lock(_lockObj)  // начало критической секции
    {
        x = 1;
        for (int i = 1; i < _____; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (2)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

Поток 1: 1
Поток 1: 2
Поток 1: _____
Поток 1: _____
Поток 1: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5
Войдите, чтобы отправить решение
Среднее Ручная проверка Расставьте строки кода в правильном порядке
+20 XP

Строки кода из примера этого урока перемешаны. Впишите их в правильном порядке, каждую строку — с новой строки.

Поток 1: 5
Поток 1: 3
Поток 1: 4
Поток 1: 1
Поток 1: 2
Войдите, чтобы отправить решение
Сложное Ручная проверка Объясните и модифицируйте пример из урока «Класс Lock и синхронизация»
+30 XP

Опишите своими словами, что делает приведённый пример кода из урока «Класс Lock и синхронизация», и предложите одно осмысленное изменение (например, другое входное значение или дополнительное условие). Приведите итоговый код.

int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    lock(_lockObj)  // начало критической секции
    {
        x = 1;
        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}
Войдите, чтобы отправить решение
Сложное Ручная проверка Найдите потенциальную проблему в коде урока «Класс Lock и синхронизация»
+30 XP

Изучите приведённый фрагмент кода и опишите, при каких входных данных или условиях он может сработать не так, как ожидается (граничный случай, отсутствие проверки и т.п.). Предложите исправленный вариант.

Поток 1: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5
Войдите, чтобы отправить решение
Мини-проект Ручная проверка Мини-проект по теме «Класс Lock и синхронизация»
+50 XP

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

Войдите, чтобы отправить решение
Домашнее задание Ручная проверка Домашнее задание по теме «Класс Lock и синхронизация»
+25 XP

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

Войдите, чтобы отправить решение

Обсуждение (0)

Войдите, чтобы оставить комментарий.