我读过很多关于volatile和VoletileRead(ReadAcquireFence)的矛盾信息(msdn,SO等).
我理解那些内存访问重新排序的限制含义 - 我仍然完全混淆的是新鲜度保证 - 这对我来说非常重要.
(...)这可确保始终在字段中显示最新值.
读取volatile字段称为volatile读取.易失性读取具有"获取语义"; 也就是说,保证在指令序列之后发生的任何内存引用之前发生.
public static int VolatileRead(ref int address)
{
int ret = address;
MemoryBarrier(); // Call MemoryBarrier to ensure the proper semantic in a portable way.
return ret;
}
Run Code Online (Sandbox Code Playgroud)
根据msdn MemoryBarrier doc内存屏障阻止重新排序.然而,这似乎对新鲜度没有任何影响 - 对吗?
如何获得新鲜度保证?标记字段volatile与使用VolatileRead和VolatileWrite语义访问它之间有区别吗?我目前正在执行我的性能关键代码,需要保证新鲜度,但读者有时会得到陈旧的价值.我想知道是否标记状态不稳定将使情况不同.
EDIT1:
我正在努力实现的目标 - 保证读者线程将尽可能获得共享变量(由多个编写者编写)的最新值 - 理想情况下不会超过上下文切换或其他可能推迟立即操作的操作的成本写国家.
如果挥发性或更高级别的构造(例如锁定)具有这种保证(是吗?)而不是它们如何实现这一点?
EDIT2:
非常简洁的问题应该是 - 如何在读取过程中尽可能保证新的价值?理想情况下没有锁定(因为不需要独占访问,并且存在高争用的可能性).
从我在这里学到的东西,我想知道这可能是解决方案(求解(?)行标有注释):
private SharedState _sharedState;
private SpinLock _spinLock = new SpinLock(false);
public void Update(SharedState newValue) …Run Code Online (Sandbox Code Playgroud) 我最近阅读了Paul McKenney的2010年白皮书"内存障碍:软件黑客的硬件视图".
我非常感谢关于下面给出的一小部分C代码的反馈/评论/指导,这些代码实现了M&S队列排队功能,特别是在内存和编译器障碍方面.
此代码使用指针计数器对来处理ABA,并且为了这篇文章,应该被认为是为x86/x64编写的.
对于这篇文章,内联评论现在都写完了,并且是这篇文章的一部分,因为它们表达了我现在的想法.
为简洁起见,我在结构中删除了断言代码和缓存行填充等.
目前,我认为代码很破碎,但我想确保我认为正确的原因.
#define PAC_SIZE 2
struct lfds_queue_element
{
struct lfds_queue_element
*volatile next[PAC_SIZE];
void
*user_data;
};
struct lfds_queue_state
{
struct lfds_queue_element
*volatile enqueue[PAC_SIZE];
struct lfds_queue_element
*volatile dequeue[PAC_SIZE];
atom_t volatile
aba_counter;
};
void lfds_queue_internal_dcas_pac_enqueue( struct lfds_queue_state *lqs, struct lfds_queue_element *lqe )
{
ALIGN(ALIGN_DOUBLE_POINTER) struct lfds_queue_element
*local_lqe[PAC_SIZE], *enqueue[PAC_SIZE], *next[PAC_SIZE];
unsigned char cas_result = 0;
unsigned int backoff_iteration = 1;
/* TRD : here we have been passed a new element to place
into the …Run Code Online (Sandbox Code Playgroud) 我遇到了MS C编译器重新排序某些语句的问题,这些语句在多线程上下文中非常关键,处于高优化级别.我想知道如何在特定的地方强制订购,同时仍然使用高水平的优化.(在低优化级别,此编译器不重新排序语句)
以下代码:
ChunkT* plog2sizeChunk=...
SET_BUSY(plog2sizeChunk->pPoolAndBusyFlag); // set "busy" bit on this chunk of storage
x = plog2sizeChunk->pNext;
Run Code Online (Sandbox Code Playgroud)
产生这个:
0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh
Run Code Online (Sandbox Code Playgroud)
其中pPoolAndBusyFlag的写入由编译器重新排序,在pNext fetch 之后发生.
SET_BUSY本质上是
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
Run Code Online (Sandbox Code Playgroud)
我认为编译器已经正确地决定重新排序这些访问是可行的,因为它们是同一结构的两个独立成员,并且这种重新排序对单线程执行的结果没有影响:
typedef struct chunk_tag{
unsigned pPoolAndBusyFlag; // Contains pointer to owning pool and a busy flag
natural log2size; // holds log2size of the chunk if Busy==false
struct chunk_tag* pNext; // holds pointer to next block of same size
struct …Run Code Online (Sandbox Code Playgroud) 在实现一个用于线程安全的类时,我是否应该在其构造函数的末尾包含一个内存屏障,以确保在访问它们之前已完成任何内部结构的初始化?或者消费者有责任在将实例提供给其他线程之前插入内存屏障吗?
简化问题:
由于初始化和线程安全类的访问之间缺乏内存屏障,下面的代码中是否存在种族危险可能会导致错误行为?或者线程安全类本身是否应该防止这种情况?
ConcurrentQueue<int> queue = null;
Parallel.Invoke(
() => queue = new ConcurrentQueue<int>(),
() => queue?.Enqueue(5));
Run Code Online (Sandbox Code Playgroud)
请注意,程序可以接受任何内容,如果第二个委托在第一个委托之前执行,则会发生这种情况.(null条件运算符?.可以防止NullReferenceException这里.)然而,程序抛出一个IndexOutOfRangeException,多次入NullReferenceException队5,陷入无限循环,或者做任何由种族危险引起的其他奇怪事情都是不可接受的关于内部结构.
详细问题:
具体来说,假设我正在为队列实现一个简单的线程安全包装器.(我知道.NET已经提供了ConcurrentQueue<T>;这只是一个例子.)我可以写:
public class ThreadSafeQueue<T>
{
private readonly Queue<T> _queue;
public ThreadSafeQueue()
{
_queue = new Queue<T>();
// Thread.MemoryBarrier(); // Is this line required?
}
public void Enqueue(T item)
{
lock (_queue)
{
_queue.Enqueue(item);
}
}
public bool TryDequeue(out T item)
{
lock (_queue)
{
if (_queue.Count == …Run Code Online (Sandbox Code Playgroud) 假设我有两个线程来操纵全局变量x.每个线程(或我认为的每个核心)都有一个缓存副本x.
现在说Thread A执行以下说明:
set x to 5
some other instruction
Run Code Online (Sandbox Code Playgroud)
现在set x to 5执行时,缓存的值x将设置为5,这将导致缓存一致性协议使用新值来操作和更新其他核心的缓存x.
现在我的问题是:什么时候x实际设置5在Thread A缓存中,其他内核的缓存在some other instruction执行之前是否会更新?或者应该使用内存屏障来确保?:
set x to 5
memory barrier
some other instruction
Run Code Online (Sandbox Code Playgroud)
注意:假设指令是按顺序执行的,也假设set x to 5执行时,5会立即放入线程A的缓存中(因此指令不会放在队列中或稍后要执行的内容).
我最近在我的代码库中找到了这个gem:
/** This class is used to "publish" changes to a non-volatile variable.
*
* Access to non-volatile and volatile variables cannot be reordered,
* so if you make changes to a non-volatile variable before calling publish,
* they are guaranteed to be visible to a thread which calls syncChanges
*
*/
private static class Publisher {
//This variable may not look like it's doing anything, but it really is.
//See the documentaion for this class.
private volatile AtomicInteger sync …Run Code Online (Sandbox Code Playgroud) 似乎接受和释放语义的公认定义是这样的:(引自http://msdn.microsoft.com/en-us/library/windows/hardware/ff540496(v=vs.85).aspx)
如果其他处理器在任何后续操作的效果之前始终看到其效果,则操作已获取语义.如果其他处理器在操作本身的影响之前将看到每个先前操作的效果,则操作具有释放语义.
我简要地读过半存储器障碍的存在,并且据说它们具有获取障碍的风格并且遵循上述相同的语义释放障碍.
查看硬件指令的真实示例我遇到了SFENCE.此博客(http://peeterjoot.wordpress.com/2009/12/04/intel-memory-ordering-fence-instructions-and-atomic-operations/)表示它是一种释放栅栏/屏障:
英特尔提供双向围栏指令MFENCE,获取围栏LFENCE和释放围栏SFENCE.
但是,阅读SFENCE的定义,它似乎没有提供发布语义,因为它根本不与负载同步?而我所理解的释放语义定义了关于所有内存操作(加载和存储)的排序.
在C#中,这是以线程安全方式调用事件的标准代码:
var handler = SomethingHappened;
if(handler != null)
handler(this, e);
Run Code Online (Sandbox Code Playgroud)
其中,可能在另一个线程上,编译器生成的add方法用于Delegate.Combine创建新的多播委托实例,然后在编译器生成的字段上设置该实例(使用互锁比较交换).
(注意:出于这个问题的目的,我们不关心在事件订阅者中运行的代码.假设它在删除时是线程安全且健壮的.)
在我自己的代码中,我想按照以下方式做类似的事情:
var localFoo = this.memberFoo;
if(localFoo != null)
localFoo.Bar(localFoo.baz);
Run Code Online (Sandbox Code Playgroud)
哪里this.memberFoo可以由另一个线程设置.(这只是一个线程,所以我不认为它需要联锁 - 但也许这里有副作用?)
(并且,显然,假设它Foo是"不可变的",我们在这个线程上使用它时不会主动修改它.)
现在我理解这是线程安全的明显原因:从引用字段读取是原子的.复制到本地可确保我们不会获得两个不同的值.(显然只能从.NET 2.0保证,但我认为它在任何理智的.NET实现中都是安全的吗?)
但我不明白的是:被引用的对象实例所占用的内存如何?特别是在缓存一致性方面?如果"writer"线程在一个CPU上执行此操作:
thing.memberFoo = new Foo(1234);
Run Code Online (Sandbox Code Playgroud)
什么保证Foo分配新内存的内存不会出现在"读取器"运行的CPU的缓存中,具有未初始化的值?什么确保localFoo.baz(上面)不读取垃圾?(跨平台的保证有多好?在Mono上?在ARM上?)
如果新创建的foo恰好来自游泳池呢?
thing.memberFoo = FooPool.Get().Reset(1234);
Run Code Online (Sandbox Code Playgroud)
从内存的角度来看,这似乎没有什么不同,只是一个新的分配 - 但是.NET分配器可能会让第一个案例有效吗?
在我提出这个问题时,我的想法是需要一个内存屏障来确保 - 不是因为读取依赖而不能移动内存访问 - 而是作为CPU的一个信号来清除任何缓存失效.
我的来源是维基百科,所以你要做的就是这样.
(我可能会猜测,也许在连动比较交换作家线程上无效缓存读者?或者,也许所有的读取原因失效吗?或者指针引用引起失效?我特别关注如何平台特有的这些东西的声音.)
更新:只是为了更明确地说明问题是关于CPU缓存失效以及.NET提供的保证(以及这些保证可能如何依赖于CPU架构):
Q(内存位置)中存储了引用.R …英特尔内存模型保证:
http://bartoszmilewski.com/2008/11/05/who-ordered-memory-fences-on-an-x86/
我已经看到声称由于Intel内存模型,SFENCE在x86-64上是多余的,但从来没有LFENCE.上述内存模型规则是否使指令冗余?
假设我想在线程之间使用布尔状态标志来进行协作取消.(我意识到最好使用一个CancellationTokenSource;这不是这个问题的重点.)
private volatile bool _stopping;
public void Start()
{
var thread = new Thread(() =>
{
while (!_stopping)
{
// Do computation lasting around 10 seconds.
}
});
thread.Start();
}
public void Stop()
{
_stopping = true;
}
Run Code Online (Sandbox Code Playgroud)
问:如果我在另一个线程上调用Start()0s和Stop()3s,那么循环是否保证在当前迭代结束时在10s左右退出?
我见过的绝大多数消息来源表明上述内容应该按预期工作; 见: MSDN ; Jon Skeet ; 布赖恩吉迪恩 ; 马克格拉维尔 ; Remus Rusanu.
但是,volatile只会在读取时生成获取栅栏,并在写入时生成释放栅栏:
易失性读取具有"获取语义"; 也就是说,保证在指令序列之后发生的任何内存引用之前发生.易失性写入具有"释放语义"; 也就是说,保证在指令序列中的写指令之前的任何存储器引用之后发生.(C#规格)
因此,正如Joseph Albahari所观察到的那样,无法保证不会(似乎)交换易失性写入和易失性读取.因此,后台线程可能会在当前迭代结束后继续读取_stopping(即false)的陈旧值.具体地说,如果我Start()在0和Stop()3s …
memory-barriers ×10
c# ×4
.net ×3
concurrency ×3
volatile ×3
assembly ×2
c ×2
x86 ×2
64-bit ×1
atomic ×1
cpu-cache ×1
java ×1
lock-free ×1
memory-model ×1
mesi ×1
optimization ×1