【Windows核心编程学习笔记】用户模式下的线程同步之一---Interlocked系列函数及高速缓存行
在下面两种基本情况下,线程之间需要相互通信:
(1)需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性。
(2)一个线程需要通知其他线程某项任务已经完成。
一、下面介绍线程同步内容之一---原子访问:Interlocked系列函数
线程同步的一大部分与原子访问(atomic access)有关。所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。
下面从一段简单的代码说起。
LONGLONG InterlockedAnd64(LONGLONG* Destination, LONGLONG value) { LONGLONG old = *Destination; do { old = *Destination; } while(InterlockedCompareExchange64(Destination, old&value, old) != old); return old;}
如果只需要以原子方式修改一个值,那么interlocked系列函数非常好用。我们当然应该优先使用它们。但大多数实际的编程问题需要处理的数据结构往往要比一个简单的32为值或64位值复杂得多。为了能够以原子方式访问复杂数据结构,我们必须超越interlocked函数,转而使用Windows提供的一些其他特性。
前面提到了旋转锁要谨慎使用,是因为它会浪费CPU时间。我们需要一种机制,既能够让线程等待共享资源的访问权,又不会浪费CPU时间。
二、高速缓存行
如果要为装配多处理器的机器构建高性能应用程序,那么应该注意高速缓存行。当CPU从内存中读取一个字节的时候,它并不是从内存中取回一个字节,而是取回一个高速缓存换行。高速缓存行可能是32字节(老式CPU),64字节,甚至是128字节()取决于CPU。高速缓存行的目的是提高性能。一般来说,应用程序会对一组相邻的字节进行操作。如果所有字节都在高速缓存行中,那么CPU就不必访问内存总线,后者耗费的时间比前者耗费的时间多得多。
但是,在多处理器环境中,高速缓存行是的对内存的更新变得更加困难。
(1)CPU1读取一个字节,这使得该字节和与他相邻的字节被读到CPU1的高速缓存行中。
(2)CPU2读取一个字节,这使得该字节被读到CPU2的高速缓存行中。
(3)CPU1对该字节进行修改,是的该字节被写入到CPU1的高速缓存行中,但这一信息还没被写回到内存中。
(4)CPU2再次读取同一个字节。由于该字节已经在CPU2的高速缓存行中,因此CPU2不需要再访问内存。但CPU2将无法看到该字节在内存中新的值。
这种情况十分糟糕。CPU芯片设计者做了专门的设计来处理这个问题:当一个CPU修改了高速缓存行中的一个字节时,机器中的其他CPU会收到通知,并使自己的高速缓存行作废。于是在上述第四步中,CPU1必须将它的高速缓存行写回到内存中,CPU2必须重新访问内存来填满它的高速缓存行。可以看出,损失了性能。
因此,我们应该根据高速缓存行的大小将应用程序的数据组织在一起,并将数据与缓存行的边界对齐。这样做的目的是确保不同的CPU能够各自访问不同的内存地址,而且这些地址不在同一个高速缓存行中。此外,我们应该把只读数据与可读写数据分别存放。我们还应该把差不多会在同一时间访问的数据组织在一起。