РЕДАКТИРОВАНИЕ:
Из ваших комментариев я могу сделать вывод, что вы не понимаете разницы между неизменяемостью и безопасностью потоков.
Неизменяемость означает, что экземпляр объекта можетне может быть изменен (его внутреннее или внешнее состояние не может быть изменено).
Потокобезопасный означает, что несколько потоков могут получить доступ к классу / методу, не вызывая ошибок, таких как состояние гонки, взаимоблокировки или неожиданное поведение, например, что-то, что должно выполнятьсяодин раз выполняется дважды.
Неизменяемые объекты являются поточно-ориентированными, но что-то не обязательно должно быть неизменным, чтобы быть поточно-ориентированным.
Ваш класс Tenants
не является ни неизменным, ни поточно-ориентированным, потому что:
- Это внутреннее состояние может измениться после создания экземпляра.
- Он содержит непредвиденное поведение, когда запрос на получение конфигурации выполняется дважды, тогда как это должно происходить только один раз.
Если вы прочитаете мой ответ ниже, вы можете определить, что если вы не противce (которым вы не должны быть): вам не нужно ничего делать, но вы можете добавить ключевое слово volatile в поле parser
, чтобы предотвратить сценарии SOME , но определенно не все.
Вы не видите никаких блокировок в неизменяемых объектах, потому что в состоянии объекта не происходит запись.
Когда в объекте выполняются операции записи, он больше не является неизменяемым (например, ваш * 1029). * класс). Чтобы сделать объект, подобный этому потоку, безопасным, вам нужно заблокировать операции записи, которые могут вызвать ошибки, такие как неожиданное поведение чего-то, что должно быть выполнено только один раз, если выполняется дважды.
ConfigParser
Кажетсячтобы быть потокобезопасным, Tenants
определенно не является.
Ваш класс Tenants
также не является неизменяемым, поскольку он предоставляет метод, который изменяет состояние класса (как GetSettings
, так и Refresh
методов).
Если 2 потока вызывают GetSettings
одновременно, когда parser
равен null
, будет сделано 2 запроса на получение ConfigParser
. Вы можете быть в порядке с этим, но это плохая практика, а также означает, что метод не является потокобезопасным.
Если у вас все в порядке с выполнением запроса дважды, вы можете использовать volatile
here:
Ключевое слово volatile указывает, что поле может быть изменено несколькими потоками, которые выполняются одновременно. Компилятор, система времени выполнения и даже аппаратное обеспечение могут перестраивать операции чтения и записи в области памяти по соображениям производительности. Поля, которые объявлены как изменчивые, не подлежат этой оптимизации. Добавление модификатора volatile гарантирует, что все потоки будут наблюдать записи volatile, выполняемые любым другим потоком в том порядке, в котором они были выполнены.
Volatile будет препятствовать тому, чтобы потоки имели устаревшие значения. Это означает, что вы можете предотвратить некоторые дополнительных запросов (от потоков, которые по-прежнему считают parser
нулевым), но это не будет полностью препятствовать выполнению метода или инструкции несколько раз в одно и то же время. ,
В этой ситуации вам необходимо lock
:
Оператор блокировки получает блокировку взаимного исключения для данного объекта, выполняет блок операторов,и затем снимает блокировку. Пока блокировка удерживается, нить, которая удерживает блокировку, может снова получить и снять блокировку. Любой другой поток блокируется от получения блокировки и ожидает, пока блокировка не будет снята.
Это означает, что вы можете запретить нескольким потокам выполнять метод или инструкцию несколько раз одновременно.
К сожалению, вы не можете использовать await
внутри lock
.
. Что вы хотите сделать:
- Если необходимо вызвать
Refresh
: - Если другой поток уже работает с
Refresh
- Подождите, пока другой поток завершит работу, и не вызывайте
Refresh
- Продолжите с результатом из другого потока
- , если на
Refresh
Я написал для этого библиотеку под названием TaskSynchronizer . Вы можете использовать это для создания истинной поточно-безопасной версии вашего класса Tenants
.
Пример:
public static TaskSynchronizer Synchronizer = new TaskSynchronizer();
public static async Task DoWork()
{
await Task.Delay(100); // Some heavy work.
Console.WriteLine("Work done!");
}
public static async Task WorkRequested()
{
using (Synchronizer.Acquire(DoWork, out var task)) // Synchronize the call to work.
{
await task;
}
}
static void Main(string[] args)
{
var tasks = new List<Task>();
for (var i = 0; i < 2; i++)
{
tasks.Add(WorkRequested());
}
Task.WaitAll(tasks.ToArray());
}
выведет:
Work done!
EG: TheМетод async DoWork
вызывается только один раз, даже если он вызывался дважды одновременно.