Это верно для потолка?
Просто проверка... _count
осуществляется безопасно, правильно?
Доступ к обоим методам осуществляется несколькими потоками.
private int _count;
public void CheckForWork() {
if (_count >= MAXIMUM) return;
Interlocked.Increment(ref _count);
Task t = Task.Run(() => Work());
t.ContinueWith(CompletedWorkHandler);
}
public void CompletedWorkHandler(Task completedTask) {
Interlocked.Decrement(ref _count);
// Handle errors, etc...
}
Ответы
Ответ 1
Нет, if (_count >= MAXIMUM) return;
не является потокобезопасным.
edit: вам нужно будет также блокировать чтение, которое затем должно быть логически сгруппировано с приращением, поэтому я переписал бы как
private int _count;
private readonly Object _locker_ = new Object();
public void CheckForWork() {
lock(_locker_)
{
if (_count >= MAXIMUM)
return;
_count++;
}
Task.Run(() => Work());
}
public void CompletedWorkHandler() {
lock(_locker_)
{
_count--;
}
...
}
Ответ 2
Это потокобезопасность, правильно?
Предположим, что MAXIMUM равен единице, число равно нулю, а пять потоков - CheckForWork.
Все пять потоков могут проверить, что счетчик меньше MAXIMUM. Счетчик будет сбит до пяти и начнется пять заданий.
Это противоречит намерению кода.
Кроме того: поле нестабильно. Итак, какой механизм гарантирует, что любой поток прочитает актуальное значение на пути без памяти? Ничто не гарантирует этого! Вы только делаете барьер памяти, если условие ложно.
В более общем плане: здесь вы делаете ложную экономию. Перейдя к решению с низким уровнем блокировки, вы сохраняете дюжину наносекунд, которые потребует блокировка без ограничений. Просто возьмите блокировку. Вы можете позволить себе дополнительные дюжины наносекунд.
И даже в более общем плане: не писать код с низким уровнем блокировки, если вы не являетесь экспертом по архитектуре процессоров и не знаете всех оптимизаций, которые CPU может выполнять на путях с низкой блокировкой. Вы не такой эксперт. Я тоже. Вот почему я не пишу код с низкой блокировкой.
Ответ 3
Что то, что Semaphore и SemaphoreSlim предназначены для:
private readonly SemaphoreSlim WorkSem = new SemaphoreSlim(Maximum);
public void CheckForWork() {
if (!WorkSem.Wait(0)) return;
Task.Run(() => Work());
}
public void CompletedWorkHandler() {
WorkSem.Release();
...
}
Ответ 4
Нет, то, что у вас есть, небезопасно. Проверка того, может ли _count >= MAXIMUM
участвовать в гонке с вызовом Interlocked.Increment
из другого потока. На самом деле это действительно сложно решить с использованием методов с низким уровнем блокировки. Чтобы это правильно работало, вам нужно сделать ряд из нескольких операций, которые кажутся атомарными без использования блокировки. Это трудная часть. Ниже приведен ряд операций:
- Прочитайте
_count
- Тест
_count >= MAXIMUM
- Принять решение на основе вышеизложенного.
- Приращение
_count
в зависимости от принятого решения.
Если вы не сделаете все 4 из этих шагов атомарными, тогда будет состояние гонки. Стандартный шаблон для выполнения сложной операции без фиксации выглядит следующим образом.
public static T InterlockedOperation<T>(ref T location)
{
T initial, computed;
do
{
initial = location;
computed = op(initial); // where op() represents the operation
}
while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
return computed;
}
Обратите внимание, что происходит. Операция повторяется до тех пор, пока операция ICX не определит, что начальное значение не изменилось между моментом, когда оно было впервые прочитано, и временем, когда была сделана попытка изменить его. Это стандартный шаблон, и все это происходит из-за вызова CompareExchange
(ICX). Обратите внимание, однако, что это не учитывает проблему ABA. 1
Что вы могли бы сделать:
Таким образом, взятие вышеуказанного шаблона и включение его в ваш код приведет к этому.
public void CheckForWork()
{
int initial, computed;
do
{
initial = _count;
computed = initial < MAXIMUM ? initial + 1 : initial;
}
while (Interlocked.CompareExchange(ref _count, computed, initial) != initial);
if (replacement > initial)
{
Task.Run(() => Work());
}
}
Лично я целиком и полностью опираюсь на стратегию с низким уровнем блокировки. Есть несколько проблем с тем, что я представил выше.
- Это может работать медленнее, чем жесткая блокировка. Причины трудно объяснить и вне сферы моего ответа.
- Любое отклонение от того, что выше, вероятно, приведет к сбою кода. Да, это действительно то, что хрупкое.
- Трудно понять. Я имею ввиду, посмотри на это. Это уродливо.
Что вы должны сделать:
Переход с жесткой блокировкой вашего кода может выглядеть так.
private object _lock = new object();
private int _count;
public void CheckForWork()
{
lock (_lock)
{
if (_count >= MAXIMUM) return;
_count++;
}
Task.Run(() => Work());
}
public void CompletedWorkHandler()
{
lock (_lock)
{
_count--;
}
}
Обратите внимание, что это намного проще и значительно меньше подвержено ошибкам. Вы действительно можете найти, что этот подход (жесткий замок) на самом деле быстрее, чем то, что я показал выше (низкий замок). Опять же, причина сложная, и есть методы, которые могут быть использованы для ускорения работы, но они выходят за рамки этого ответа.
1 Проблема ABA на самом деле не является проблемой в этом случае, потому что логика не зависит от того, что _count
остается неизменным. Важно только то, что его ценность одинакова в два момента времени независимо от того, что произошло между ними. Другими словами, проблема может быть сведена к той, в которой казалось, что значение не изменилось, хотя на самом деле оно может иметь.
Ответ 5
Определите безопасный поток.
Если вы хотите убедиться, что _count никогда не будет больше MAXIMUM, чем вам не удалось.
Что вы должны сделать, так это заблокировать это:
private int _count;
private object locker = new object();
public void CheckForWork()
{
lock(locker)
{
if (_count >= MAXIMUM) return;
_count++;
}
Task.Run(() => Work());
}
public void CompletedWorkHandler()
{
lock(locker)
{
_count--;
}
...
}
Вы также можете взглянуть на класс SemaphoreSlim.
Ответ 6
вы можете сделать следующее, если вы не хотите блокировать или перемещаться в семафор:
if (_count >= MAXIMUM) return; // not necessary but handy as early return
if(Interlocked.Increment(ref _count)>=MAXIMUM+1)
{
Interlocked.Decrement(ref _count);//restore old value
return;
}
Task.Run(() => Work());
Приращение возвращает добавочное значение, по которому вы можете дважды проверить, было ли значение _count меньше максимального, если тест не удается, я восстанавливаю старое значение