我要投搞

标签云

收藏小站

爱尚经典语录、名言、句子、散文、日志、唯美图片

当前位置:港彩神鹰 > 读入原语 >

Windows Vista 新增的同步原语

归档日期:04-17       文本归类:读入原语      文章编辑:爱尚语录

  条件变量在其他线程库中已经存在一段时间了,只是很遗憾地被 Windows SDK 遗漏了。条件变量主要用来根据一些条件测试的结果同步化一组线程。尽管可通过使用现有同步结构组合来做到这一点,但是条件变量具有释放已获取的锁,并通过一个原子操作进入休眠状态的能力。它还提供了一种更清楚且又少出错的方法,以用于实现所需的行为。Windows SDK for Windows Vista(可供下载)公开了 CONDITION_VARIABLE 结构的条件变量。您可用 InitializeConditionVariable 函数创建该结构。没有用来清理或销毁 CONDITION_VARIABLE 结构的函数,因为底层实现用不着它。通过使用函数 SleepConditionVariableCS(使用关键节时)或 SleepConditionVariableSRW(使用 Slim 读取器锁/写入器锁时),您可让线程等待条件变量。当另一条线程调用 WakeConditionVariable 或 WakeAllConditionVariable 时,这些休眠线程将被释放,这取决于调用线程是想要释放等待条件变量的一条线程还是所有线程。常见的生产者/使用者问题代表了可使用条件变量的情况。这一典型示例是指生产者生成数据并将其置入缓冲区,而使用者则从缓冲区抓取待处理的数据片段的情形。该问题指出了一种需求,即保证生产者不会试图向填满的缓冲区添加数据,而使用者不会试图从空缓冲区抓取数据。我们将分析本情形,以向您说明条件变量是如何帮助解决问题的。针对此例,我们将创建一个向共享队列传送数值数据的单一生产者线程。我们然后创建五个使用者线程。每个使用者线程将从队列中转移一个项目并进行处理。当处理完当前数据段后,使用者线程将循环,无限重复该过程。在早期版本的 Windows 中,可用 Win32 事件和关键节组合来解决生产者/使用者问题。当资源可供使用者使用时,关键节会保护共享资源、避免出现并发性访问和事件信号。在我们首次尝试解决这个问题时,我们将标准模板库 (Standard Template Library, STL) 整数列表用作共享资源。由于列表会动态扩展,我们无需使用事件以信号形式通知列表何时是未填满的,我们只需了解它何时不是空的,这样使用者就能知道其中有内容可供使用。(如果您打算使用固定大小的数组来容纳共享队列,则需要一个未满事件,以确保您不会向缓冲区写入过多内容。)我们随后声明并初始化 CRITICAL_SECTION 对象以及用于说明列表何时不为空的自动重置事件。所示的生产者线程将首先尝试获取关键节,并且如果获取成功,将随后在共享列表的末尾插入一个整数值。该线程然后释放关键节并设置非空事件。因为我们在使用一个自动重置事件,因此只释放一个等待本事件的线所示的使用者线程将查看队列是否为空。如果队列不为空,该线程将转移一个项目并释放关键节。如果队列为空,使用者线程将返回休眠状态,继续等待非空事件。在第一个使用者线程忙于处理它从队列转移的项目时,生产者将唤醒另一个使用者线程拾取下一段工作以确保队列处于移动状态。

  unsigned _stdcall ProducerFunc(void *pParam) { for (unsigned int i = 0;i g_uiIterations;i++) { EnterCriticalSection( // Produce work g_listWork.push_back(i++); LeaveCriticalSection( SetEvent(g_hNotEmpty); Sleep(g_uiProducerDelay); // Simulating work } return 0; }while (true) { EnterCriticalSection( if (g_listWork.empty()) { LeaveCriticalSection( WaitForSingleObject(g_hNotEmpty,INFINITE); } else { i = g_listWork.front(); g_listWork.pop_front(); LeaveCriticalSection( wcout LThread iThread L Consumed: i endl; Sleep(g_uiConsumerDelay); // Simulating work } }尽管本解决方案通常是可行的,但对本实现也有一些局限。比如说,正确初始化同步对象就是个难题。例如,我们必须决定在将数据推入列表时,生产者应该只唤醒一个使用者还是唤醒所有使用者。这可通过我们初始化事件的方法来控制。如果我们使用自动重置事件,就只能释放一个线程。但如果我们想要唤醒所有线程,我们将使用手动重置事件,并且必须记住在正确的时间调用 ResetEvent。必须确保当队列为空时,使用者线程在等待非空事件之前释放关键节。我们还必须确保不要错误使用生产者线程中的 PulseEvent 来发送非空事件信号,因为这样会导致争用状况。如果在使用者线程刚刚释放关键节、尚未调用 WaitForSingleObject 时被抢占,并且生产者随即调用 PulseEvent,就会出现这个问题。PulseEvent 并非“粘滞的”(sticky),它将只释放目前正在等待事件的那些线程。当被抢占的线程恢复时,将不会发送事件信号,并且将丢失唤醒。使用条件变量将更容易获得正确的解决方案。本方法仍然使用关键节,但它用条件变量取代非空事件。我们通过调用指向我们的 CONDITION_VARIABLE 结构的 InitializeConditionVariable,来初始化主函数内的条件变量。所示使用者线程进入关键节并查看队列是否为空。如果为空,则调用 SleepConditionVariableCS。此函数会释放关键节,并通过一个原子操作让此线程进入休眠状态,从而避免在两个任务间隙使用者被抢占时可能出现的争用情况。SleepConditionVariableCS 函数还接受一个超时参数,从而让我们在不想等得太久时,可以做别的工作。

  读取器锁/写入器锁用于保护您想允许多个读取器并发访问,但在更新时只允许写入器访问的一段数据。通常,这些锁最适用于需要频繁读取和更新数据的情形。恰当使用读取器锁/写入器锁有利于增加可伸缩性。读取器锁/写入器锁出现已有一段时间了。但在 Windows Vista 发布之前,如果您在 Windows 中编写本机用户模式代码并且需要读取器锁/写入器锁,您唯一的选择是编写自己的锁或修改教科书上的实现。Windows Vista 包含了一种称为 Slim 读取器锁/写入器锁 (SRW) 的读取器锁/写入器锁。让我们来看看这个新同步原语的功能集,测试一下可用来使用它的 API,并与用 Win32SRW 锁的设计初衷是快速、小型(如其名),同时仍能保持使用时的资源效率。它以 Windows 内核键控事件机制为基础生成,引入此机制是为了解决应用程序使用大量 CRITICAL_SECTION 对象时可能发生的资源匮乏问题。有些读取器锁/写入器锁旨在使读取器优先于写入器,反之亦然。但 SRW 锁旨在不偏向任何一方。也就是说,如果您的应用程序要求数据更新的优先级高于数据读取,则最好考虑选用偏向写入器的另一种读取器锁/写入器锁。但在编写自己的锁之前,您最好先试试 SRW 锁,看看它在您的应用程序环境中表现如何。读取器锁/写入器锁有时支持的其他两项功能分别是递归获得锁,以及升级(或降级)线程被授予的锁访问权。首先,关于递归获得:如果您为应用程序设计的锁策略要求递归获得同步目标,很可能出现一个红色标志,提示您重新检查锁策略以清除递归。这是我们的看法,是由于多次执行锁获得和释放代码所带来的额外系统开销而造成的,或许还有一个更重要的原因,那就是确保锁释放和锁获得之间的平衡通常是很难证明是正确的。SRW 锁不支持递归获得。此支持会造成额外系统开销,原因是为了维持准确性需进行逐线程的计数。SRW 锁也不支持从共享访问升级到独占访问,反之也不支持从独占访问降级(较少见)到共享访问。支持升级能力可能会造成难以接受的复杂性和额外系统开销,这种开销甚至会影响锁内共享和独占获得代码的常见情况。它还要求定义关于如何选择等待中的读取器、等待中的写入器和等待升级的读取器的策略,这又将与无偏向的基本设计目标相抵触。使用 Slim 读取器锁/写入器锁的第一步是声明 SRWLOCK 结构并用 InitializeSRWLock 进行初始化:VOID WINAPI InitializeSRWLock(PSRWLOCK SRWLock);SRW 锁摆脱了每个对象均有一个初始化和清理函数的 Win32 惯例模式。当您结束使用 SRW 锁时,无需调用清理函数。VOID WINAPI AcquireSRWLockExclusive(PSRWLOCK SRWLock); VOID WINAPI ReleaseSRWLockExclusive(PSRWLOCK SRWLock); VOID WINAPI AcquireSRWLockShared(PSRWLOCK SRWLock); VOID WINAPI ReleaseSRWLockShared(PSRWLOCK SRWLock);顾名思义,AcquireSRWLockExclusive 函数用于获得供调用方独占访问的锁。一旦被授予独占访问权,所有要求任一访问类型的其他线程均将被阻止,直到用 ReleaseSRWLockExclusive 互补函数释放该锁。相对的,AcquireSRWLockShared 会以共享访问权获得该锁。此时,如果锁是无主的或者已被其他具有共享访问权的线程获得,同样要求共享访问的其他线程则无需等待。Slim 读取器锁/写入器锁可与使用 SleepConditionVariableSRW 函数的条件变量结合使用:BOOL WINAPI SleepConditionVariableSRW( PCONDITION_VARIABLE ConditionVariable, PSRWLOCK SRWLock, DWORD dwMilliseconds, ULONG Flags );SleepConditionVariableSRW 释放 SRW 锁,并作为原子操作等待特定条件变量。在发送条件变量信号或 dwMilliseconds 内指定的超时结束之前,此函数不会返回。如果 dwMilliseconds 为 INFINITE,该函数将永不超时。如果 Flags 参数指定 CONDITION_VARIABLE_LOCKMODE_SHARED,此函数认为 SRW 锁具有共享访问权;否则将认为是独占访问,并在成功返回后,该锁将以指定的访问权被重新获得。在试验这些 API 的过程中,我们发现了一些编码时需格外注意的问题。请注意,没有任何锁获得或释放 API 被定义为返回结果的类型。如果 SRW 锁当前没有为一个线程所有,并发生了对 ReleaseSRWLockShared 或 ReleaseSRWLockExclusive 的调用,将会引发一个 STATUS_RESOURCE_NOT_OWNED 结构的异常。这并非坏事,因为开发人员可以明显地看出错误。我们以前提到,不支持递归锁获得。再次尝试递归获得独占访问权的结果是,对 AcquireSRWExclusive 的第二次调用将永不返回,原因是它将自动发生死锁,使线程被阻止在内部,只有等待锁被释放。您可能需要附加一个调试器到此过程中,以查看具体情形并了解到底发生了什么问题。如果另一个线程已经尝试在两次 AcquireSRWShared 调用间隙以独占方式获得锁,那么从已获得共享访问权的线程调用 AcquireSRWShared 也将导致死锁。在确保使用正确的获得与释放函数对时也要格外注意 — 错误地将 AcquireSRWLockExclusive 与 ReleaseSRWLockShared 配对(或反之亦然)不会引发任何异常。如果已引发针对这些出错情形的异常将很有帮助,但检测错误可能会产生不必要的资源或性能开销。本文中的示例代码包括称为 ReaderWriterExample 的程序源代码,它允许针对不同读取器锁/写入器锁实现进行试验。该程序支持的锁类型是 SRW 锁、我们实现的读取器锁/写入器锁的两个变量和关键节。自定义读取器锁/写入器锁是用 Win32 关键节生成的,以保护锁的数据、向等待中的读取器发送信号的事件和用于写入器的信号灯。两种自定义锁的区别在于,一种偏向写入器,另一种既不偏向读取器也不偏向写入器。所有这些新奇的测试都是在双核 Intel Xeon 3.20 GHz 处理器系统、64 位的 Windows Vista 平台上进行的。为进行这些测试,系统被配置成禁用超线 包含每个线 个迭代,其中有锁时所做的工作最少。因此,结果可反映出锁获得和释放的实际消耗。总结的结果如

  其中有一些有趣的发现。首先,在只使用一个读取器线程或一个写入器线程(结果的前两列)时,未出现锁竞争,并且 SRW 锁的性能非常接近关键节的性能。在使用四个写入器线程时,所有线程都竞争对锁的独占访问权,SRW 锁的用时仅约占关键节方法所用时间的一半。使用独占模式锁的 SRW 锁的性能似乎优于关键节,并且值得考虑用作替代品。请注意,在标有 2 个读取器/2 个写入器和 3 个读取器/1 个写入器的栏中,SRW 锁比使用关键节的锁速度快很多。这说明了允许数据读取器并行工作的读取器锁/写入器锁的优点。

  关于我们自己开发的读取器锁/写入器锁,我们该作何评价呢?与两个内置 Windows 锁相比,它们的性能似乎很差,尤其是在读取器和写入器争用的重负载下运行时。但请看看 2 个读取器/2 个写入器和 3 个读取器/1 个写入器的结果。请注意,锁策略确实会影响总体行为和性能。偏向写入器的锁速度慢于既不偏向读取器也不偏向写入器的锁。这是因为在写入器等待更新时,并行机制被迫向写入器提供优先权。当然,这么做通常是为了确保读取器能看到最新数据,因此使用偏向写入器的锁在本例中是正确的选择。

  当四个写入器竞争锁时,会出现自定义锁的另一个问题。在出现对独占访问权的许多竞争时,性能为何变得如此之差?问题在于每个线程都在竞争保护内部数据的关键节的访问权。对关键节的竞争导致进入内核模式的行程,线程在其中被置于事件休眠状态。一旦进入关键节,就会检查锁的状态,以查看任何读取器是否具有共享访问权或另一个写入器是否已经具有独占访问权。在任何一种情况下,想得到独占访问权的线程必须离开关键节,然后等待信号灯。要线程具有独占访问权并可继续运行时,将发送上述信号灯。如您可看到的,必须频繁等待多个同步目标以获得独占访问权确实降低了性能。

  ReaderWriterExample 应用程序包括多个命令行开关,这样就可定制它的行为以允许通过使用不同类型的读取器锁/写入器锁来试验不同的方案。受锁保护的数据属于简单的 LONG 类型。然而,应用程序在有锁时允许指定待完成的额外数量的工作,以便模拟访问或更新更复杂的数据结构。可为读取器和写入器单独指定额外工作参数,从而允许数据结构模拟,该结构会因为更新而不是读取而带来更多负担。还有用来指定读取器/写入器在每个锁访问间隙的工作量的参数。通过固定执行计算的循环来模拟工作。

  我们都知道,自己开发的锁在读取器和写入器频繁争用而处于重负载下的性能不是很好。另外,SRW 锁在第一个示例中的所有情况下则都运行良好。这是否意味着既然有了 SRW 锁,您便没必要再构建自己的读取器锁/写入器锁呢?不一定。我们来看看,将锁用于更接近应用程序实际应用方案时会发生什么。

  在这些方案中,读取器的共享访问次数是 1,000,000,而写入器的独占访问次数是 150,000。这与我们前面所说的相符 — 共享访问对独占访问的比例较高时,读取器锁/写入器锁才有意义。另外,读取器拥有对 2000 个工作单元的锁以模拟读取请求,而写入器则拥有 3000 个工作单元的锁以模拟升级数据所带来的额外成本。一个读取器线程在每次访问锁的间隙执行 100 个工作单元,这样访问的时间间隔较短,而写入器在尝试独占访问之前执行 10,000 个工作单元,这样更新的时间间隔就会延长。本方法可减少对锁的整体争用。

  我们使用 2 个读取器/ 2 个写入器和 3 个读取器/1 个写入器来进行本测试。总结的结果如

  所示。表中的数字是每个线程完成其工作所需的时间(以秒为单位)。对于读取器线程,第二个数字是读取器观察到的数据更新次数。

  Figure 5 在 3 个读取器/1 个写入器方案中测试更高速的读取至更新(结果用秒表示)

  这些结果显示我们自己开发的锁和 SRW 锁存在更多的相似性。请注意不同的策略是如何影响结果的。偏向写入器的锁会让读取器等待而允许写入器访问,这意味着更新被赋予更高的优先级并相应地允许读取器看到更多更新。在应用程序中赋予更新优先权有时非常重要,因此了解预期负载下的锁策略及其性能特征是需要考虑的重要因素。

  SRW 锁是 Windows 平台上杰出的新同步原语。Windows 首次向本机系统程序员提供内置读取器锁/写入器锁。它在许多不同的环境下均运行良好,并应成为您使用的首选读取器锁/写入器锁。您的应用程序有时可受益于不同的锁策略,但正如我们所演示的,建立适用于许多方案的读取器锁/写入器锁并不那么容易。

  在建立多线程系统时,时不时会出现如何确保正确初始化供多个线程共享的对象或资源的问题。C 和 C++ 并未提供解决这一问题的帮助,原因是语言标准未提及多线程支持。假设这样一个示例,该示例包含一个用于记录消息日志的 Logger 对象实例,其中一个要求是按需进行对象实例化,而不是在开始执行程序时创建对象。当有多个线程在 GetLogger 函数内部同时执行以访问我们系统中的 Logger 对象时,

  Logger* GetLogger() { // C++ will execute the Logger’s constructor // only the first time this method is called. static Logger logger; return }

  Logger* GetLogger() { static Logger* pLogger = 0; EnterCriticalSection( if (pLogger == 0) { try { pLogger = new Logger(); } catch (...) { // Something went wrong. } } LeaveCriticalSection( return pLogger; }

  静态 Logger 对象的初始化实际上不止发生一次,原因是编译器并未在其结构上加入任何同步。这可能会产生损坏的 Logger 对象,当使用该对象时,即使处于最佳情形,也会产生异常 — 但不能保证错误很明显,并且系统可能运行了很长时间后才会有人发现此问题。解决此问题的一种办法是重新编写函数并引入同步功能,这样它就可利用

  现在,进入 GetLogger 函数的每个线程都将试图进入关键节,即意味着每次只允许一个线程进入受保护的代码块。在关键节执行的线程会检查 pLogger 的值,只有值为 NULL 时才会创建一个 Logger 对象的实例。此操作仅在第一个线程进入关键节后会发生一次。随后进入的其他所有线程将发现 pLogger 不是 NULL,然后会退出关键节而不再执行任何工作。到达返回语句时,pLogger 值将是非空值,并可被返回给调用方。线程安全初始化代码表面上看似合理的解决方案。但在此必须打个折扣,尤其是当许多线程结束时同时调用 GetLogger 函数的情况下。一旦第一个线程已经完成分配、构建和设置 pLogger 指针,实际上根本不再需要后面的线程进入关键节对象。该指针始终保持有效。这种认识带来了一个称为仔细查过锁定模式的 C++ 编程设计模式。显示了使用仔细查过锁定模式的 GetLogger 实现。此模式规定 pLogger 变量要被检查两次。第一次是检查变量,在关键节之外进行。如果发现 pLogger 为 NULL,线程随后将进入关键节。一旦进入,它将在实例化和设置 pLogger 变量之前再次检查 pLogger 是否为空,原因是等待关键节时由于受另一线程的竞争,该线程可能被阻止。

  Logger* GetLogger() { volatile static Logger* pLogger = 0; if (pLogger == NULL) { EnterCriticalSection( if (pLogger == NULL) { try { pLogger = new Logger(); } catch (...) { // Something went wrong. } } LeaveCriticalSection( } return pLogger; }看起来仔细查过锁定版本好像能提供世界上最好的解决方案。只有少数几个在对象被实例化之前进入 GetLogger 的线程将被强行同步;以后到达的线程根本无需进入关键节。我们还想再要什么?仔细查过锁定模式尽管在概念上很简单,但事实证明,到目前为止,仍然难以正确编码。这是因为 C++ 标准没有定义线程模型。它假定只有一个执行线程,并且没有定义可供开发人员表示相关指令排序的约束的方式,这使得编译器可以自由重新排序内存的读取和写入。在多线程环境中,重新排序可能会导致线程在实际执行源代码中位置比它更靠前的所有语句之前就观察内存的写入。对于仔细查过锁定代码,可以在执行 Logger 构造函数之前,用分配给 Logger 对象的内存地址更新 pLogger 变量。观察到非空值的第二个线程将不再进入关键节,并返回未完全构造完成的对象的地址。(您可参见 Vance Morrison 撰写的关于托管代码的类似问题的文章“解读多线程应用程序中 Low-Lock 技术的影响”。)在旧版 Visual StudioC++ 编译器中,即使对变量 pLogger 使用 volatile 限制符也不能完全保证多线程方案中的正确行为。但在 Visual Studio 2005 中,只要用关键字 volatile 限定变量 pLogger,就能使仔细查过锁定模式在 Windows 平台上可靠执行。尽管 Visual Studio 2005 能正确实现仔细查过锁定模式,但还是很容易因为忽略包含实例指针的变量上的 volatile 限制符,或忽略关键节内的检查而使实现出错;而程序仍在编译并且似乎仍然有效。建立保证有效的机制将是对 C++ 有重要的补充。Windows Vista 没有坐等对标准主体的修订,而是提供了一个专门针对这一问题,称为一次性初始化的工具。不管您在哪一种硬件平台上使用 Windows,都能保证有效。一次性初始化既允许同步初始化,也允许异步初始化。让我们先来看看同步初始化。在概念上,同步初始化的工作模式与仔细查过锁定模式一样。在第一批 n 个试图同步执行初始化的线程中,实际上只有一个线程将实例化资源 — 其余线程将一直被阻止,直到初始化完成。一旦完成初始化,将不再阻止后面试图访问资源的线程;只会返回存储的资源。需要完成的第一件事情是声明并初始化 INIT_ONCE 结构的实例。您可使用 InitOnceInitialize 函数来执行初始化。对同步一次性初始化和异步一次性初始化而言,这是必须完成的,并且必须在任何其他一次性初始化函数使用结构之前完成。其定义如下:VOID WINAPI InitOnceInitialize( PINIT_ONCE InitOnce );

  BOOL WINAPI InitOnceExecuteOnce( PINIT_ONCE InitOnce, PINIT_ONCE_FN InitFn, PVOID Parameter, LPVOID* Context );第一个参数 InitOnce 是指向 INIT_ONCE 结构实例的指针。初始化时,所有传送同一个 INIT_STRUCTURE 地址的线程都将被相互同步化。第二个参数 InitFn 是指向开发人员所编写函数的指针;此函数执行实际初始化。第三个参数(暂且命名为 Parameter)属于可选值,被调用时将被传送回 InitFn。您可在此指定执行初始化所需的任何信息。最后一个参数 Context 是指向 void 指针的指针。这是函数运行成功时存储已初始化对象的地址。请注意,我是在使用“对象”一词来描述一次性初始化的结果。它不必非得是指向 C++ 对象的指针。这可以是符合执行所在平台上的指针大小值的任何东西。InitOnceExecuteOnce 如果成功则返回 TRUE,反之返回 FALSE。现在我们来看看 InitFn 所指向的函数。本函数的定义具有以下签名:BOOL CALLBACK InitOnceCallback( PINIT_ONCE InitOnce, PVOID Parameter, PVOID* Context );该参数和 InitOnceExecuteOnce 函数里的一样。InitOnceCallback 函数原本是用于存储 Context 内已初始化对象的地址。包含了为使用同步一次性初始化而重新编写的 GetLogger 函数。请注意,回调函数通过返回 FALSE 来表示失败。通过从回调函数返回 FALSE,系统允许另一个试图初始化的进程执行回调函数以期获得成功。此过程将一直持续,直到没有更多的初始化尝试或回调函数返回 TRUE。BOOL WINAPI InitLoggerFunction(PINIT_ONCE intOncePtr, PVOID Parameter, PVOID* contextPtr) { try { Logger* pLogger = new Logger(); *contextPtr = pLogger; return TRUE; } catch (...) { // Something went wrong. return FALSE; } } Logger* GetLogger() { static INIT_ONCE initOnce; PVOID contextPtr; BOOL status; status = InitOnceExecuteOnce(&initOnce, InitLoggerFunction, NULL, &contextPtr); if (status) { return (Logger*)contextPtr; } return NULL; }异步一次性初始化比同步操作更复杂些,但它允许一次性初始化时不会造成任何执行初始化的线程在等待初始化完成时被阻止。因此,初始化完成之前,允许所有尝试初始化的线程并行执行并争用初始化函数,其中,每个线程都初始化自身的对象专有副本,并随后尝试将对象注册为唯一一个已初始化的对象。其中只有一个线程将成为所谓的对象注册获胜者,其他线程(失败者)必须销毁各自的专有对象实例,然后向系统查询获胜对象。BOOL WINAPI InitOnceBeginInitialize( LPINIT_ONCE InitOnce, DWORD dwFlags, PBOOL fPending, LPVOID* Context ); BOOL WINAPI InitOnceComplete( LPINIT_ONCE lpInitOnce, DWORD dwFlags, LPVOID lpContext );显示了上面使用的同一个 GetLogger 函数,但现在它被重新编写,以使用异步一次性初始化。我们将逐步解释本例程。线程做的第一件事是调用 InitOnceBeginInitialize。该线程必须提供指向待使用的 INIT_ONCE 结构的指针,并且 dwFlags 参数应设置为 INIT_ONCE_ASYNC,以指定此线程正尝试开始异步一次性初始化。通过使用其余两个函数,fPending 和 Context,将初始化状态传回给调用方,上述两个参数均被指定为指针以便函数可以更新它们。如果成功,InitOnceBeginInitialize 返回 TRUE,否则返回 FALSE,表示有些地方已经出错,初始化无法继续。必须检查 fPending 以确认其他线程是否已完成初始化。如果 fPending 是 FALSE,则初始化已经完成,并且已将初始化对象存入 Context 参数。在此例中,GetLogger 剩下的唯一工作就是将 Context 转换为 Logger* 并将它返回给调用方。

  Logger* GetLogger() { static INIT_ONCE initOnce; PVOID contextPtr; BOOL fStatus; BOOL fPending; fStatus = InitOnceBeginInitialize(&initOnce,INIT_ONCE_ASYNC, &fPending, &contextPtr); // Failed? if (!fStatus) { return NULL; } // Initialization already completed? if (!fPending) { // Pointer to the logger is contained context pointer. return (Logger*)contextPtr; } // Reaching this point means that the logger needs to be created. try { Logger* pLogger = new Logger(); fStatus = InitOnceComplete( &initOnce,INIT_ONCE_ASYNC,(PVOID)pLogger); if (fStatus) { // The Logger that was created was successfully // set as the logger instance to be used. return pLogger; } // Reaching this point means fStatus is FALSE and the object this // thread created is not the sole instance. // Clean up the object this thread created. delete pLogger; } catch (...) { // Instantiating the logger failed. // Fall through and see if any of // the other threads were successful. } // Call again but this time ask only for // the result of one-time initialization. fStatus = InitOnceBeginInitialize(&initOnce,INIT_ONCE_CHECK_ONLY, &fPending,contextPtr); // Function succeeded and initialization is complete. if (fStatus) { // The pointer to the logger is in the contextPtr. return (Logger*) contextPtr; } // InitOnceBeginInitialize failed, return NULL. return NULL; }当 fPending 携带 TRUE 值返回时,事情变得更有趣。这表明初始化应当继续,并且同时可能还有其他线程作为调用方的线程在运行初始化代码。在中,这导致创建新的 Logger 对象,并随后尝试将该实例设置为唯一一个起源于异步一次性初始化的实例结果。可从新语句之后发生的 InitOnceComplete 调用中看到这一点。第一个参数指定了指向以前使用的同一个 INIT_ONCE 结构的指针,INIT_ONCE_ASYNC 标志被作为第二个参数传递,而指向记录器实例的指针则通过第三个参数提供。如果 InitOnceComplete 返回 TRUE,则本实例将被返回给 GetLogger 的所有调用方。假如 InitOnceComplete 返回 FALSE,则另一个线程创建的记录器被存储,调用方应销毁它创建的实例,这样它使用的所有资源才不会被搁置。此后,InitOnceBeginInitialize 再次被调用,但这次不用 INIT_ONCE_ASYNC 标志,而是使用 INIT_ONCE_CHECK_ONLY 标志。INIT_ONCE_CHECK_ONLY 标志用于查看初始化是否已完成。如果是,被存储的初始化值则被复制到提供的 Context 指针参数中。如果返回的是有效的初始化值,该函数将返回 TRUE,并且 fPending 参数将被设置为 FALSE。因此,您如何在同步和异步一次性初始化之间作出选择?在单一处理器系统中,使用同步很有意义,因为一次只能执行一个线程。在多处理器或多核系统中,如果创建了多对象实例,您将需要尝试量化因此可能产生的成本。成本可包括时间、内存(或其他稀缺资源类型),以及通常在对象被成功初始化之前并发执行初始化代码的线程的数量。如果构造对象需要花费不短的时间,或对逐个对象或可能同时创建的所有对象合计将使用大量内存,则同步初始化可能是更好的选择。另外,如果对象创建过程可能导致执行初始化的线程受阻,则同步初始化是更好的选择,因为并发的机会随即会消失,使用异步初始化好处并不大。本文章的源代码包含一个称为 OneTimeInit.exe 的示例,用于演示共享事件句柄的同步一次性初始化和异步一次性初始化。本程序使用许多用于运行的线程和一个指明是否执行同步或异步初始化的标志。指定的线程数量是生成的,并且每个线程均试图检索用来信号通知何时终止线 事件对象句柄。每个线程的进度会被发送至 stdout,让您可以准确了解初始化的进度。每个线程会完成一些设计好的工作,直到主线程发出终止事件信号。要获得程序使用信息,只需不带任何参数运行程序即可。关于一次性初始化,还有几个值得一提的遗留问题。我们发现,使用同步初始化,回调函数将被继续调用,直到成功。假定您的应用程序方案规定只尝试一次初始化,如果失败,则不再尝试。要实现这一点的一个方法是让回调函数仍然返回 TRUE,但在 lpContext 设置一个其他线程可检查的、表明出错的特殊值。另一种机制是使用 InitiOnceBeginInitialize/InitOnceComplete API 代替 InitOnceExecuteOnce API 来进行同步初始化。要做到这一点,请在 dwFlags 中省略 INIT_ONCE_ASYNC 标志,并将初始化代码置于 InitOnceBeginInitialize 调用和 InitOnceComplete API 之间,而不是置于单独的回调函数内。如果出错,则会使用 INIT_ONCE_INIT_FAILED 标志,并用 InitOnceComplete API 表明不会出现更多的初始化尝试。可通过使用其他线程能检查的 lpContext 来设置其他可选出错值。第二个遗留问题是一次性初始化并不能帮助清理已创建的对象。有许多解决这一问题的方法,但没有一种方法是万能的。如果对象属于操作系统在进程终止后清理的对象,请考虑将它泄漏。但要注意,如果您使用监视程序是否泄漏资源的工具,这可能会成为一个问题。如果对象需调用其清理函数以确保正确行为,您别无选择,只有在所有使用该对象的线程结束后协调此函数的调用。但您也可使用类似示例所用的技术方法。该示例使用已初始化的 HANDLE 注册的静态对象,这样在所有线程结束后,它将在程序执行结束时被自动销毁。在本文章中,我们向您介绍了 Windows Vista 中许多适于本机 C/C++ 开发人员的线程同步原语的重要增强功能。这些增强功能可让开发人员更容易地解决线程同步问题。但本文只涉及了一些皮毛。我们鼓励您更深入地研究它们以及其他更改。对于初学者,请先试着了解线程池 API 增强功能。很遗憾,我们没有机会在此讨论线程池 API 增强功能了,但它们确实是值得思考的宝贵的新增功能。在此,我们要特别感谢 Neill Clift 和 Arun Kishan 解答我们的问题并提供了很有见地的反馈。此外,还要感谢 Jerry 审阅文章内容并为我们提供宝贵的意见。是 Forefront Server Security 小组的一名架构师。他感兴趣的领域是大型软件设计、分布式系统和操作系统实现。

  是微软 Forefront Server Security 小组的高级软件开发工程师。他负责设计和开发 Forefront Server Security 产品的安全功能。

  最新Windows下c++读写锁SRWLock介绍    本文导语: 读写锁在对资源进行保护的同时,还能区分想要读取资源值的线程(读取者线程)和想要更新资源的线程(写入者线程)。对于读取者线程,读写锁...

  为什么要进行线程同步?在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果...

  多线进程和线程是操作系统里面经常遇到的两个概念,还有一个概念,是应用程序。应用程序包括指令和数据,在开始运行之前,只是分布在磁盘上的指令和数据。正在执行的应用程序称为进程,进程不仅仅是指令和数据,它还有状...博文

  转贴自破解激活Vista的利器——集三种破解方法于一身的破解...博文来自:afterain的专栏

  企业版双语言版(英文版(原版)/简体中文版/英简双语版---2.35 GB )12-02

  当你插入移动硬盘、鼠标、SD卡、MP3等驱动时,系统提示要安装驱动,这个SB系统说它找到通用卷的驱动了,就是安装的时候出了点问题,它找不到指定的文件了可笑的是微软出了一个号称可以解决这个问题的f...

  联想Windows Vista HomePremium SP1 Lenovo OEM x86序列号,保证激活成功.

  (转)彻底解决工行U盾windows7驱动程序无法使用的问题vista下U盾驱动问题也可以参考此方法...

  Windowsvista32Sp1整合官方中文旗舰版dowsVistaSP1刚刚已经有了新版本(Vista之家提示,2008年第一季度才有VistaSP1正式版本),现在就版本号问题做一个小小的算术,...

  阅读数 770目前的WindowsVista已经可以从现有老版本的系统上升级,不过这要求系统盘具有至少15GB的剩余空间,并且要求两个系统地语种必须一致。除了升级安装,我们还可以进行全新安装,在进行全新安装的时候也...博文

  虽然TabletPC版的XP中也有墨球这个游戏,不过运行起来在游戏窗口范围内没有鼠标光标,不太爽.昨天无聊的时候又在尝试VAIO可以破解些什么,偶然发现这个游戏可以成功破解,没有任何问题,而且有鼠标光...

  事先准备:装好xp系统的电脑一台,vista安装iso。    注:(1)WindowsVista要求安装在NTFS分区,但是其他分区可以是FAT32格式!    (2)简单无损NTFS转换方式:运行...

  引言:在很多用户在犹豫“我是否要安装Vista”时,有的网友采取了一个“折衷”的方案:模仿。通过一些第三方软件、主题包等,网友将WindowsXP模仿成WindowsVista界面的样子,在效果方...博文来自:

  Windows Vista SP2 MSDN版本种子,是从MSDN官方网站上下载回来做种的,每天晚上8点到12点做种

  1、把自己的U盘先插入到系统中,让系统可以正常使用U盘,接着进入“控制面板”,双击“设备管理器”,在里面展开“便携设备”,可以看见里面有你的U盘,2、在自己的u盘上面点击鼠标右键来选择“属性”,...

  教务工具箱包括成绩统计、成绩分段、多排表、表格比较与填充、检测同列重复值、考场编排助手、转换座位表、批量导入图片、批量读取文件名、文件批量改名、表格数据合成、批量设置上下标、多表合并等在学生数据分析、处理中常用的多个工具,运行平台为Mis...

  这篇文章适合于开发人员,普通Vista用户请飘过一下:)实际上,vista上不能安装Oracle11g的问题,和几年以前在IntelP4的机器上不能安装Oracle8.1.7一样,都是Java运行环境...

  万能网卡驱动,win7/winvista/winxp/win2000/winme/win98se

  (点击上方公众号,可快速关注)转自:36氪(编译组出品。编辑:郝鹏程、王雅琪)编者按:从后来的很多反馈看来,Vista都是一个超前于时代的操...博文来自:

  12-01阅读数 1295来自VistaFans和大道理投递的信息:微软很准时地在12月的第一天在MSDN提供了多语种版的WindowsVista下载,而且迅速泄露,我们已经得到了种子,大家一起加速!包含下载下载:Window...

  卷积神经网络是深度学习的基础,但是学习CNN却不是那么简单,虽然网络上关于CNN的相关代码很多,比较经典的是tiny_cnn(C++)、DeepLearnToolbox(Matlab)等等,但通过C语...博文来自:tostq的专栏

  1. 前言 隐马尔科夫HMM模型是一类重要的机器学习方法,其主要用于序列数据的分析,广泛应用于语音识别、文本翻译、序列预测、中文分词等多个领域。虽然近年来,由于RNN等深度学习方法的发展,HMM模型...

  小憩之后,继续为你解读AndFix热修复框架,呵呵。上一篇Alibaba-AndFix Bug热修复框架的使用已经介绍了AndFix的使用,这篇主要介绍AndFix原理以及源码解析。AndFix原理A...

  做MFC+opencv项目时,对于我来说,将视频显示到相应控件上(static或者picture)这个问题一直存在,虽然之前写个一个帖子,介绍了一种将opencv的显示window贴到相应控件上的方法...

  0.绪论这篇文章主要为了研究双目立体视觉的最终目标——三维重建,系统的介绍了三维重建的整体步骤。双目立体视觉的整体流程包括:图像获取,摄像机标定,特征提取(稠密匹配中这一步可以省略),立体匹配,三维重...

  帐号相关流程注册范围 企业 政府 媒体 其他组织换句话讲就是不让个人开发者注册。 :)填写企业信息不能使用和之前的公众号账户相同的邮箱,也就是说小程序是和微信公众号一个层级的。填写公司机构信息,对公账...

  webService学习(二)—— 调用自定义对象参数 本文主要内容: 1、如何通过idea进行webService Client的简单实现(不再使用wsimport的方式,其实是ide帮我们做了...

  灰度图像的自动阈值分割(Otsu 法)机器视觉领域许多算法都要求先对图像进行二值化。这种二值化操作阈值的选取非常重要。阈值选取的不合适,可能得到的结果就毫无用处。今天就来讲讲一种自动计算阈值的方法。这...

  最近比较有空,大四出来实习几个月了,作为实习狗的我,被叫去研究Docker了,汗汗! Docker的三大核心概念:镜像、容器、仓库 镜像:类似虚拟机的镜像、用俗话说就是安装文件。 容器:类似一个轻量...

  jquery/js实现一个网页同时调用多个倒计时(最新的) 最近需要网页添加多个倒计时. 查阅网络,基本上都是千遍一律的不好用. 自己按需写了个.希望对大家有用. 有用请赞一个哦! //js ...

  局部异常因子算法-Local Outlier Factor(LOF)在数据挖掘方面,经常需要在做特征工程和模型训练之前对数据进行清洗,剔除无效数据和异常数据。异常检测也是数据挖掘的一个方向,用于反...

  强连通分量: 简言之 就是找环(每条边只走一次,两两可达) 孤立的一个点也是一个连通分量   使用tarjan算法 在嵌套的多个环中优先得到最大环( 最小环就是每个孤立点)   定义: int Ti...

  1. 基本概念     方向导数:是一个数;反映的是f(x,y)在P0点沿方向v的变化率。     偏导数:是多个数(每元有一个);是指多元函数沿坐标轴方向的方向导数,因此二元函数就有两个偏导数。  ...

  结果分析(): 1.聚合报告 Aggregate Report 是 JMeter 常用的一个 Listener,中文被翻译为“聚合报告”。今天再次有同行问到这个报告中的各项数据表示什么意思,...

  上一篇文章讲解了SNMP的基本架构,本篇文章将重点分析SNMP报文,并对不同版本(SNMPv1、v2c、v3)进行区别! 四、SNMP协议数据单元 在SNMP管理中,管理站(NMS)和代理(Age...

本文链接:http://chuyenchame.com/duruyuanyu/51.html