Я имею в виду страницы 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 говорится следующее:
Конечно, теперь, когда у вас есть средство для добавления данных в стек, вам нужен способ снова его отключить. На первый взгляд это довольно просто:
- Прочитать текущее значение
head
- Прочтите
head->next
- Настройте
head
наhead->next
- Возврат данных из извлеченного узла
- Удалить извлеченный узел
Однако при наличии нескольких потоков это не так просто. Если есть два потока, удаляющих элементы из стека, они оба могут прочитать одно и то же значение
head
на шаге 1. Если один поток затем проходит весь путь до шага 5, прежде чем другой переходит к шагу 2, второй поток будет разыменовывать висячий указатель.
Я думал, что compare_exchange_weak() можно использовать для поэтапного завершения шага 2 и 3, а второй поток может видеть, что head->next
больше недействителен?
Я удивлен, что мы не можем использовать CAS для решения вышеуказанной проблемы?
При удалении элемента вы не меняете 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;
}
head->next
будет нулевым? (Если честно, ваш вопрос кажется далеко не осмысленным, и мне интересно, понимаете ли вы логику этого кода вообще.)