Win 32 多线程程序设计学习笔记之六:异步I/O(Overlapped I/O)

来源:互联网 发布:数据恢复电脑版 编辑:程序博客网 时间:2024/05/29 16:21

          某些时候 overlapped I/O 可以取代多线程的功用。然而,overlapped I/O 加上completion ports,常被设计为多线程处理,以便在一个“受制于 I/O 的程序”(所谓 I/O bound 程序)中获得高效率。

        overlapped I/O 是 Win32 的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这项技术使你的程序在I/O 进行过程中仍然能够继续处理事务。事实上,操作系统内部正是以线程来完成 overlapped I/O。你可以获得线程的所有利益,而不需付出什么痛苦代价。

  • 激发的文件 handles
  • 激发的 event 对象
  •  异步过程调用(Asynchronous Procedure Calls,APCs)
  •  I/O completion ports
        其中以 I/O completion ports 特别显得重要,因为它们是唯一适用于高负载服务器(必须同时维护许多连接线路)的一个技术。Completion ports 利用一些线程,帮助平衡由“I/O 请求”所引起的负载。这样的架构特别适合用在SMP 系统(支持多个 CPU 的操作系统)中产生所谓的 “scalable” 服务器。

        所谓 scalable 系统,是指能够藉着增加 RAM 或磁盘空间或 CPU 个数而提升应用程序效能的一种系统。

1. Win32 文件操作函数
Win32 之中有三个基本的函数用来执行 I/O,它们是:
  • CreateFile()
  • ReadFile()
  •  WriteFile()
       没有另外哪一个函数用来关闭文件,只要调用 CloseHandle() 即可。

CreateFile() 可以用来打开各式各样的资源,包括(但不限制于):
  • 文件(硬盘、软盘、光盘或其他)
  • 串行口和并行口(serial and parallel ports)
  •  Named pipes
  • Console
