Windows高级编程之用户方式中线程的同步

线程很少能够在所有的时间都独立地进行操作。通常情况下,要生成一些线程来处理某个任务。当这个任务完成时,另一个线程必须了解这个情况。 系统中的所有线程都必须拥有对各种系统资源的访问权,这些资源包括内存堆栈,串口,文件,窗口和许多其他资源。如果一个线程需要独占对资源的访问权,那么其他线程就无法完成它们的工作。反过来说,也不能让任何一个线程在任何时间都能访问所有的资源。 线程需要在下面两种情况下互相进行通信: • 当有多个线程访问共享资源而不使资源被破坏时。 • 当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时 要搞好线程的同步,唯一的办法是通过实践。 8.1 原子访问:互锁的函数家族 所谓原子访问,是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。 LONG InterlockedExchangeAdd(PLONG plAddend,LONG lIncrement); LONG InterlockedExchange(PLONG plTarget,LONG lValue); PVOID InterlockedExchangePointer(PVOID *ppvTarget,PVOID pvValue); InterlockedExchange和InterlockedExchangePointer能够以原子操作方式用第二个参数中传递的值来取代第一个参数中传递的当前值,两个函数都返回原始值。 8.2 高速缓存行 如果想创建一个能够在多处理器计算机上运行的高性能应用程序,必须懂得 C P U的高速缓存行。当一个C P U从内存读取一个字节时,它不只是取出一个字节,它要取出足够的字节来填入高速缓存行。高速缓存行由3 2或6 4个字节组成(视C P U而定) ,并且始终在第3 2个字节或第6 4个字节的边界上对齐。高速缓存行的作用是为了提高C P U运行的性能。通常情况下,应用程序只能对一组相邻的字节进行处理。如果这些字节在高速缓存中,那么 C P U就不必访问内存总线,而访问内存总线需要多得多的时间。 当一个C P U修改高速缓存行中的字节时,计算机中的其他C P U会被告知这个情况,它们的高速缓存行将变为无效。 应该将高速缓存行存储块中的和高速缓存行边界上的应用程序数据组合在一起。 这样做的目的是确保不同的C P U能够访问至少由高速缓存行边界分开的不同的内存地址。还有,应该将只读数据(或不常读的数据)与读写数据分开。同时,应该将同一时间访问的数据组合在一起。 8.3 高级线程同步 当线程想要访问共享资源,或者得到关于某个“特殊事件”的通知时,该线程必须调用一个操作系统函数,给它传递一些参数,以指明该线程正在等待什么。如果操作系统发现资源可供使用,或者该特殊事件已经发生,那么函数就返回,同时该线程保持可调度状态(该线程可以不必立即执行,它处于可调度状态) 。 如果资源不能使用,或者特殊事件还没有发生,那么系统便使该线程处于等待状态,使该线程无法调度。这可以防止线程浪费 C P U时间。当线程处于等待状态时,系统作为一个代理,代表你的线程来执行操作。系统能够记住你的线程需要什么,当资源可供使用的时候,便自动使该线程退出等待状态,该线程的运行将与特殊事件实现同步。 8.4 关键代码段 关键代码段是指一个小代码段,在代码能够执行前,它必须独占对某些共享资源的访问权。这是让若干行代码能够“以原子操作方式”来使用资源的一种方法。所谓原子操作方式,是指该代码知道没有别的线程要访问该资源。当然,系统仍然能够抑制你的线程的运行,而抢先安排其他线程的运行。不过,在线程退出关键代码段之前,系统将不给想要访问相同资源的其他任何线程进行调度。 EnterCriticalSection(CRITICAL_SECTION *pSect); LeaveCriticalSection(CRITICAL_SECTION *pSect); 编写的需要使用共享资源的任何代码都必须封装在EnterCriticalSection和LeaveCriticalSection函数中。关键代码段的优点在于它们的使用非常容易,它们在内部使用互锁函数,这样它们就能够迅速运行。关键代码的主要缺点是无法用它们对多个进程中的各个线程进行同步。 8.4.1 关键代码段准确的描述 通常情况下,CRITICAL_SECTION结构可以作为全局变量来分配,这样,进程中的所有线程就能够很容易地按照变量名来引用该结构。但是, CRITICAL_SECTION结构也可以作为局部变量来分配,或者从堆栈动态地进行分配。它只有两个要求,第一个要求是,需要访问该资源的所有线程都必须知道负责保护资源的CRITICAL_SECTION结构的地址,你可以使用你喜欢的任何机制来获得这些线程的这个地址;第二个要求是,CRITICAL_SECTION结构中的成员应该在任何线程试图访问被保护的资源之前初始化。该结构通过调用下面的函数来进行初始化: VOID InitializeCriticalSection(PCRITICAL_SECTION pcs); 该函数用于对(pcs指向的)CRITICAL_SECTION结构的各个成员进行初始化。由于该函数只是设置了某些成员变量。因此它的运行不会失败,并且它的原型采用了VOID的返回值。该函数必须在任何线程调用EnterCriticalSection函数之前被调用。Platform SDK的文档清楚地说明,如果一个线程试图进入一个未初始化的CRITICAL_SECTION,那么结果将是很难预计的。 VOID DeleteCriticalSection(PCRITICAL_SECTION pcs); DeleteCriticalSection用于对该结构中的成员变量进行删除。 VOID EnterCriticalSection(PCRITICAL_SECTION pcs); EnterCriticalSection负责查看该结构中的成员变量。这些变量用于指明当前是哪个变量正在访问该资源。EnterCriticalSection负责进行下列测试: • 如果没有线程访问该资源,EnterCriticalSection便更新成员变量,以指明调用线程已被赋予访问权并立即返回,使该线程能够继续运行(访问该资源) 。 • 如果成员变量指明,调用线程已经被赋予对资源的访问权,那么EnterCriticalSection便更新这些变量,以指明调用线程多少次被赋予访问权并立即返回,使该线程能够继续运行。这种情况很少出现,并且只有当线程在一行中两次调用EnterCriticalSection而不影响对LeaveCriticalSection的调用时,才会出现这种情况。 • 如果成员变量指明,一个线程(除了调用线程之外)已被赋予对资源的访问权,那么EnterCriticalSection将调用线程置于等待状态。这种情况是极好的,因为等待的线程不会浪 费任 何 C P U 时 间。 系 统能 够记 住 该线 程想 要 访问 该资 源并 且 自动 更新CRITICAL_SECTION的成员变量,一旦目前访问该资源的线程调用LeaveCriticalSection函数,该线程就处于可调度状态。 BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs); TryEnterCriticalSection函数决不允许调用线程进入等待状态。相反,它的返回值能够指明调用线程是否能够获得对资源的访问权。因此,如果TryEnterCriticalSection发现该资源已经被另一个线程访问,它就返回FA L S E。在其他所有情况下,它均返回T R U E。 运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行某些其他操作,而不必进行等待。如果TryEnterCriticalSection函数确实返回了T R U E,那么CRITICAL_SECTION的成员变量已经更新,以便反映出该线程正在访问该资源。因此,对返回T R U E的TryEnterCriticalSection函数的每次调用都必须与对LeaveCriticalSection函数的调用相匹配。 VOID LeaveCriticalSection(PCRITICAL_SECTION pcs); LeaveCriticalSectionn要查看该结构中的成员变量。该函数每次计数时要递减1,以指明调用线程多少次被赋予对共享资源的访问权。如果该计数大于0,那么LeaveCriticalSection不做其他任何操作,只是返回而已。 如果该计数变为0,它就要查看在调用EnterCriticalSection中是否有别的线程正在等待。如果至少有一个线程正在等待,它就更新成员变量,并使等待线程中的一个线程( “公正地”选定)再次处于可调度状态。如果没有线程正在等待,LeaveCriticalSectionn函数就更新成员变量,以指明没有线程正在访问该资源。 与LeaveCriticalSectionn函数一样,LeaveCriticalSectionn函数也能以原子操作方式执行所有这些测试和更新。不过,LeaveCriticalSectionn从来不使线程进入等待状态,它总是立即返回。 8.4.2 关键代码段与循环锁 当线程试图进入另一个线程拥有的关键代码段时,调用线程就立即被置于等待状态。多处理器计算机上,很浪费CPU时间。 为了提高关键代码段的运行性能, Microsoft将循环锁纳入了这些代码段。当EnterCriticalSection函数被调用时,它就使用循环锁进行循环,以便设法多次取得该资源。只有当为了取得该资源的每次试图都失败时,该线程才转入内核方式,以便进入等待状态。 BOOL InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount); 参数1是关键代码段结构的地址,参数2-dwSpinCount传递的是在使线程等待之前它试图获得资源时想要循环锁循环迭代的次数。。这个值可以是0至0x00FFFFFF之间的任何数字。 如果在单处理器计算机上运行时调用该函数, d w S p i n C o u n t参数将被忽略,它的计数始终被置为0。因为在单处理器计算机上设置循环次数是毫无用处的。 DWORD SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount); 作为一个指导原则,保护对进程的堆栈进行访问的关键代码段使用的循环次数是 4 0 0 0次。 8 .4.3 关键代码段与错误处理 InitializeCriticalSection函数的运行可能失败,但原型设计为VOID。使用InitializeCriticalSectionAndSpinCount跟踪错误码。 从内部来说,如果两个或多个线程同时争用关键代码段,那么关键代码段将使用一个事件内核对象。在初次需要之前,系统将不创建事件内核对象。这可以节省大量的系统资源,因为大多数关键代码段从来不被争用。可以使用结构化异常处理方法来跟踪错误。当错误发生时,既可以不访问关键代码段保护的资源,也可以等待某些内存变成可用状态,然后再次调用EnterCriticalSection函数。 8.4.4 非常有用的提示和技巧 1. 每个共享资源使用一个CRITICAL_SECTION变量 如果应用程序中拥有若干个互不相干的数据结构,应该为每个数据结构创建一个CRITICAL_SECTION变量。这比只有单个CRITICAL_SECTION结构来保护对所有共享资源的访问要好。 2. 同时访问多个资源 有时需要同时访问两个资源。必须始终按照完全相同的顺序请求对资源的访问(即调用EnterCriticalSection)。注意,当调用LeaveCriticalSection函数时,按照什么顺序访问资源是没有关系的,因为该函数决不会使线程进入等待状态。 3. 不要长时间运行关键代码段 当一个关键代码段长时间运行时,其他线程就会进入等待状态,这会降低应用程序的运行性能。 无法确定窗口处理过程WM_SOMEMSG消息话费多长时间。最好写成如下形式 这个代码将该值保存在临时变量sTemp中。也许你能够猜到CPU需要多长时间来执行这行代码—只需要几个CPU周期。当该临时变量保存后,LeaveCriticalSection函数就立即被调用,因为这个全局结构不再需要保护。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值