动态更新全局数据

想象这样一种场景,有一份全局数据在启动的时候加载,多线程并发访问这份全局数据,那么在运行过程中如何动态更新这份全局数据?


前面说到的问题,究根结底在于读写操作是并发的,不可避免的会出现冲突。例如有一张物品价格表,多个线程并发查询这张表获取物品价格,另外一个线程想更新物品价格。这种场景,我们可以抽象为单生产者-多消费者的问题。


通常的做法,就是直接重启程序一了百了,但是如果重启的代价比较大可能这种方法就不好用了。还有一种做法就是每次访问的时候加锁,可是加锁明显非常影响性能。有没有一种办法可以动态更新数据,又不能影响到性能?答案是有,我们以实际数据为例说明。


假设有一个全局物品表,用map存储。我们可以定义两个同样结构的物品表:itemsA_,itemsB_,还有一个标志表示当前用的是哪个物品表:flagA_。访问数据的时候,如果flagA_为true,那么访问的就是itemsA_,反之就是itemsB_。更新数据的时候,如果flagA_为true,更新的就是itemsB_,并且将flagA_置为false,反之同理。通过这种方式,读取和修改两种操作就不会冲突,因为他们操作的是两份不同的数据。



细心的同学会发现,代码中flagA_的类型是volatile bool。这个关键字volatile的用处在于,它告诉编译器不要对这个变量的相关代码进行优化,同时每次都要重新读取这个值,而不是去寄存器读旧的数据。如果不加volatile会有什么问题呢?假如现在flagA_是true,那么写线程会将数据更新到itemsB_,并且将flagA_置为false。这时候读线程去读数据,那么它认为flagA_是多少?答案是有可能还是true。为什么会这样,写线程不是将flagA_置为false了吗?原因是处理器为了提高效率,会将数据缓存到寄存器中,在读取数据的时候,可能只是从寄存器读旧的数据,而不是从内存中读最新的数据。所以加上volatile还是必要的,这样能保证线程每次读取的都是最新的flagA_。


当然同学会发现还有一个不同的地方,那就是在代码行63和68加了语句:MemoryBarrier()。可能有同学认为这个语句没什么用,因为从常理来说,更新items之后,再更新flag,那么读线程肯定会读到正确的数据,除非执行语句不按代码的顺序来。很不幸地恭喜你猜对了!虽然你看到了代码是这种顺序,但是实际上执行的顺序还真可能不是这样的。这里要说到的内容就是指令重排和内存屏障(MemoryBarrier)。我们知道,现代计算机,CPU的性能已经远远超过内存的性能,所以为了提高效率,CPU会将一些不会互相影响的指令同时执行。在这份代码中为了更新数据我们做了两个操作:itemsB_= items; flagA_ =false。那么这样写可能会出现的情况是,CPU判断两个语句中的指令互不干扰,因为CPU有多个处理单元,所以同时执行不冲突的指令。那么可能出现的结果是,flagA_已经变成false,但是可能itemsB_= items还没有执行完。在这个时候,读线程读取数据,因为flagA_ =false,所以获取的是itemsB_的数据。但是根据前面的描述,itemsB_还在赋值,所以问题就出现了,我们读取了一份还在赋值的数据。而MemoryBarrier()的作用就在于,让这个语句之后的指令在前面的语句执行完后才能执行。通过这个语句,就可以避免指令重排问题的出现。

猜你喜欢

转载自blog.csdn.net/windpenguin/article/details/78061111