在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 …据我所知,C#是一种安全的语言,除了通过unsafe关键字之外,不允许访问未分配的内存.但是,当线程之间存在不同步的访问时,其内存模型允许重新排序.这会导致竞争危险,在实例完全初始化之前,对新实例的引用似乎可用于竞赛线程,并且是双重检查锁定的众所周知的问题.Chris Brumme(来自CLR团队)在他们的Memory Model文章中解释了这一点:
考虑标准的双锁协议:
if (a == null)
{
lock(obj)
{
if (a == null)
a = new A();
}
}
Run Code Online (Sandbox Code Playgroud)
这是在典型情况下避免锁定读取'a'的常用技术.它在X86上运行得很好.但它将被ECMA CLI规范的合法但薄弱的实施所打破.确实,根据ECMA规范,获取锁具有获取语义并释放锁具有释放语义.
但是,我们必须假设在建造'a'期间已经发生了一系列商店.这些商店可以任意重新排序,包括将它们推迟到将新对象分配给'a'的出版商店之后的可能性.那时,在store.release之前有一个小窗口离开锁.在该窗口内,其他CPU可以在引用'a'中导航并查看部分构造的实例.
我一直对"部分构造的实例"的含义感到困惑.假设.NET运行时在分配而不是垃圾收集(讨论)时清除内存,这是否意味着另一个线程可能会读取仍包含垃圾收集对象数据的内存(如不安全语言中发生的情况)?
考虑以下具体示例:
byte[] buffer = new byte[2];
Parallel.Invoke(
() => buffer = new byte[4],
() => Console.WriteLine(BitConverter.ToString(buffer)));
Run Code Online (Sandbox Code Playgroud)
以上是竞争条件; 输出将是00-00或00-00-00-00.但是,第二个线程是否有可能在数组的内存初始化为0 buffer 之前读取新引用,并输出一些其他任意字符串?
是否有可能在C#中抢占构造函数?
例如,考虑代码:
public class A
{
public bool ready = true;
public A()
{
ready = false; // Point #1
// Other initialization stuff
ready = true; // Point #2
}
}
Run Code Online (Sandbox Code Playgroud)
在代码中的其他地方,两个线程可以访问类型A的变量,第一个线程调用在#1点抢占的构造函数.然后第二个线程测试ready并发现它仍然是真的因此它做了一些坏事.
这种情况可能吗?
进一步来说:
lock构造函数中的同步代码?假设我们有以下代码:
class Program
{
static volatile bool flag1;
static volatile bool flag2;
static volatile int val;
static void Main(string[] args)
{
for (int i = 0; i < 10000 * 10000; i++)
{
if (i % 500000 == 0)
{
Console.WriteLine("{0:#,0}",i);
}
flag1 = false;
flag2 = false;
val = 0;
Parallel.Invoke(A1, A2);
if (val == 0)
throw new Exception(string.Format("{0:#,0}: {1}, {2}", i, flag1, flag2));
}
}
static void A1()
{
flag2 = true;
if (flag1)
val = 1;
} …Run Code Online (Sandbox Code Playgroud) fallowing子句来自jetbrains.net在阅读了这篇以及网上的其他文章后,我仍然不明白在第一个线程进入锁之后如何返回null.有人确实理解它可以帮助我并以更人性化的方式解释它吗?
"考虑以下代码:
public class Foo
{
private static Foo instance;
private static readonly object padlock = new object();
public static Foo Get()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Foo();
}
}
}
return instance;
}
};
Run Code Online (Sandbox Code Playgroud)
给定上面的代码,初始化Foo实例的写入可以被延迟,直到写入实例值,从而产生实例返回处于单元化状态的对象的可能性.
为了避免这种情况,必须使实例值易变."