БЕЗОПАСНОСТЬ ПОТОКОВ В СЕРВЕРНЫХ ПРИЛОЖЕНИЯХ
Руслан Гибадуллин (КНИТУ-КАИ)

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

✅ РЕКОМЕНДУЕМАЯ КНИГА C#. Справочник. Полное описание языка | Албахари Бен, Албахари Джозеф https://www.ozon.ru/context/detail/id/145563645/

🔹 Потоковая безопасность (по первому слайду)
В качестве первого ингредиента блюда разбирается понятие потокобезопасности. Код потокобезопасен, если он функционирует исправно при его использовании несколькими потоками одновременно. На первом слайде видео представлен код, который не является потокобезопасным. В коде создаются экземпляры класса Thread, в параметре конструктора данного класса задается метод, который будет выполняться потоком после его запуска.  Метод Start переводит потоки в статус "Готовый к работе". Посредством метода Join ожидается завершение работы дочерних потоков в основном потоке. Далее выводится значение переменной Counter. Вследствие того, что инструкция Counter++ не является атомарной, то вывод может быть неоднозначным. Например, Counter  может получить итоговое значения 1 или 2. Чтобы код получился потокобезопасным необходимо обеспечить взаимно исключительный доступ к разделяемой переменной Counter.

🔹 Оператор lock (по второму слайду)
Класс ThreadUnsafe не безопасен в отношении потоков. Если метод Go будет вызван двумя потоками одновременно, то появится возможность получения ошибки деления на ноль. Дело в том, что в одном потоке поле v2 может быть установлено в 0 как раз тогда, когда выполнение в другом потоке находится между оператором if и вызовом метода Console.WriteLine. Ниже на втором слайде показано, как исправить данную проблему с помощью оператора lock. В каждый момент времени блокировать объект синхронизации locker может только один поток, и любые соперничающие потоки задерживаются до тех пор, пока блокировка не будет освобождена. Если за блокировку соперничают несколько потоков, тогда они ставятся в “очередь готовности” с предоставлением блокировки на основе “первым пришел — первым обслужен”. Здесь уместно говорить, что монопольные блокировки иногда приводят к последовательному доступу к объекту, защищаемому блокировкой, т.к. доступ одного потока не может совмещаться с доступом другого. В рассматриваемом случае посредством оператора lock обеспечивается потоковая безопасность логики внутри метода Go, а также полей v1 и v2.

🔹 Безопасность потоков в серверных приложениях (по третьему слайду)
Вполне очевидно, что серверные приложения должны быть многопоточными, чтобы одновременно  обрабатывать клиентские запросы. Это означает, что при написании кода на серверной стороне вы должны принимать во внимание безопасность к потокам, если есть хотя бы малейшая возможность взаимодействия между потоками, обрабатывающими клиентские запросы. К счастью, такая возможность возникает редко; типичный серверный класс либо не сохраняет состояние, либо имеет модель активизации, которая создает отдельный его экземпляр для каждого клиента. Взаимодействие обычно происходит только через статические поля, которые иногда применяются для кеширования в памяти частей базы данных с целью повышения производительности. Например, предположим, что имеется метод RetrieveUser, выдающий запрос к базе данных. Если этот метод вызывается часто, тогда показатели производительности можно было бы улучшить, кешируя результаты в статическом объекте Dictionary. На третьем слайде показано решение, учитывающее безопасность к потокам. Для обеспечения безопасности в отношении потоков мы должны, как минимум, применить блокировку к чтению и обновлению словаря. В приведенном на слайде примере мы отдаем предпочтение практичному компромиссу между простотой и производительностью в блокировании. Предлагаемое на слайде решение на самом деле создает очень небольшой потенциал для неэффективности: если два потока одновременно вызовут данный метод с одним и тем же ранее не извлеченным идентификатором id, то метод RetrieveUser будет вызван дважды — и словарь обновится лишний раз. Одиночное блокирование всего метода могло бы предотвратить такую ситуацию, но породить серьезную неэффективность: на протяжении вызова метода RetrieveUser блокировался бы целый кеш, и в это время другие потоки не могли бы извлекать информацию о любых пользователях.