Подтвердить что ты не робот

Это верно для потолка?

Просто проверка... _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...
}
4b9b3361

Ответ 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 меньше максимального, если тест не удается, я восстанавливаю старое значение