CreateFile() 的函数原型看起来像这样:
HANDLE CreateFile(LPCTSTR lpFileName, // 指向文件名称DWORD dwDesiredAccess, // 存取模式(读或写)DWORD dwShareMode, // 共享模式(share mode)LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性结构DWORD dwCreationDisposition, // 如何产生DWORD dwFlagsAndAttributes, // 文件属性HANDLE hTemplateFile // 一个临时文件,将拥有全部的属性拷贝);

            其中第6个参数 dwFlagsAndAttributes 是使用 overlapped I/O 的关键。这个参数可以藉由许多个数值组合在一起而完成,其中对于本处讨论最重要的一个数值便是 FILE_FLAG_OVERLAPPED。你可以藉着这个参数,指定使用同步(传统的)调用,或是使用 overlapped(异步)调用,但不能够两个都指定。换句话说,如果这个标记值设立,那么对该文件的每一个操作都将是overlapped。

        一个不常被讨论的 overlapped I/O 性质是,它可以在同一时间读(或写)文件的许多部分。微妙处在于这些操作都使用相同的文件 handle。因此,当你使用 overlapped I/O 时,没有所谓“目前的文件位置”这样的观念。每一次读或写的操作都必须包含其文件位置。

        如果你发出许多个 overlapped 请求,那么执行次序无法保证。虽然你在单一磁盘中对文件进行操作时很少会有这样的行为,但如果面对多个磁盘,或不同种类的设备(如网络和磁盘),就常常会看到 I/O 请求完全失去次序。

        你将不可能藉由调用 C runtime library 中的 stdio.h 函数而使用overlapped I/O。因此,没有很方便的方法可以实现 overlapped text-based I/O。例如,fgets() 允许你一次读取一行文字,但你不能够使用 fgets()、fprintf() 或任何其他类似的 C runtime 函数来进行 overlapped I/O。

        Overlapped I/O 的基本型式是以 ReadFile() 和 WriteFile() 完成的。这两个函数的原型如下:
BOOL ReadFile(HANDLE hFile, // 欲读之文件LPVOID lpBuffer, // 接收数据之缓冲区DWORD nNumberOfBytesToRead, // 欲读取的字节个数LPDWORD lpNumberOfBytesRead, // 实际读取的字节个数的地址LPOVERLAPPED lpOverlapped // 指针,指向 overlapped info);BOOL WriteFile(HANDLE hFile, // 欲写之文件LPCVOID lpBuffer, // 储存数据之缓冲区DWORD nNumberOfBytesToWrite, // 欲写入的字节个数LPDWORD lpNumberOfBytesWritten, // 实际写入的字节个数的地址LPOVERLAPPED lpOverlapped // 指针,指向 overlapped info);

        这两个函数很像 C runtime 函数中的 fread() 和 fwrite(),差别在于最后一个参数lpOverlapped 。如果CreateFile() 的第6 个参数被指定为FILE_FLAG_ OVERLAPPED,你就必须在上述的 lpOverlapped 参数中提供一个指针,指向一个 OVERLAPPED 结构。

OVERLAPPED 结构
        OVERLAPPED 结构执行两个重要的功能。第一,它像一把钥匙,用以识别每一个目前正在进行的 overlapped 操作。第二,它在你和系统之间提供了一个共享区域,参数可以在该区域中双向传递。
        OVERLAPPED 结构看起来像这样:
typedef struct _OVERLAPPED {DWORD Internal;DWORD InternalHigh;DWORD Offset;DWORD OffsetHigh;HANDLE hEvent;} OVERLAPPED, *LPOVERLAPPED;

OVERLAPPED 结构中的成员:


           由于 OVERLAPPED 结构的生命期超越 ReadFile() 和 WriteFile() 函数,所以把这个结构放在一个安全的地方是很重要的事情。通常局部变量并不是一个安全的地方,因为它会很快就越过了生存范围(out of scope)。最安全的地方就是 heap。

2. 被激发的File Handles
       
           最简单的 overlapped I/O 类型,是使用它自己的文件 handle 作为同步机制。首先你以 FILE_FLAG_OVERLAPPED 告诉 Win32 说你不要使用默认的同步 I/O。然后,你设立一个 OVERLAPPED 结构,其中内含“I/O 请求”的所有必要参数,并以此识别这个“I/O 请求”,直到它完成为止。接下来,调用 ReadFile() 并以 OVERLAPPED 结构的地址作为最后一个参数。这时候,理论上,Win32 会在后台处理你的请求。你的程序可以放心地继续处理其他事情。

        如果你需要等待 overlapped I/O 的执行结果, 作为WaitForMultipleObjects() 的一部分, 请在 handle 数组中加上这个文件handle。文件 handle 是一个核心对象,一旦操作完毕即被激发。当你完成操作之后,请调用 GetOverlappedResult() 以确定结果如何。

        调用 GetOverlappedResult() , 你获得的结果和“ 调用 ReadFile() 或WriteFile() 而没有指定 overlapped I/O”所传回的东西一样。这个函数的价值在于,在文件操作真正完成之前,你不可能确实知道它是否成功。甚至在一个完美无瑕的环境下读一个已知的磁盘文件,也有可能发生硬件错误、服务器当掉,或任何未能预期的错误。因此,调用 GetOverlappedResult() 是很重要的。

GetOverlappedResult() 规格如下:
BOOL GetOverlappedResult(HANDLE hFile,LPOVERLAPPED lpOverlapped,LPDWORD lpNumberOfBytesTransferred,BOOL bWait);

参数
hFile 文件或设备(device)的 handle。
lpOverlapped 一个指针,指向 OVERLAPPED 结构。
lpNumberOfBytesTransferred 一个指针,指向 DWORD,用以表示真正被传输的字节个数。
bWait 一个布尔值,用以表示是否要等待操作完成。TRUE 表示要等待。

返回值
如果 overlapped 操作成功,此函数传回 TRUE。失败则传回 FALSE。GetLastError() 可获得更详细的失败信息。如果 bWait 为 FALSE 而overlapped 还是没有完成,GetLastError() 会传回ERROR_IO_INCOMPLETE。

3. 被激发的Event 对象

          以文件 handle 作为激发机制,有一个明显的限制,那就是没办法说出到底是哪一个 overlapped 操作完成了。如果每个文件 handle 只有一个操作等待决定,上述问题其实并不成为问题。但是如我稍早所说,系统有可能同时接受数个操作,而它们都使用同一个文件 handle。于是很明显地,为每一个可能正在进行中的 overlapped 操作调用 GetOverlappedResult(),并不是很有效率的做法。毫不令人惊讶,Win32 提供了一个比较好的做法,用以解决这样的问题。

        OVERLAPPED 结构中的最后一个栏位,是一个 event handle。如果你使用文件 handle 作为激发对象,那么此栏位可为 NULL。当这个栏位被设定为一个 event 对象时,系统核心会在 overlapped 操作完成的时候,自动将此event 对象给激发起来。由于每一个 overlapped 操作都有它自己独一无二的OVERLAPPED 结构,所以每一个结构都有它自己独一无二的一个 event 对象,用以代表该操作。

        有一件事很重要:你所使用的 event 对象必须是手动重置(manual-reset)而非自动重置(auto-reset)。如果你使用自动重置,就可能产生出 race condition,因为系统核心有可能在你有机会等待该 event 对象之前,先激发它,而你知道,event 对象的激发状态是不能够保留的(这一点和semaphore 不同)。于是这个 event 状态将遗失,而你的 Wait...() 函数永不返回。但是一个手动重置的 event,一旦激发,就一直处于激发状态,直到你动手将它改变。

        使用 event 对象搭配 overlapped I/O,你就可以对同一个文件发出多个读取操作和多个写入操作,每一个操作有自己的 event 对象;然后再调用WaitForMultipleObjects() 来等待其中之一(或全部)完成。

4. 异步过程调用(Asynchronous Procedure Calls,APCs)

        使用 overlapped I/O 并搭配 event 对象,会产生两个基础性问题。第一个问题是,使用WaitForMultipleObjects(),你只能够等待最多达 MAXIMUM_WAIT_OBJECTS 个对象。在 Windows NT 3.x 和 4.0 所提供的 Win32 SDK中,此最大值为 64。如果你要等待 64 个以上的对象,就会出问题。所以即使在一个客户/服务器环境(client-server)中,你也只能同时拥有 64 个连接点。第二个问题是,你必须不断根据“哪一个 handle 被激发”而计算如何反应。你必须有一个分派表格(dispatch table)和 WaitForMultipleObjects() 的
handles 数组结合起来。

        这两个问题可以靠一个所谓的异步过程调用(Asynchronous Procedure Call,APC)解决。只要使用“Ex” 版的 ReadFile() 和 WriteFile(),你就可以调用这个机制。这两个函数允许你指定一个额外的参数,是一个 callback 函数地址。当一个 overlapped I/O 完成时,系统应该调用该 callback 函数。这个 callback 函数被称为 I/O completion routine,因为系统是在某一个特别的overlapped I/O 操作完成之后调用它。

        然而,Windows 不会贸然中断你的程序,然后调用你提供的这个 callback函数。系统只有在线程说“好,现在是个安全时机”时才调用你的 callback 函数。以 Windows 的说法就是:你的线程必须在所谓的 “alertable” 状态之下才行。如果有一个 I/O 操作完成,而线程不处于 “alertable” 状态,那么对 I/O completion routine 的调用就会暂时被保留下来。因此,当一个线程终于进入 “alertable” 状态时,可能已经有一大堆储备的 APCs 等待被处理。

        如果线程因为以下五个函数而处于等待状态,而其 “alertable” 标记被设为 TRUE,则该线程就是处于 “alertable” 状态:
  • SleepEx()
  • WaitForSingleObjectEx()
  •  WaitForMultipleObjectsEx()
  • MsgWaitForMultipleObjectsEx()
  • SignalObjectAndWait()
        “只有当程序处于 “alertable” 状态下时,APCs 才会被调用”这个观念是很重要的。其结果就是,当你的程序正在进行精确至小数点后10000 位的圆周率的计算时,I/O completion routine 不会被调用。如果原线程正忙于重绘屏幕,也不会有另一个线程被使用。

         用于 overlapped I/O 的 APCs 是一种所谓的 user mode APCs。Windows NT 另有一种所谓的 kernel mode APCs。Kernel mode APCs 也会像 user mode APCs 一样被保存起来,但一个 kernel mode APC 一定会在下一个timeslice 被调用,不管线程当时正在做什么。 Kernel mode APCs 用来处理系统机能,不在应用程序的控制之中。

你所提供的 I/O completion routine 应该有这样的型式:
VOID WINAPI FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransferred,LPOVERLAPPED lpOverlapped};

参数
dwErrorCode 这个参数内含以下的值:0 表示操作完成,ERROR_HANDLE_EOF 表示操作已经到了文件尾端。
dwNumberOfBytesTransferred 真正被传输的数据字节数。
lpOverlapped 指向 OVERLAPPED 结构。此结构由开启overlapped I/O 操作的函数提供。

        I/O completion routine 需要一些东西以了解其环境。如果它不知道 I/O 操作完成了什么,它也就很难决定对此数据要做些什么。使用 APCs 时,OVERLAPPED 结构中的 hEvent 栏位不需要用来放置一个 event handle。Win32 文件上说此时 hEvent 栏位可以由程序员自由运用。那么最大的用途就是:首先配置一个结构,描述数据来自哪里,或是要对数据进行一些什么操作,然后将 hEvent 栏位设定指向该结构。

在C++ 中产生一个I/O Completion Routines

        一个 C++ 成员函数不能拿来当做一个 I/O completion routine,除非它是一个 static 函数。如果它是 static,它就没有 this 指针,也就不能够直接调用那些需要 this 指针的成员函数。

        解决之道是储存一个指针,指向用户自定义数据(一个对象),然后经由此指针调用一个 C++ 成员函数。由于 static 成员函数是类的一部分,你还是可以调用 private 成员函数。这个问题和“如何把 C++ 成员函数当做一个线程函数”非常类似。我会在第9章描述解决方法。

        有两种情况,overlapped I/O 总是同步执行,甚至即使 FILE_FLAG_NO_BUFFERING 已经指定。第一种情况是你进行一个写入操作而造成文件的扩展。第二种情况是你读写一个压缩文件。

5. I/O Completion Ports

        虽然 APCs 是完成 overlapped I/O 的一个非常便捷的方法,但它们还是有它们的缺点。最大的问题就是,有好几个 I/O APIs 并不支持 APCs,如listen() 和 WaitCommEvent() 便是两个例子。APCs 的另一个问题是,只有发出“overlapped 请求”的那个线程才能够提供 callback 函数。然而在一个“scalable)系统中,最好任何线程都能够服务 events。

        在 Windows NT 3.5 中有第四种 overlapped I/O 的存在,称为 I/O completion ports。靠着“一大堆线程服务一大堆 events”的性质,completion ports 比较容易建立起 “scalable” 服务器。

        尽管名称类似,I/O completion ports 与 APCs 中所用的I/O completion routines 没有任何关联
        Completion ports 解决了我们截至目前看到的所有问题:
  • 与 WaitForMultipleObjects() 不同,这里不限制 handles 的个数。
  •  I/O completion ports 允许一个线程将一个请求暂时保存下来,而由另一个线程为它做实际服务。
  • I/O completion ports 默默支持 scalable 架构。
