.net - 代码应该对CPU内存模型做出哪些假设,以及如何记录这些假设?

标签 .net multithreading memory-barriers memory-model

据我所读,英特尔处理器体系结构执行的内存模型比.net实现所要求的要强。代码在多大程度上可以利用英特尔处理器的保证,或者代码应在多大程度上增加英特尔实现不需要的内存屏障,以防代码被迁移到性能较弱的平台上内存模型?用以下方法定义静态类是否合适: “如果使用弱内存模型,则执行内存屏障”,并要求代码适本地与该库的“强模型”或“弱模型”版本链接?或者,可以使用反射在程序启动时生成这样的静态类,以使JIT编译器在使用强模型时可以“内联扩展”“弱内存条”指令,而不执行任何操作(即,省略)它们完全来自JITted代码)?

如果我有德鲁特,.net将通过一些半锁操作提供MemoryLock类的变体,这将要求所有持有半锁的线程都必须遵守该半锁的内存模型。在具有非常强大的内存模型的系统中,半锁不会执行任何操作。在内存模型非常薄的系统中,任何希望进入半锁且已经有另一个线程的线程都必须等待,直到第一个线程退出,或者可以与CPU或内核进行调度(基于在第一个线程正在使用的半锁指定的模型上)。请注意,与普通锁不同,MemoryLock永远不会死锁,因为可以通过调度所有线程在同一CPU上运行来解决冲突的锁要求的任何组合,并且系统可以释放死掉的线程持有的任何MemoryLock(因为目的MemoryLock的功能是保护资源免于以违反内存模型的方式被访问,并且死线程当然不能进行这种访问)。

当然,从.net 4.0开始,这种东西就不存在了。鉴于此,处理确实存在的情况的最佳方法是什么?在没有某种手段来执行更强大的模型的情况下,将旨在将更强大的内存模型设计的代码迁移到具有较弱模型的系统,这将是灾难的根源,但是增加了许多LockMemoryBarrier调用,对于代码的原始目标平台似乎并不吸引人。我所知道的用于代码强制建立强大内存模型的唯一方法是让每个线程都设置其CPU亲和力。如果有一种方法可以设置进程选项,则.net一次只能使用一个内核,这可能会很有用(特别是如果这意味着JIT可以用更快的非总线锁定等效项代替总线锁定互锁操作) ,但我知道设置CPU亲和力的唯一方法是将程序限制为对其所有线程使用特定的选定CPU,即使该CPU被其他应用程序大量负载并且其他CPU处于空闲状态也是如此。

附录

考虑以下代码:

//线程1 –假设在开始时SharedPerson指向一个人“Smiley”,“George”
var newPerson = new Person();
newPerson.LastName =“辛普森”;
newPerson.FirstName =“Bart”;
//MaybeMemoryBarrier1
SharedPerson = newPerson;

//线程2
var wasPerson = SharedPerson;
//MaybeMemoryBarrier2
var wasLastName = wasPerson.FirstName;
var WasFirstName = wasPerson.LastName;

据我了解,即使在没有内存障碍的情况下,在Intel处理器上运行的代码也将保证不会对写入进行重新排序。因此,在线程2中,被读取的人将是“Smiley”,“George”或“Simpson”,“Bart”。但是,.net内存模型弱于.net内存模型,.net程序可能会发现自己正在处理器中运行,其中线程2可能会看到一个不完整的对象(因为对SharedPerson的写入可能发生在对newPerson.FirstName的写入之前)。在MaybeMemoryBarrier1上添加一个内存屏障可以避免这种危险,但是无论是否实际需要,内存屏障都会降低性能。

我不认为最低要求的.net内存模型如此之弱,以至于在确保线程2在读取MaybeMemoryBarrier2本身之前从未访问过SharedPerson所引用的对象的情况下,就需要SharedPerson(就像在以上代码,因为在将新实例存储在SharedPerson中之前,新实例不会暴露于任何外部代码。另一方面,假设情况稍有变化,那么Thread 2创建了JobInfo记录,然后将其放入Thread 1队列中(假定队列本身具有所有必要的锁和内存屏障);之后,处理器执行以下操作:

//线程1
var newJob = JobQueue.GetJob();//获取由Thread2编写的JobInfo
newJob.StartTime = DateTime.Now();//八字节结构可能会跨越缓存行
//一旦写就永远不会改变
//MaybeMemoryBarrier1
CurrentJob = newJob;

//线程2
var wasJob = CurrentJob;
//MaybeMemoryBarrier2
var wasStartTime = CurrentJob.StartTime();

如果线程1有内存障碍,但线程2没有,则可以保证当线程2看到它创建的JobInfo记录出现在CurrentJob中时,它将正确读取其StartTime字段(并且不会看到已缓存或部分缓存)从Thread 2处理该对象时剩余的缓存值?

最佳答案

TL; DR:您应该只针对.net内存模型编写代码;没有更强大。

确实,x86体系结构比.net所描述的体系结构具有更强大的内存模型。

但是,即使您从未计划将代码移植到其他平台(例如ARM),也不应考虑x86内存模型。因为您的编译器和JITer可以自由进行破坏x86模型的优化。因此,即使在Intel CPU上,您也不安全。

例如,JIT可以决定在您的示例中完全避免使用newPerson局部变量,这等效于以下代码:

SharedPerson = new Person();
SharedPerson.LastName = "Simpson";
SharedPerson.FirstName = "Bart";

你看到这有多坏吗?即使使用先前初始化的SharedPerson,线程2仍可以看到FirstName和LastName == null(如果在设置它们之前先读取)!此优化是完全合法的,并且不会更改单线程行为。

如果没有适当的同步,则只要单线程行为不变,硬件和运行时就可以随意引入/消除/重新排序内存的读写。

要以原子方式发布对其他线程的引用,应使用 volatile 写入。如果SharedPerson是 volatile 的,则您的代码就可以了(不需要其他显式的内存屏障)。请注意,在x86上, volatile 写入只是常规写入,因此它是“免费的”:运行时不添加任何指令。但是它确实禁止.net运行时进行优化(上面的示例变得不合法,因为在 volatile 写操作之后没有先前的内存操作可以移动。因此,必须在 volatile 写操作发生之前分配.LastName和.FirstName)。

关于.net - 代码应该对CPU内存模型做出哪些假设,以及如何记录这些假设?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11054017/

相关文章:

c# - Windows Phone 像 Facebook 一样创建侧面菜单栏

c# - 如何自动化使用 Web 服务的任务

c# - 创建新的 .Net Core 控制台进程 : Exception when using environment variables with UseShellExecute=true

java - 在不获取新内存地址的情况下在其他线程中加载位图

c++ - 宽松的原子规则有什么(轻微)差异?

.net - 如何停止显示默认上下文菜单的文本框?

java - 锁定问题: needs some brainstorming suggestions

c# - 指令重新排序

c# - 锁语句的内存屏障

c# - 创建一个任务列表,包含任务但不执行