这是我正在维护的一些代码的简化版本:
int SomeFunc()
{
const long lIndex = m_lCurrentIndex;
int nSum = 0;
nSum += m_someArray[lIndex];
nSum += m_someArray[lIndex];
return nSum;
}
Run Code Online (Sandbox Code Playgroud)
lCurrentIndex由另一个线程定期更新.问题是; 将制作m_CurrentIndex的本地副本确保对m_someArray的两次访问都使用相同的索引?
请注意,这是一个简化的例子; 我正在考虑制作本地副本的概念,而不是这里显示的确切代码段.我知道编译器会将值放在寄存器中,但这仍然是本地副本,而不是从lCurrentIndex读取两次.
谢谢!
编辑:初始分配是安全的,在我们的设置中保证两者都是32位.Edit2:它们在32位边界上正确对齐(忘了那个)
我正在阅读Anthony Williams的C++ Concurrency in Action.在"理解轻松订购"部分,它有:
还有一些额外的东西可以告诉那个小隔间里的男人,比如"记下这个号码,并告诉我列表底部的内容 "(交换)和"如果底部的数字写下这个号码" 列表是那个; 否则告诉我我应该猜到的"(compare_exchange_strong),但这并不影响一般原则.
这是否意味着此类操作始终按修改顺序读取最后一个值(如果在约束之前没有其他内部线程发生)?即,有一些缓存更新/等(即使在轻松的订购)?
此代码段ConcurrentQueue来自此处给出的实现。
internal bool TryPeek(out T result)
{
result = default(T);
int lowLocal = Low;
if (lowLocal > High)
return false;
SpinWait spin = new SpinWait();
while (m_state[lowLocal] == 0)
{
spin.SpinOnce();
}
result = m_array[lowLocal];
return true;
}
Run Code Online (Sandbox Code Playgroud)
它真的是无锁的,而不是旋转的吗?
我已经看到了几个过于复杂的(在我看来很明显)在c ++中使用无锁堆栈的实现(使用像这里的标签),我想出了一个简单但仍然有效的实现.因为我无法在任何地方找到这个实现(我已经看到Push函数的实现类似于我所做的而不是Pop),我猜它在某种程度上是不正确的(很可能是ABA案例失败):
template<typename Data>
struct Element
{
Data mData;
Element<Data>* mNext;
};
template<typename Data>
class Stack
{
public:
using Obj = Element<Data>;
std::atomic<Obj*> mHead;
void Push(Obj *newObj)
{
newObj->mNext = mHead.load();
//Should I be using std::memory_order_acq_rel below??
while(!mHead.compare_exchange_weak(newObj->mNext, newObj));
}
Obj* Pop()
{
Obj* old_head = mHead.load();
while (1)
{
if (old_head == nullptr)
return nullptr;
//Should I be using std::memory_order_acq_rel below??
if(mHead.compare_exchange_weak(old_head, old_head->mNext)) ///<<< CL1
return old_head;
}
}
};
Run Code Online (Sandbox Code Playgroud)
我假设Push和Pop的调用者将负责内存分配和释放.另一种选择是制作上述Push和Pop私有方法,并使用新的公共函数来处理内存分配并在内部调用这些函数.我相信这个实现中最棘手的部分是我用"CL1"标记的行.我认为它是正确的并且仍然适用于ABA案例的原因如下:
让我们说ABA案件确实发生了.这意味着"CL1"处的mHead将等于old_head,但是它们指向的对象实际上与我将mHead分配给它时最初指向的old_head不同.但是,我认为即使它是一个不同的对象我们仍然可以,因为我们知道它是一个有效的"头".old_head指向与mHead相同的对象,因此它是堆栈的有效头部,这意味着old_head-> mNext是有效的下一个头.因此,将mHead更新为old_head-> mNext仍然是正确的!
总结一下:
我正在从c ++ 11移植一个无锁队列,然后我遇到了诸如此类的东西
auto currentRead = writeIndex.load(std::memory_order_relaxed);
Run Code Online (Sandbox Code Playgroud)
在某些情况下std::memory_order_release,并std::memory_order_aqcuire
也为C11以上的equivelent是一样的东西
unsigned long currentRead = atomic_load_explicit(&q->writeIndex,memory_order_relaxed);
Run Code Online (Sandbox Code Playgroud)
这里描述了那些的含义
是否有相当于这样的事情或我只是使用类似的东西
var currentRead uint64 = atomic.LoadUint64(&q.writeIndex)
Run Code Online (Sandbox Code Playgroud)
在移植我基准测试并且仅使用LoadUint64后,它似乎按预期工作但数量级更慢,我想知道这些专业操作对性能有多大影响.
memory_order_relaxed:宽松操作:没有同步或排序约束,此操作只需要原子性.
memory_order_consume:具有此内存顺序的加载操作对受影响的内存位置执行使用操作:在此加载之前,可以重新排序当前线程中与当前加载的值无关的读取.这确保了在当前线程中可以看到对释放相同原子变量的其他线程中的数据相关变量的写入.在大多数平台上,这仅影响编译器优化.
memory_order_acquire:具有此内存顺序的加载操作会对受影响的内存位置执行获取操作:在此加载之前,不能对当前线程中的内存访问进行重新排序.这确保了在当前线程中可以看到释放相同原子变量的其他线程中的所有写入.
memory_order_release:具有此内存顺序的存储操作执行释放操作:在此存储之后,当前线程中的内存访问不能重新排序.这确保了当前线程中的所有写入在获取或相同原子变量的其他线程中可见,并且带有依赖关系到原子变量的写入在消耗相同原子的其他线程中变得可见.
需要这个调用来实现一个无锁链表。AtomicMarkableReference 是 java.util.concurrent.atomic 包中的一个对象,它封装了对 T 类型对象的引用和布尔标记。这些字段可以一起或单独地自动更新。
谢谢你。
我的测试代码如下,我发现只有memory_order_seq_cstforbade编译器重新排序.
#include <atomic>
using namespace std;
int A, B = 1;
void func(void) {
A = B + 1;
atomic_thread_fence(memory_order_seq_cst);
B = 0;
}
Run Code Online (Sandbox Code Playgroud)
而其他选择memory_order_release,memory_order_acq_rel根本没有产生任何编译屏障.
我认为他们必须使用原子变量,如下所示.
#include <atomic>
using namespace std;
atomic<int> A(0);
int B = 1;
void func(void) {
A.store(B+1, memory_order_release);
B = 0;
}
Run Code Online (Sandbox Code Playgroud)
但我不想使用原子变量.与此同时,我认为"asm("":::"记忆")"太低了.
还有更好的选择吗?
编写满足无锁进度保证的算法或数据结构的困难之一是动态内存分配:以可移植的方式调用类似malloc或new不保证无锁的东西。然而,存在许多malloc或的无锁实现,new也有多种无锁内存分配器可用于实现无锁算法/数据结构。
但是,我仍然不明白这实际上如何完全满足无锁进度保证,除非您将数据结构或算法明确限制为某些预先分配的静态内存池。但是,如果您需要动态内存分配,我不明白从长远来看,任何所谓的无锁内存分配器如何才能真正实现无锁。问题是,无论您的无锁malloc或new可能多么出色,最终您可能会耗尽内存,此时您必须退回到向操作系统请求更多内存。这意味着最终你必须打电话brk()或mmap()或者一些这样的低级等价物来实际访问更多内存。并且根本无法保证任何这些低级调用都是以无锁方式实现的。
根本没有办法解决这个问题(除非您使用的是像 MS-DOS 这样不提供内存保护的古老操作系统,或者您编写自己的完全无锁的操作系统——这两种情况都不实用或不太可能。)那么,动态内存分配器如何才能真正做到无锁呢?
实现无锁数据结构的典型方法是使用原子 CAS 操作,例如std::compare_exchange_strong或std::compare_exchange_weak。这种技术的使用示例可以在 Antony Williams 的“C++ Concurrency in Action”中看到,其中实现了无锁堆栈。堆栈被实现为带有std::atomic<node*>头指针的链表。CAS 操作在推送和弹出期间在此指针上执行。但是 C++ 标准保证只有无std::atomic_flag锁,其他原子类型,包括std::atomic<T*>,可能不是无锁的。
1)我是否正确理解,如果std::atomic<T*>不是无锁(std::atomic::is_lock_free()返回false),那么基于CAS操作的数据结构std::atomic<T*>不是无锁的?
2)如果是,那么,如果std::atomic_flag是某些编译器的唯一无锁原子类型,那么在 C++ 上实现无锁数据结构的替代方法是什么?
我有一个小类,它使用std :: atomic进行无锁操作。由于该课程被广泛调用,因此影响了性能,并且遇到了麻烦。
该类类似于LIFO,但是一旦调用pop()函数,它仅返回其环形缓冲区的最后写入元素(仅当自上次pop()之后存在新元素时)。
一个线程正在调用push(),另一个线程正在调用pop()。
由于这占用了我的计算机时间太多,因此我决定进一步研究std :: atomic类及其memory_order。我已经阅读了很多StackOverflow以及其他来源和书籍中的memory_order帖子,但是我无法对不同的模式有一个清晰的了解。特别是,我在获取和释放模式之间挣扎:我也看不出为什么它们与memory_order_seq_cst不同。
memory_order_relaxed:在同一线程中,原子操作是即时的,但是其他线程可能无法立即看到最新的值,它们将需要一些时间才能被更新。编译器或OS可以自由地对代码进行重新排序。
memory_order_acquire / release:由atomic :: load使用。它防止重新排序之前存在的代码行(编译器/ OS可能在此行之后对其重新排序),并使用此线程或另一个线程中的memory_order_release或memory_order_seq_cst读取存储在此原子上的最新值。memory_order_release还可以防止对该代码重新排序之后的代码。因此,在获取/发布中,两者之间的所有代码都可以被OS改组。我不确定这是在同一线程还是不同线程之间。
memory_order_seq_cst:最容易使用,因为就像我们使用变量的自然写法一样,立即刷新其他线程加载函数的值。
template<typename T>
class LockFreeEx
{
public:
void push(const T& element)
{
const int wPos = m_position.load(std::memory_order_seq_cst);
const int nextPos = getNextPos(wPos);
m_buffer[nextPos] = element;
m_position.store(nextPos, std::memory_order_seq_cst);
}
const bool pop(T& returnedElement)
{
const int wPos = m_position.exchange(-1, std::memory_order_seq_cst);
if (wPos != -1)
{
returnedElement = m_buffer[wPos]; …Run Code Online (Sandbox Code Playgroud)