заблокировать свободную очередь, поставить в очередь, если она не пуста

Я реализовал очередь без блокировок в C, используя сравнение и замену на основе http://www.boyet.com/articles/LockfreeQueue.html.

Он отлично работает, но я пытаюсь интегрировать эту очередь в список пропуска без блокировки, который я реализовал. Я использую список пропуска в качестве очереди с приоритетом и хотел бы использовать очередь без блокировок внутри каждого узла для хранения нескольких значений при конфликте приоритетов. однако из-за способа управления узлами в списке пропуска, когда я обнаруживаю конфликт приоритетов, мне нужно иметь возможность добавлять элемент в очередь, только если очередь не пуста.

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

Итак, как мне написать атомарную операцию enqueue_if_not_empty?


person luke    schedule 03.05.2011    source источник
comment
В этом коде есть тонкая ошибка. Как указано в документе (boyet.com/articles/ABAProblem.html), он работает в C # из-за сборки мусора. Он НЕ РАБОТАЕТ в C.   -  person Richard Schneider    schedule 03.05.2011
comment
Я использую систему подсчета ссылок, которая решила проблему ABA на узлах в моей очереди. поэтому гарантируется, что узлы не будут освобождены раньше времени.   -  person luke    schedule 03.05.2011
comment
Я никогда не встречал решения, основанного на подсчете ссылок, которое разрешало бы free () ing элементов очереди. Я мог бы представить решение на основе подсчета ссылок, которое позволяло бы возвращать элементы в список фрилансеров, но это все. Вы хотите сказать, что разработали алгоритм безопасного восстановления памяти, основанный на подсчете ссылок?   -  person    schedule 04.05.2011
comment
Я не понимаю разницы между возвратом к бесплатному списку и бесплатным вызовом (который просто возвращает его в другой бесплатный список). как это могло быть безопасно для одного, а не для другого.   -  person luke    schedule 05.05.2011
comment
Если память возвращается в ваш свободный список, эта память остается правильно выделенной для вашего процесса, и любые потоки, которые у вас есть, могут безопасно разыменовать указатели на эти элементы (что происходит во многих алгоритмах без блокировки). Память, которая была освобождена () ed, возвращается в операционную систему и больше не принадлежит вашему процессу. Если поток попытается разыменовать такой указатель, ваш процесс выйдет из строя.   -  person    schedule 06.05.2011
comment
Возьмем, к примеру, очередь M&S. Представьте, что у вас есть элемент очереди, который обрабатывается потоком. Затем этот поток приостанавливается (выключается). Затем другой поток выводит этот элемент из очереди - и вместо того, чтобы возвращать его в список фрилансеров, фактически вызывает free (). Первый поток возвращается к жизни и пытается получить доступ к следующему указателю этого элемента. После этого процесс завершится аварийно; недопустимый доступ к памяти.   -  person    schedule 06.05.2011
comment
Это нормально работает, когда элемент находится в свободном списке из-за счетчика ABA; элемент из списка фрилансеров никогда не будет случайно использован. Но иметь возможность получить доступ к этому элементу необходимо.   -  person    schedule 06.05.2011
comment
Вы написали тест для своего кода? что-то, у кого одновременно работает куча потоков? вам нужно что-то подобное, чтобы начать обнаруживать какие-либо проблемы с синхронизацией.   -  person    schedule 06.05.2011
comment
Я много тестировал. поскольку я использую счетчик ссылок, когда первый поток выгружается, он уже будет иметь счетчик ссылок, поэтому узел не будет освобожден до тех пор, пока он не проснется (и не вызовет dec_ref).   -  person luke    schedule 07.05.2011
comment
У меня нет информации, чтобы продолжить, потому что я не знаю, как вы реализовали подсчет ссылок, но я чувствую, что это не может решить безопасное восстановление памяти, и если это так, то есть случаи сбоев, которых еще нет. очевидный. Вы бы опубликовали код в этой ветке? Мне любопытно посмотреть, что вы наделали.   -  person    schedule 07.05.2011


Ответы (2)


РЕДАКТИРОВАТЬ: Как было замечено, я написал функцию с совершенно противоположной семантикой - постановка в очередь только в пустую очередь. Я исправил имя, чтобы отразить это, и решил оставить его как есть на случай, если кому-то будет интересно. Итак, это неправильный ответ на вопрос, но, пожалуйста, не голосуйте против, если не найдете другую причину :)


