Привет, мир! :)

В последнем рассказе я рассмотрел Цель 1.1: реализовать многопоточность и асинхронную обработку.

В этой истории я собираюсь осветить вторую цель необходимого навыка «Управление потоком программы»: Управление многопоточностью!

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

Все коды для сертификационных серий можно найти здесь.

Синхронизация ресурсов

Я собираюсь предоставить вам один пример того, что может пойти не так при выполнении операций с использованием Threads с общими данными.

Пример:

Что ж, может быть, вы догадались, что номер отпечатка - 0… Верно? А может вы просто поняли, что что-то не так.

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

Предположим, что на первой итерации синхронного кода значение theAlmightyZero все еще равно 0. Оно уменьшит единицу, поэтому будет -1. Но в то же время асинхронный код уже получил доступ к начальному значению (0). Затем он увеличит это значение на 1, и оно будет равно 1.

Но подождите, значение уже обновлено! Да, значение должно быть 0 (сначала уменьшилось на 1, затем увеличилось на 1), но теперь оно равно 1!

Так что заметно, что нельзя полагаться на несинхронизированные данные!

Чтобы избежать такой ситуации, есть несколько вариантов.

Первое, что я собираюсь здесь рассмотреть, - это использование оператора lock. Как вы можете догадаться, он блокирует блок кода оператора. Никакие дополнительные потоки не могут получить доступ к заблокированному коду во время его выполнения.

Пример:

Однако вы можете попасть в тупик, если неосторожно воспользуетесь оператором lock.

Пример:

Вы должны использовать оператор lock с частным ссылочным объектом (компилятор выдаст ошибку, если вы используете для него тип значения), поскольку ваш объект может быть доступен другим потокам, если вы оставите его общедоступным. .

Кроме того, вам следует избегать использования lock для строки, потому что есть что-то, называемое внутренним хранилищем, которое хранит содержимое строки, не повторяя его. Это означает, например, что независимо от того, сколько строковых переменных имеют содержимое «Luis», во внутреннем пуле будет только один объект. Это позволяет вызывать объект в разных местах.

Летучий класс

Даже если вы этого не заметили, компилятор C # поможет вам оптимизировать ваш код. Если есть какой-то код, который никогда не будет выполнен, компилятор может даже полностью удалить его.

Пример:

Можно было бы ожидать, что либо ничего не будет напечатано, либо напечатано 10. Однако компилятор может решить переключить присвоение значения, и при выполнении метода TheSecondThread он напечатает 0.

Однако вы можете отключить оптимизацию компилятора, добавив ключевое слово volatile.

Класс Interlocked

Существует другой способ выполнить операции увеличения или уменьшения общего целого числа вместо использования оператора lock. В пространстве имен System.Threading есть класс Interlocked, который может сделать такие операции атомарными.

Пример:

Отмена задач

При выполнении длительных задач с помощью класса PLINQ или Parallel вы часто будете думать о том, как отменить выполнение в зависимости от результатов обработки. Для отмены таких задач можно использовать класс CancellationToken.

CancellationToken можно передать как параметр при выполнении задачи.

Пример:

Как показано в коде, вы можете проверить свойство IsCancellationRequest CancellationToken и решить, продолжать выполнение Task или нет.

Вы можете создать исключение OperationCanceledException внутри кода Task, чтобы было ясно, что Task был намеренно отменен. Кроме того, вы можете поместить вызов метода Wait в блок try-catch и обработать отмену Task.

Вместо того, чтобы перехватывать исключение, вы также можете использовать метод ContinueWith (вы его помните? Я рассказывал о его использовании в последней истории).

Пример:

Наконец, вы также можете отменить Task через определенное время, используя перегрузку метода Task.WaitAny, передав время ожидания (в миллисекундах).

Пример:

Если время истекло, метод Task.WaitAny вернет -1.

Что ж, это в основном то, что книга Exam Ref 70–483: Programming in C # охватывает вторую цель требуемого навыка Управление потоком программы. Я знаю, что для достижения этой цели нечего скрывать. :)

Я надеюсь тебе это понравится! В следующем рассказе я расскажу о цели Реализовать поток программы .

Код C # уже есть на GitHub!

Увидимся!

PS: Если вы найдете эту историю полезной, я приглашаю вас нажать «Хлопок». Также буду рад видеть Вас в качестве моего нового последователя!