服务器的线程模型

有三个基本方法,可以决定一个服务器上需要多少个线程:

  1. 单独一个线程。在一个文件服务器或一个简单的 Web 服务器上,单一线程可以使用 overlapped I/O 来搬移数据。这个线程可以常保 CPU 和磁盘尽可能地忙碌。然而,如果线程还必须进行任何其他操作,则整个服务器会陷入泥淖之中。
  2. 每个 client 给予一个线程。如果你为每一个 client 产生一个线程,那么理论上每个人都可以分到相当不错的反应时间,因为 CPU 的能量被平均分配了。然而事实上系统资源是有限的,到了某种情况下,系统效率就会剧烈下降。如果有 2000 个 clients,这种做法就不切实际了。
  3. 每个 CPU 给予一个线程。这种做法让每一个 CPU 尽可能忙碌,不至于出现哪一个 CPU 有过度饱和的情况。如果你使用 event 对象或 APCs,此法将难以实现,因为它们都十分紧密地和某个线程绑在一起。这个问题的解决方案必须靠一种特殊的同步对象,称为 I/O completion ports。
        I/O completion port 是一种非常特殊的核心对象,用来综合一堆线程,让它们为“overlapped 请求”服务。其所提供的功能甚至可以跨越多个 CPUs。Completion port 可以自动补偿成长中的服务器,适合应用于沉重的负担。

        为了使用 I/O completion port,你的程序应该产生一堆线程,统统在 I/O completion port 上等待着。这些线程都将成为“ 能够处理 completed overlapped I/O request”的线程之一——只要线程在 I/O completion port 上等待,它就自然而然成为那种线程。

        每次有新的文件因为 overlapped I/O 而开启,你就可以让它的文件 handle和 I/O completion port 产生关联。一旦这样的关系建立起来,任何文件操作如果成功完成,便会导致 I/O completion packet 被送到 completion port 去。这是发生于操作系统之内的操作,对应用程序而言是透明的。

        为了回应 I/O completion packet,completion port 释放了一个等待中的线程。如果目前并没有线程正在等待,completion port 不会产生新线程。

        释放出来的线程被授予足够的信息, 使它能够辨识 “ completed overlapped I/O operation” 的背景环境,然后这个线程就起而行,处理该操作请求。但它还是属于原来那一堆线程(指定给此 completion port)的一分子,差别在于这个线程现在成为一个作用中的(active)线程,而不是一个等待中的(waiting)线程。当这个线程将“overlapped I/O 请求”处理完毕时,它应该再次在这个 I/O completion port 上等待。

          以此概观为基础,大略可以这样描述一个 completion port:它是一个机制,用来管理一堆线程如何为 completed overlapped I/O requests 服务。然而,completion port 远比一个简单的分派器丰富得多,I/O completion port 也像一个活门(阀)一样,保持一个 CPU 或多个 CPUs 尽可能地忙碌,但也避免