Ниже представлена ​​попытка добавить EnqueueIfEmpty() к реализации очереди в указанной статье. Я не проверял, что он работает и даже компилируется. Основная идея состоит в том, что вы вставляете новый узел сразу после головы (а не хвоста), при условии, что следующий заголовок в настоящее время равен нулю (что является необходимым условием для пустой очереди). Я оставил дополнительные проверки на равенство головы и хвоста, которые, возможно, можно удалить.

public bool EnqueueIfEmpty(T item) {
  // Return immediately if the queue is not empty.
  // Possibly the first condition is redundant.
  if (head!=tail || head.Next!=null)
      return false;

  SingleLinkNode<T> oldHead = null;

  // create and initialize the new node
  SingleLinkNode<T> node = new SingleLinkNode<T>();
  node.Item = item;

  // loop until we have managed to update the tail's Next link 
  // to point to our new node
  bool Succeeded = false;
  while (head==tail && !Succeeded) {

    // save the current value of the head
    oldHead = head;         

    // providing that the tail still equals to head...
    if (tail == oldHead) {

      // ...and its Next field is null...
      if (oldhead.Next == null) {

        // ...try inserting new node right after the head.
        // Do not insert at the tail, because that might succeed
        // with a non-empty queue as well.
        Succeeded = SyncMethods.CAS<SingleLinkNode<T>>(ref head.Next, null, node);
      }

      // if the head's Next field was non-null, another thread is
      // in the middle of enqueuing a new node, so the queue becomes non-empty
      else {
        return false;
      }
    }
  }

  if (Succeeded) {
    // try and update the tail field to point to our node; don't
    // worry if we can't, another thread will update it for us on
    // the next call to Enqueue()
    SyncMethods.CAS<SingleLinkNode<T>>(ref tail, oldHead, node);
  }
  return Succeeded;
}
person Alexey Kukanov    schedule 03.05.2011
comment
если head.next имеет значение null, а tail == head, тогда очередь пуста ... поэтому мы бы хотели, чтобы enqueue завершился ошибкой. я думаю, вы написали enqueue_if_empty not enqueue_if_not_empty ... если я не запутался. - person luke; 04.05.2011
comment
Отличный улов! Моя ошибка - действительно, я написал прямо противоположное тому, что хотел :) Оставлю это в этом ответе, а может, напишу еще один. - person Alexey Kukanov; 04.05.2011
comment
Если head == tail и oldHead = head, не будет ли oldHead.next всегда NULL? зачем чек на это? - person ; 04.05.2011
comment
@Blank Xavier: не будет. Фактически, первичным условием того, что очередь пуста, является head.Next == null, потому что изменение ссылки на следующий элемент является первой операцией, видимой для других потоков. Таким образом, проверка на head == tail может считаться избыточной; но оставил на всякий случай и в некоторой степени для наглядности. - person Alexey Kukanov; 04.05.2011

Что ж, Enqueue-If-Not-Empty кажется относительно простым, но с ограничением: другие потоки могут одновременно удалять все предыдущие элементы из очереди, так что после завершения вставки в хвосте новый элемент может оказаться первый в очереди. Поскольку атомарные операции сравнения и замены выполняются с разными полями (постановка изменений tail.Next в очередь при выходе из очереди вперед head), более строгие гарантии потребуют дополнительной сложности не только в этой функции, но, по крайней мере, в Dequeue().

Достаточно следующих изменений в обычном методе Enqueue():
1) в начале функции проверьте, не является ли head.Next равным нулю, и если да, немедленно вернитесь, поскольку очередь пуста.
2) добавьте head.Next!=null в условие цикла в случае, если попытки постановки в очередь должны быть остановлены, если изначально непустая очередь становится пустой до того, как вставка будет успешной. Это не предотвращает описанную выше ситуацию (поскольку существует временной интервал между проверкой пустоты и вставкой узла), но снижает вероятность того, что это произойдет.
3) в конце функции попробуйте только продвинуть вперед tail, если новый узел был успешно поставлен в очередь (как я сделал в ответе Enqueue-If-Empty).

person Alexey Kukanov    schedule 04.05.2011