Chapter08-用户模式下的线程同步教程
Cache Lines如果想编写一个能够在多核上高效率的程序,你就有必要理解Cache Lines.学过《操作系统》应该都知道,CPU从物理内存中读取内容的时候不是每次读取一个字节,而是读取多个字节的数据放入Cache Line之中。一个Cache Line可能是32、64或128个字节(总之是2的指数),并且它们一般都是按照32、64或128字节数对齐的。 值得注意的是,在多核上的Cache Line进行内存更新时可能会出现问题,看下面这个实例:
上面提到的情形在某种情况下,可能导致灾难性的后果。不过芯片制造者已经意识到这个问题,目前的多核系统中如果某个Cache Line修改后将会通知其他CPU去校验其Cache Line对应的内容;会强制CPU1把修改后的内容写入RAM中,然后CPU2再去RAM读取内容,重新填充对应的Cache Line。正如你所了解的,一方面Cache Line可以提高性能;另一方面它又可能降低性能。 总之,如果你想提高性能的话,你就需要将所有的数据结构聚齐在Cache Line之中。目标就是确保不同的CPU访问不同的内存地址时,它们不会分布在同一个Cache Line访问内。 structCUSTINFO { DWORDdwCustomerID; // Mostly read-only intnBalanceDue; // Read-write wchar_tszName[100]; // Mostly read-only FILETIMEftLastOrderDate; // Read-write }; 获取CPU的Cache Line的方法是:调用GetLogicalProcessorInformation函数,该函数返回一个SYSTEM_LOGICAL_PROCESSOR_INFORMATION结构体,再通过结构体内的Cache域,其指向CACHE_DESCRIPTOR结构体,在该结构体内有一个域:LineSize。一旦你有了这个域值就可以用如下相似的代码进行程序优化: [cpp] view plaincopy
高级线程同步—Critical Section我们需要一种机制:当线程等待一种可用资源时,不用浪费CPU资源。 当一个线程想去访问一个共享资源或等待某个特殊事件通知时,线程需要调用一个系统函数,并传递一个标识线程是否正在等待的参数。 如果系统侦测到资源可用或者特殊事件发生了,函数返回并且使得线程处于可调度状态;如果资源不可用或者特殊事件没有发生,系统将线程置于等待状态,使之不可调度(这样就不会浪费CPU资源了)。 看下面的一段代码: [cpp] view plaincopy
如果这两个线程单独运行,一切正常;设想在FirstThread执行过程中CPU时间片用完,SecondThread被调度到CPU上运行,这是g_nSum的值就会乱掉,最后g_nSum值是不可知的。这显然与程序编程初衷相悖。 Windows提供了一种解决这种问题的方法--CRITICAL_SECTION结构体加上几个对应的API函数: 1.VOID InitializeCriticalSection(PCRITICAL_SECTION pcs) 该函数提供CRITICAL_SECTION结构体的初始化,设置一些成员变量的值,这个函数必须在EnterCriticalSection之前调用。 2.VOID EnterCriticalSection(PCRITICAL_SECTIONpcs) 该函数检查结构体的一些成员变量,这些变量标识了哪些线程在访问资源,该函数做了一下检查:
也许你会认为:被EnterCriticalSection函数挂起的线程,可能长期处于不可调度状态,这种状态称之为饿死。但是实际上这种情况是不会发生的,因为EnterCriticalSection最后会因为超时而引起异常。 3. BOOLTryEnterCriticalSection(PCRITICAL_SECTIONpcs) 与EnterCriticalSection函数不同的是:TryEnterCriticalSection不会让线程处于等待状态,它用返回值来标示调用线程是否有权访问资源。如果资源已经被其他线程占用时,则返回FALSE;否则返回TRUE。 利用这个函数,线程可以很快检查出它是否能访问资源,如果不能则继续做其他事而不是简单的等待;如果TryEnterCriticalSection函数返回TRUE,则更新CRITICAL_SECTION结构体的成员变量来标识当前线程正在访问资源。因此每个TryEnterCriticalSection返回TRUE之后就得再调用LeaveCriticalSection。 4. VOIDLeaveCriticalSection(PCRITICAL_SECTION pcs) 该函数检查CRITICAL_SECTION结构体内的成员变量,并将标识当前线程被授权访问资源次数的计数器减一。如果计数器还是大于0,则函数只是简单返回即可;如果计数器为0,则更改结构体内的成员变量来标示没有线程在占用资源,再查看是否有线程在调用EnterCriticalSection,如果有再选中一个线程使之处于可调度状态。
Critical Sections and Spinlocks
当一个线程尝试进入被其他线程占用的临界区域,当前线程会立即被置于等待状态,这意味着线程必须从用户模式(User mode)转化为内核模式(Kernelmode)(耗时大概1000CPU周期),这种转换的代价是非常昂贵的。在一个多核系统中,在当前线程完成从用户模式到内核模式转化完成之前,占用资源的线程可能就已经释放资源了,在这种情况下,大量的CPU时间片被浪费了。 为了提高临界区域的性能,微软引进了自旋锁(spinlock),当EnterCriticalSection函数被调用时,它用循环使用自旋锁尝试在一定次数内获取到资源,只有所有的尝试都失败了,线程才进入内核模式处于等待状态。 两个相关函数: 1.BOOLInitializeCriticalSectionAndSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount); 第一个参数和InitializeCriticalSection的参数一样,第二个参数表示的是线程自旋锁尝试的次数。 2.DWORD SetCriticalSectionSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount); 该函数改变自旋锁尝试的次数。 PS:dwSpinCount的推荐值是4000。
Slim Reader-Writer Locks
该系列函数是源于典型的读者-写者问题,一次可以有多个读者读,但一次只能一个写者写。它与Critical Section(临界区域)不同的是:它允许你去区分哪些线程是简单地读取数据,哪些线程是要修改数据;它允许多个线程同时读取数据。 该系列有一个结构体和三个相应的函数: [cpp] view plaincopy
AcquireSRQLockShared和ReleaseSRWLockShared函数的功能就是获取和释放读者权限, AcquireSRWLockExclusive和ReleaseSRWLockExclusive函数就是获取和释放写者权限。
Volatile read &Volatile write:(这一点说的有问题,建议不要用volatile关键字来做线程同步)
是最简单直接一种共享数据的办法,其不是靠编程去实现的,而是利用volatile关键字,告诉编译器不要保留该关键字修饰的数据的临时拷贝,更新后要立即回写到内存。
interlocked APIsInterlocked API系列如下,该系列函数保证所有第一个参数指定的内存地址内的数据执行的都是原子操作。具体的函数使用我就不再赘述,查MSDN可知。 [cpp] view plaincopy
|