它们被太多的线程淹没。I/O completion port 企图保持并行处理的线程个数在某个数字左右。一般而言你希望让所有的 CPUs 都忙碌,所以默认情况下并行处理的线程个数就是 CPUs 的个数。

        I/O completion port 运作过程中令人迷惑的部分就是,当一个线程被阻塞住时,它将发出通告,并提交另一个线程。假设在单一 CPU 的系统中,有两个线程都正在一个 I/O completion port 上等待,线程1被唤醒,并且从网络上获得一包数据。为了服务这个数据包,线程1必须从磁盘中读一个文件,所以它调用 CreateFile() 和 ReadFile(),但不在 overlapped 模式中。

操作概观
使用一个 completion port 的快速摘要列在下面:
1. 产生一个 I/O completion port。
2. 让它和一个文件 handle 产生关联。
3. 产生一堆线程。
4. 让每一个线程都在 completion port 上等待。
5. 开始对着那个文件 handle 发出一些 overlapped I/O 请求。

        当新文件被开启时,它们可以在任何时候与 I/O completion port 产生关联。在 completion port 上等待的线程不应该做“为 completion port 服务”以外的事情,因为这些线程将一直都是 completion port 所持续追踪的那一堆线程的一部分。

产生一个I/O Completion Port
        I/O completion port 是一个核心对象,你必须使用 CreateIoCompletionPort()才能产生它:
HANDLE CreateIoCompletionPort(HANDLE FileHandle,HANDLE ExistingCompletionPort,DWORD CompletionKey,DWORD NumberOfConcurrentThreads);

参数
FileHandle 文件或设备(device)的 handle。在 Windows NT 3.51 之后, 此栏位可设定为INVALID_HANDLE_VALUE,于是产生一个没有和任何文件 handle 有关系的 port。
ExistingCompletionPort 如果此栏位被指定, 那么上一栏位
FileHandle 就会被加到此 port 之上,而不会产生新的 port。指定 NULL 可以产生一个新的 port。
CompletionKey 用户自定义的一个数值,将被交给提供服务的线程。此值和 FileHandle 有关联。
NumberOfConcurrentThreads 与此 I/O completion port 有关联的线程个数。

返回值
如果函数成功,则传回一个 I/O completion port 的 handle。如果函数失败,则传回 FALSE。GetLastError() 可以获得更详细的失败原因。

        任何文件只要附着到一个 I/O completion port 身上, 都必须先以FILE_FLAG_OVERLAPPED 开启。如果已经附着上去, 就不能够再以ReadFileEx() 或 WriteFileEx() 操作它。你可以任意关闭这样的一个文件,没有什么安全上的顾虑。

        通常你会想要把 NumberOfConcurrentThreads 设定为 0,如此一来,在多CPU 系统下,就可以有尽可能多的线程运行起来。这可以使每一个 CPU 都尽可能地忙碌,降低因过度的 context switch 而造成的额外浪费(overhead)。

与一个文件handle 产生关联

        CreateIoCompletionPort() 通常被调用两次。第一次先指定 FileHandle 为INVALID_HANDLE_VALUE,并设定 ExistingCompletionPort 为 NULL,用以产生一个 port。然后再为每一个欲附着上去的文件 handle 调用一次CreateIoCompletionPort()。后续的这些调用应该将 ExistingCompletionPort 设定为第一次调用所传回的 handle。

