Удаление двух потоков из стека без блокировки

0

Я имею в виду страницы 185 и 186 в C++ Параллельность в действии. Они дают следующий код как метод для стека блокировки:

void push(T const& data){
    node* const new_node = new node(data);
    new_node->next=head.load();
    while(!head.compare_exchange_weak(new_node->next, new_node));
}

и на P186 говорится следующее:

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

  1. Прочитать текущее значение head
  2. Прочтите head->next
  3. Настройте head на head->next
  4. Возврат данных из извлеченного узла
  5. Удалить извлеченный узел

Однако при наличии нескольких потоков это не так просто. Если есть два потока, удаляющих элементы из стека, они оба могут прочитать одно и то же значение head на шаге 1. Если один поток затем проходит весь путь до шага 5, прежде чем другой переходит к шагу 2, второй поток будет разыменовывать висячий указатель.

Я думал, что compare_exchange_weak() можно использовать для поэтапного завершения шага 2 и 3, а второй поток может видеть, что head->next больше недействителен?

Я удивлен, что мы не можем использовать CAS для решения вышеуказанной проблемы?

  • 0
    Почему head->next будет нулевым? (Если честно, ваш вопрос кажется далеко не осмысленным, и мне интересно, понимаете ли вы логику этого кода вообще.)
  • 0
    Я сделал предположение, что висячий указатель означает указание на объект, который теперь является нулевым. Что такого "далекого от разумного" в моем вопросе?
Показать ещё 6 комментариев
Теги:
multithreading
concurrency
stack
lock-free

1 ответ

0

При удалении элемента вы не меняете item-> далее, вы меняете голову на пункт item-> далее. Вам не разрешается изменять элемент, пока вы не вытащите его из списка. Если другой поток изменит item-> следующий, это означает, что они выскочили из списка, что означает, что версия ABA будет другой (я предполагаю, что вы используете проверку ABA в связанных списках, как это) и, вероятно (и именно поэтому вы используете ABA - из-за "вероятно" здесь) голова будет другой, поэтому ваш CAS не удастся.

Заметки об ABA, если вы никогда не кладете элемент обратно в список после его всплывания, и вы никогда не освобождаете какие-либо предметы (что позволит повторно использовать память элемента и зацикливаться на списке), тогда вам не нужен ABA проверить. Однако для реализации ABA требуется всего несколько бит, потому что вам нужно только столько версий, сколько могло произойти за время, необходимое для добавления элемента из списка. Мне нравится, по крайней мере, 16, но это могло бы сократить его. Каждый раз, когда вы добавляете элемент, изменяйте версию ABA. Каждый раз, когда вы удаляете элемент, измените версию ABA.

Другие важные примечания. Элементы в таком атомном списке, как это, не должны пересекаться с использованием головы/следующего, которые обрабатываются атомарно. Это потому, что вы никогда не знаете, удалил ли кто-нибудь предмет и изменил его. Я обнаружил, что если вы перейдете к чему-то еще в списке, скажите, чтобы пропустить элемент, это может работать в некоторых ситуациях. Элементы в списке атомов, подобные этому, никогда не должны быть добавлены или удалены в любом месте, кроме главы списка. Вы не можете сделать эту работу (легко... никогда не говорите никогда) с атомными операциями.

Вот пример, который работает в msvc 2010. Это использует версию Microsoft, что является изменчивым средством. Вы можете использовать std :: atomic, если ваш компилятор был iso. Обратите внимание на использование соединения. Union CAtomicLinkList - это то, как мы атомизировали несколько полей одновременно.

template <typename T> class CAtomicListItem : public T {
  typedef CAtomicListItem<T> CItem;
public:
  CItem*      m_pNext;

  CItem()  : m_pNext(NULL)                               { }
  CItem(T &r) : T(r),  m_pNext(NULL)                     { }
};

template <typename T> union CAtomicLinkList {
  typedef CAtomicLinkList<T> CList;
  typedef CAtomicListItem<T> CItem;
public:
  struct {
    QWORD     m_pHead : 44,    // Addr of first item.  Windows only uses 8tb
                      :  4,    // extra space.
              m_nABA  : 16;    // Version to prevent ABA.  We don't need this 
                               // many bits so there is room for expansion.
  };
  QWORD        m_n64;          // atomically update this struct by CAS on this field.

  CList() : m_n64(0)                                     { }
  // These constructors are for making copies.  These cannot threadsafe 
  // update a shared instance.
  CList(volatile const CList& r) : m_n64(r.m_n64)        { }

  void Push(CItem *pItem) volatile {
    while (1) {
      CList Old(*this), New(Old);
      New.m_pHead = UINT_PTR(pItem);
      New.m_nABA++;  // whenever you change the list, change the version
      pItem->m_pNext = (CItem*)Old.m_pHead;
      if (CAS(&m_n64, Old.m_n64, New.m_n64))
        return; // success
    }
  }

  CItem* Pop() volatile {
    while (1) {
      CList Old(*this);
      if (!Old.m_pHead)
        return NULL;
      CList New(Old);
      CItem* pItem = (CItem*)Old.m_pHead;
      New.m_pHead = UINT_PTR(pItem->m_pNext);
      New.m_nABA++; // whenever you change the list, change the version
      if (CAS(&m_n64, Old.m_n64, New.m_n64))
        return pItem; // success
    }
  }
};

inline bool CAS(volatile WORD* p, const WORD nOld, const WORD nNew) {
  Assert(IsAlign16(p)); 
  return WORD(_InterlockedCompareExchange16((short*)p, nNew, nOld)) == nOld; 
}
inline bool CAS(volatile DWORD* p, const DWORD nOld, const DWORD nNew) {
  Assert(IsAlign32(p)); 
  return DWORD(InterlockedCompareExchange((long*)p, nNew, nOld)) == nOld; 
}
inline bool CAS(volatile QWORD* p, const QWORD nOld, const QWORD nNew) { 
  Assert(IsAlign64(p)); 
  return QWORD(InterlockedCompareExchange64((LONGLONG*)p, nNew, nOld)) == nOld; 
}
inline bool CAS(volatile PVOID* pp, const void *pOld, const void *pNew) {
  Assert(IsAlign64(pp)); 
  return PVOID(InterlockedCompareExchangePointer(pp, (LPVOID)pNew, (LPVOID)pOld)) == pOld; 
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню