1.内存屏障
1.内存屏障
1).内存屏障简介 内存屏障是CPU和编译器的"交通信号灯",用来阻止指令乱序执行,强制缓存数据同步,解决多线程下的可见性和有序性
2).为什么需要内存屏障 我们写代码时,默认指令是按顺序执行的,但CPU和编译器为了提升效率,会执行以下操作 a.指令重排 编译器/CPU会打乱指令的执行顺序(只要不影响单线程逻辑),比如你写了A=1,B=2,CPU可能先执行B=2,再执行A=1单线程不会出现问题,但多线程会出大问题 b.缓存不一致 每个CPU核心都有自己的缓存(速度比内存快N倍),一个核心修改了变量(比如把isLoaded=true写到缓存),另一个核心可 能还在读缓存里的旧值(isLoad=false),导致"明明写了, 另一个线程却看不到"
// 线程1:加载资源并标记状态boolisLoaded=false;Objectresource=LoadBigResource();// 耗时操作isLoaded=true;// 线程2:检测状态并使用资源while(!isLoaded){}UseResource(resource);// 理论上isLoaded=true时,resource一定加载好了?
实际运行中,CPU可能对线程1做指令重排,先执行isLoaded=true,再执行LoadBigResource();结果就是线程2看到 isLoaded=true,直接执行UseResource,但此时加载资源未完成,会出现报错
3).内存屏障的两个作用 a.禁止指令重排 屏障前的指令必须全部执行完,才能执行屏障后的指令;比如再LoadingResource()和isLoaded=true之间加屏障,CPU就 不会把isLoaded=true提前执行 b.强制缓存同步 屏障会让核心把缓存里的修改刷写到内存(写屏障)或从内存重新读取最新数据(读屏障),让所有核心看到的变量值是一致的 比如线程1加写屏障,isLoaded=true会立刻同步到内存;线程2加读屏障,会放弃自己的旧屏障,去内存读最新的isLoaded
4).常见的内存屏障类型 a.写屏障(StoreBarrier):核心是写后同步,保证屏幕前的所有写操作,都刷到内存,让其核心可见 b.读屏障(LoadBarrier):核心是读前刷新,保证屏幕后的所有读操作,都从内存读取最新值,不是缓存旧值 c.全屏障(FullBarrier):同时禁止读写重排,强制读写都同步
5).在C#/Unity里,怎么用内存屏障 a.volatile关键字 给变量加volatile,相当于给这个变量的读写加了轻量级内存屏障:禁止指令重排,强制读写走内存而非缓存volatileboolisLoaded=false;// 加volatile
b.Thread.MemoryBarrier()手动插入全屏障,适合需要精确控制的场景Objectresource=LoadBigResource();Thread.MemoryBarrier();// 屏障:前面的写操作必须完成并同步isLoaded=true;
c.原子操作(Interlocked类)Interlocked.Increment/CompareExchange等方法,底层都带内存屏障,既保证原子性,又保证可见性和有序性
d.锁(lock)lock的Enter和Exit时,会自动插入全屏障,所以用lock的代码天然没有内存屏障问题(但锁的开销比volatile大)
-内存屏障:只解决可见性、有序性,不保证原子性(比如i++这种非原子操作,加屏障也会有线程安全问题)-锁/原子操作:既保证原子性,又隐含内存屏障的功能