产生一堆线程
        一旦 completion port 产生出来,你就可以设立在该 port 上等待的那些线程了。I/O completion port 并不自己产生那些线程,它只是使用由你产生的线程。因此,你必须自己以 CreateThread() 或 _beginthreadex()或AfxBeginThread()产生出线程。

        当你一产生这些线程时,它们都应该在 completion port 上等待。当线程开始为各个“请求”服务时,池子里的线程的组织如下:
   目前正在执行的线程
+ 被阻塞的线程
+ 在 completion port 上等待的线程
--------------------------------------------
= 池子里的所有线程的个数

        因为如此,所以你应该产生比 CPU 个数还多的线程。如果你只有一个CPU,而你也只产生了一个线程,那么当该线程阻塞(blocking)时,你的 CPU也变成闲置(idle)的了。由于池子里没有其他线程,completion port 也就没有办法为任何数据包(packets)服务——甚至即使 CPU 的能量游刃有余。

        合理的线程个数应该是 CPU 个数的两倍再加2。没有理由说你不能够产生更多的线程,但记住,线程并不是免费的,100 个 worker 线程在 completion port 上等待,并不会让系统速度更快一些。

在一个I/O Completion Port 上等待
       Worker 线程初始化自己之后,它应该调用GetQueuedCompletionStatus()。这个操作像是 WaitForSingleObject() 和 GetOverlappedResult() 的组合。函数规格如下:
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort,LPDWORD lpNumberOfBytesTransferred,LPDWORD lpCompletionKey,LPOVERLAPPED *lpOverlapped,DWORD dwMilliseconds);

参数
CompletionPort 将在其上等待的 completion port。
lpNumberOfBytesTransferred 一个指针,指向 DWORD。该 DWORD 将收到“被传输的数据字节数”。
lpCompletionKey 一个指针,指向 DWORD。该 DWORD 将收到由 CreateIoCompletionPort() 所定义的
key。
lpOverlapped 这个栏位的名称是个错误。它其实应该命名为 lplpOverlapped,你应该把一个指针的地址放在上面。系统会填以一个一个 I/O completion port 结构的指针。该结构用以初始化I/O 操作。
dwMilliseconds 等待的最长时间(毫秒)。如果时间终了,lpOverlapped 将被设为 NULL,而函数传回
FALSE。

返回值
        如果函数成功地将一个 completion packet 从队列中取出,并完成一个成功的操作,函数将传回 TRUE,并填写由 lpNumberOfBytesTransferred、lpCompletionKey、lpOverlapped 所指向的变量内容。
        如果操作失败,但 completion packet 已经从队列中取出,则函数传回FALSE,lpOverlapped 指向失败之操作。调用 GetLastError() 可获知为什么I/O 操作会失败。
        如果函数失败,则传回 FALSE, 并将 lpOverlapped 设为 NULL。调用GetLastError() 可获知为什么函数会失败。

        与其他的核心对象,如 semaphores 和 mutexes 不同,在 completion port上等待的线程是以先进后出(first in last out,FILO)的次序提供服务。没有什么理由需要担忧次序的公平性,因为所有的线程都做完全相同的事情。使用先进后出(FILO)规则,一个进行中的线程调用 GetQueuedCompletionStatus() 就可以取得下一个请求(request),并保持执行状态,没有阻塞(blocking)。这是非常有效率的。线程如果等待太长的时间,就有可能会被置换出去(paged out)。最近执行过的线程则通常还在内存中,不需要先置换进来(paged in)才能执行。

发出“Overlapped I/O 请求”
        下面这些调用可以启动“能够被一个 I/O completion port 掌握”的 I/O 操作:

  • ConnectNamePipe()
  • DeviceIoControl()
  • LockFileEx()
  • ReadFile()
  • TransactNamePipe()
  •  WaitCommEvent()
  •  WriteFile()
        为了使用 completion port,主线程(或任何其他线程)可以对着一个与此completion port 有关联的文件,进行读、写、或其他任何操作。该线程不需要调用 WaitForMultipleObjects() , 因为池子里的各个线程都曾经调用过GetQueuedCompletionStatus()。一旦 I/O 操作完成,一个等待中的线程将会自动被释放,以服务该操作。







原创粉丝点击