一、创建线程
与调用函数的过程类似;线程只不过用CreateThread的API将函数封装起来,并产生一个与主程序同时执行的程序来调用被封装的函数。
HANDLE hThread = CreateThread (
LPSECURITY_ATTRIBUTES lpThreadAtt,
DWORD dwStackSize
LPTHREAD_START_ROUTINE lpFun,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadID)
LpFun:就是指向被封装的函数的地址。
LpParameter: 就是指向被封装的函数的参数(没有参数填入null)
下图为简单的函数调用和调用线程的过程:
注意:当用CreateThread之后,产生的新的线程不会立即启动,执行的时间也是无法预期的。
二、核心对象
- DWORD WINAPI ThreadFun(LPVOID n) //线程函数:
- {
- return 10;
- }
- int main()
- {
- hThrd = CreateThread(null,0,ThreadFun, …);
- DWORD exitCode=0;
- for( ; ; ) {
- GetExitCodeThread(hThrd,&exitCode);
- if ( exitCode == STILL_ACTIVE){
- //线程仍然在运行
- }
- else{
- break;
- }
- }
- //exitCode保存了函数的返回值
- printf( “线程函数的返回值 : %d /n”, exitCode);
- CloseHandle(hThrd);
- return 0;
- }
四、线程的等待技术
三种等待线程的技术:
1)使用Sleep()函数——问题是你不可能事先知道什么事情要等待多久
2)使用busy loop,通过不断地调用GetExitCodeThread()这个函数来判断一个线程是否还在执行——问题是使用这个方法,必须持续不断的调用GetExitCodeThread(),直到其结果不再是STILL_ACTIVE,这方法不好,很浪费cpu时间,称为忙等待(busy waits)。
- for ( ; ; )
- {
- int rc;
- rc = GetExitCodeThread(hThrd,&exitCode);
- if (!rc && exitCode != STILL_ACTIVE)
- break;
- }
3)使用WaitForSingleObject(hHandle,dwMilliseconds);
功能:某一线程中调用这个函数,此线程会被挂起:
1、dwMilliseconds毫秒内,如果此线程所等待的对象(hHandle所指线程)变成有信号状态(被激发,即hHandle线程结束),则该函数立即返回执行函数下面的代码;
2、超过dwMilliseconds毫秒,hHandle所指的对象还没有变成有信号状态,照样返回。
- //参考书上例子
- //程序的目的:只用个线程 ,完成件事
- int main()
- {
- HANDLE hThrds[3];
- int slot = 0;
- for( int I=1 ; I<=6 ; I++){
- if( I > 3 ){
- //已经存在个线程了
- //等待其中的一个线程线束后,再创建线程做剩余的事情
- //效率不高,因为线程结束的次序与它们被产生的次序不同
- WaitForSingleObject( hThrds[slot] , INFINITE ) ;
- CloseHandle ( hThrds[slot] );
- }
- //构造线程
- hThrds[slot] = CreateThread(NULL,0,ThreadFunc,NULL,0,NULL);
- if (++slot>2)
- slot=0;
- }
- for ( slot=0; slot<3; slot++){
- //等待剩余的线程结束
- WaitForSingleObject( hThrds[slot] , INFINITE );
- CloseHandle ( hThrds[slot] );
- }
- }
如何让一个线程和另外一个线程合作。在同一时间段会存在多个线程,当这些线程同时存取同一数据时,就会有问题。就像在超市储物品一样,来的时候物品箱是空,转身拿物品准备储的时候,发现物品箱已被占用了。这时,物品箱就是我所说的同一数据,人指的就是线程了。
线程之间的协调工作由同步机制来完成。同步机制相当于线程之间的红绿灯系统,负责给某个线程绿灯而给其他线程红灯进行等待。
注:对同步(synchronous)和异步进行一个说明,所谓的同步:当程序1调用程序2时,程序1停下不动,直到程序2完成回到程序1来,程序1才继续下去。
Win32 API中SendMessage()就是同步行为,而PostMessage()就是异步行为。
现在,看看第一个同步机制。
一、Critical Sections(临界区域、关键区域)
主要操作有:
InitializeCriticalSection
EnterCriticalSection
LeaveCriticalSection
DeleteCriticalSection
通过一个例子说明:
- CRITICAL_SECTION gBoxKey ;
- DWORD WINAPI ThreadFun(LPVOID n){
- //进入关键区域(情景:关上物品箱,拨下钥匙)
- EnterCreiticalSection (&gBoxKey ); //()
- //处理一些不可分割的操作。。。。。
- //(情景:转身拿物品,储物品,去购物。。。。)
- //离开关键区域(情景:打开物品箱,拿出储存的物品,插上钥匙)
- LeaveCreiticalSection (&gBoxKey); //()
- }
- void main(){
- //初始化全局锁(情景:生成物品箱的钥匙 )
- InitializeCriticalSection( &gBoxKey ) ;
- //产生两个线程(情景:准备两个人抢一个物品箱 )
- HANDLE hMan1 = CreateThread(NULL,0,ThreadFun, ……);
- HANDLE hMan2 = CreateThread(NULL,0,ThreadFun, ……);
- CloseHandle(hMan1);
- CloseHandle(hMan2);
- //删除全局锁(情景:删除物品箱的钥匙 )
- DeleteCriticalSection( &gBoxKey ) ;
- }
2、很难定义最小锁定时间,如果资源一直被锁定,你就会阻止其他线程的执行,所以千万不要在critical section中调用Sleep()或任何Wait函数。
死锁问题
在使用临界区域的时候,有可能出现两个线程互相等待对方的资源从而形成等待的轮回,这种情况称为“死锁”。下面总结一下产生死锁的原因等。
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
产生死锁的四个必要条件:
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁:
死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。
避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。
预防死锁:具体的做法是破坏产生死锁的四个必要条件之一。
二、Mutexes(互斥器)
一个时间内只能有一个线程拥有mutex,就像同一时间内只能有一个线程进入同一个Critical Section一样。无论从共享资源的思路了,还是从程序代码的编制上,使用Mutexes与使用Critical Sections几乎都没有什么区别;
当然,mutex和critical section还有一些区别:
1、锁住一个未被拥有的mutex,比锁住一个未被拥有的critical section,需要花费几乎100倍的时间;
2、mutex可以跨进程使用,critical section只能在同一进程中使用;
3、等待一个mutex时,可以指定“结束等待”的时间长度,这样就避免了进入在锁住过程中出不来的问题(这一点下面接着说明)。
解决:
还记得上一章学过的WaitForSingleObject吗?上一章主要用它等待线程的结束,但这个函数的作用不仅限于此,在这里,我们再前进一小步,探究WaitForSingleObject这个函数的妙用。
在这里,我遇到了一个叫mutex核心对象,mutex对激发的定义是:“当没有任何线程拥有该mutex,而且有一个线程正以Wait…()等待该mutex,该mutex就会短暂地出现激发状态,使Wait…()得以返回, 那么在其它的情况,mutex处于未激发状态”。好了,我们又进一步的了解了WaitForSingleObject函数,那么,如何解决Critical Sections所遇到的因难呢?当拥有mutex的线程结束前没有调用ReleaseMutex(不管该线程是当了,还是忘记调用ReleaseMutex),那么其它正以WaitForSingleObject()等待此mutex的线程就会收到WAIT_ABANDONED_0。有了这个值,我就解开难题了。
举例说明:
- HANDLE hBoxKey;
- DWORD WINAPI ThreadFun(LPVOID n){
- //进入关键区域(情景:关上物品箱,拨下钥匙)
- WaitForSingleObject ( hMutex,INFINITE ); //
- //处理一些不可分割的操作。。。。。
- //(情景:转身拿物品,储物品,去购物。。。。)
- //离开关键区域(情景:打开物品箱,拿出储存的物品,插上钥匙)
- ReleaseMutex ( hMutex ); //
- }
- void main(){
- //初始化全局锁(情景:生成物品箱的钥匙 )
- hBoxKey = CreateMutex( NULL,FALSE,NULL );
- //产生两个线程(情景:准备两个人抢一个物品箱 )
- HANDLE hMan1 = CreateThread(NULL,0,ThreadFun, ……);
- HANDLE hMan2 = CreateThread(NULL,0,ThreadFun, ……);
- CloseHandle(hMan1);
- CloseHandle(hMan2);
- //删除全局锁(情景:删除物品箱的钥匙 )
- CloseHandle( hBoxKey ) ;
- }
上一笔记讲了同步机制中的临界区域(Critical Sections)、互斥器(Mutexes),下面介绍同步机制中的另外两种。
信号量(Semaphores)
举个例子:
现在有人要租车,接待他的代理人发现还有3辆车可以用,但正在给他办理手续的时候发现还有三个人也在做同样的动作。现在,就是有四个人想租三辆车。
我们写个程序解决租车问题,方法一就是为每辆车都加一个mutex保护,问题是如果是一家大型出租车公司就需要有成百上千的mutexes了。方法二使用单一的mutex为所有的车辆服务,但一次只能有一个店员出租,这样问题就是客户会减少。
解决:
现在我们将所有的车视为相同,车交到客户手上之前,唯一需要知道的就是现在有几辆车可以用,我们用semaphore来维护这个数字,每一个锁定动作成功,semaphore的现值就会减1。
注意:与mutex不同的是,调用ReleaseSemaphore()的那个线程,并不一定就得是调用wait...()的那个线程。任何线程都可以在任何时间调用ReleaseSemaphore(),解除被任何线程锁定的semaphore。
事件(Event)
//复习
从第三章以来,每章都讲到了一个重要的函数,就是Wait...()系列函数。
第三章判断一个线程是否结束:WaitForSingleObject(HANDLE hthred,...);
第四章中判断是否能够进入锁住互斥器:WatiForSingleObject(hMutex,...);
Wait...()函数会在核心对象被激发时返回。对于hthred而言,线程结束,意味着核心对象被激发;对于hMutex而言, hMutex不再被其它任何线程使用,意味着核心对象被激发。反正对于各种核心对象而言,一定是有某种场景的出现使得核心对象被激发,除了Event这个核心对象。
===============================================
对于Event这个核心对象而言,它的激发状态完全由程序来控制,也就是说,由自己来控制Event的激发或未激发状态( 通过SetEvent() , ResetEvent() )。当线程1因调用Wait…(hEvent)而被阻塞后,一定是某个线程调用了SetEvent( hEvent )使hEvent被设为激发状态,从而使线程1被解除阻塞继续向下运行,具体的运用参见下表:
(使用CreateEvent ()函数构造Event核心对象,CreateEvent ()的第二个参数决定了产生的Event对象是Manual(手工)方式还是Auto(自动)方式;第三个参数决定了决定了产生的Event对象初始状态是激发还是未激发)
函数
|
EVENT对象
[Manual
方式产生
]
|
EVENT对象
[Auto
方式产生
]
|
Wait…()
| 当EVENT对象变为激发状态(使得因调用Wait…()而等待的线程被唤醒)之后,不会被重置为非激发状态(必须调用ResetEvent()) | 当EVENT对象变为激发状态(使得因调用Wait…()而等待的线程被唤醒)之后,自动重置为非激发状态 |
SetEvent() | 把EVENT对象设为激发状态 | 把EVENT对象设为激发状态 |
ResetEvent() | 把EVENT对象设为非激发状态 | 把EVENT对象设为非激发状态 |
PulseEvent() | 把EVENT对象设为激发状态,唤醒“所有”等待中的线程,然后把EVENT对象设为非激发状态 |
把
EVENT对象设为激发状态,唤醒“一个”等待中的线程,然后把
EVENT对象设为非激发状态
|
前面章节介绍了线程创建等过程,现在的问题是:如何在某个线程内终止另外一个正在运行的线程?
windows核心编程中提到终止运行线程的方法:
1)线程函数自己返回;
2)线程通过调用ExitThread函数“杀死”自己,该函数将终止线程的运行并导致操作系统清理该线程使用的所有操作系统资源,但是使用的C/C++资源不会被销毁(慎用);
3)调用TerminateThread函数,不同于ExitThread总是“杀死”主调线程,TreminateThread能杀死任何线程。但是TerminateThread函数时异步的,它告诉系统你想终止线程,但在函数返回时并不能保证线程已经终止了,线程无法正确清理(慎用)。
本章中提到的做法是:使用一个手动重置的Event对象,线程检查该Event对象的状态或是等待它,举例说明。
- //线程退出事件
- HANDLE hExitEvent = null ;
- //一个需要长时间运行的线程
- DWORD WINAPI ThreadFun ( LPVOID p )
- {
- for ( int i =0 ; i < 1000000; i++ ){ //判断线程是否要被强制结束
- /* 可能在这里大家有点疑惑,如果没有调用 SetEvent()的方法,
- hExitEvent 不是总处于未激发状态吗?哪线程不就停在这里不动了?
- 答案是:这里用到了 P74提到的一个Wait… ()一个用法,当time_out
- 为,检查hExitEvent的状态,如果 hExitEvent处于未激发状态,
- 立刻返回WAIT_TIMEOUT,如果 hExitEvent处于激发状态,则立刻
- 返回WAIT_OBJECT_0。 */
- if ( WaitForSingleObject ( hExitEvent , 0 ) !=WAIT_TIMEOUT ){
- //做一些退出线程前的清理工作
- return (DWORD) -1 ;
- }
- //做一些极其消耗时间的处理…
- //……
- //……
- }
- }
- void main()
- {
- HANDLE hThrd;
- //构造EVENT 核心对象,初始状态处于未激发状态, Manual方式
- hExitEvent = CreateEvent ( NULL,TRUE,FALSE,NULL ) ;
- //线程启动
- hThrd = CreateThread ( null,ThreadFun, …);
- Sleep(1000); //等待
- SetEvent ( hExitEvent ) ;//等待了很久,实在等不下去了,发出结束线程退出事件(激发 EVENT核心对象),迫使线程结束
- //等待线程结束
- WaitForSingleObject (hThrd, …);
- CloseHandle ( hThrd );
- }
大致的一个过程就是下图所示:
接下来就是线程优先权,调整线程优先权很简单,无非就是用SetThreadPriority()和GetThreadPriority()等函数。但如何有效,合理的调整线程的优先权却是一个复杂的问题,正如书上所说,“如果你的目标是保持简单,那就还是避免处理[优先权]这个烫山芋吧“。
方法一:使用另一个线程进行I/O。问题是在主线程中操控多个线程,如何设置同步机制、如何处理错误情况都是非常复杂麻烦的。
方法二:使用overlapped I/O(就是所谓的异步asynchronous I/O,第四章讲到了同步机制),你可以让这些I/O操作并行处理。
===================================================================
在对overlapped I/O的具体讨论之前,先介绍Win32中执行I/O的基本函数。
1、打开资源
- HANDLE CreateFile(
- LPCTSTR lpFileName, // 指向文件名的指针
- DWORD dwDesiredAccess, // 访问模式(写 / 读)
- DWORD dwShareMode, // 共享模式
- LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性的指针
- DWORD dwCreationDisposition, // 如何创建
- DWORD dwFlagsAndAttributes, // 文件属性
- HANDLE hTemplateFile // 用于复制文件句柄
- );<span style="font-size:14px">
- </span>
2、读操作
- BOOL ReadFile(
- HANDLE hFile, //欲读文件
- LPVOID lpBuffer, //用于保存读入数据的一个缓冲区
- DWORD nNumberOfBytesToRead, //要读入的字节数
- LPDWORD lpNumberOfBytesRead, //指向实际读取字节数的指针
- LPOVERLAPPED lpOverlapped
- //如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数指向一个OVERLAPPED结构。
- //该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
- );
- BOOL WriteFile(
- HANDLE hFile, // 欲写文件
- LPCVOID lpBuffer, // 数据缓存区指针
- DWORD nNumberOfBytesToWrite, // 你要写的字节数
- LPDWORD lpNumberOfBytesWritten, // 用于保存实际写入字节数的存储区域的指针
- LPOVERLAPPED lpOverlapped // OVERLAPPED结构体指针
- );
4、OVERLAPPED结构
- typedef struct _OVERLAPPED {
- DWORD Internal; //预留给操作系统使用。它指定一个独立于系统的状态,当GetOverlappedResult函数返回时没有设置扩展错误信息ERROR_IO_PENDING时有效
- DWORD InternalHigh; // 预留给操作系统使用。它指定长度的数据转移,当GetOverlappedResult函数返回TRUE时有效
- DWORD Offset; //该文件的位置是从文件起始处的字节偏移量。调用进程设置这个成员之前调用ReadFile或WriteFile函数。当读取或写入命名管道和通信设备时这个成员被忽略设为零
- DWORD OffsetHigh; //指定文件传送的字节偏移量的高位字。当读取或写入命名管道和通信设备时这个成员被忽略设为零。
- HANDLE hEvent; //一个手动重置的event对象,当overlapped I/O完成时即被激发。
- } OVERLAPPED,*LPOVERLAPPED;
接着,我们讲解具体overlap I/O的使用。对于overlapped I/O的讨论,从简单的应用开始,然后再演变到最高级的应用:
- 激发的文件handles;
- 激发的Event对象;
- 异步过程调用(APCs);
- I/O completion ports
一、激发的文件Handles
- //功能:从指定文件的1500位置读入300个字节
- int main()
- {
- BOOL rc;
- HANDLE hFile;
- DWORD numread;
- OVERLAPPED overlap;
- char buf[512];
- char *szPath=” x://xxxx/xxxx” ;
- //检查系统,确定是否支持 overlapped,(NT以上操作系统支持OVERLAPPED)
- CheckOsVersion();
- // 以overlapped 的方式打开文件
- hFile = CreateFile( szPath, // 将被打开的文件
- GENERIC_READ,
- FILE_SHARE_READ|FILE_SHARE_WRITE,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_OVERLAPPED,
- NULL
- );
- // OVERLAPPED结构实始化为
- memset(&overlap, 0, sizeof(overlap));
- //指定文件位置是;
- overlap.Offset = 1500;
- rc = ReadFile(hFile,buf,300,&numread,&overlap);
- //因为是overlapped操作, ReadFile会将读文件请求放入读队列之后立即返回( false),
- //而不会等到文件读完才返回 (true)
- if (rc){
- //文件真是被读完了,rc为 true
- // 或当数据被放入cache中,或操作系统认为它可以很快速地取得数据, rc为true
- }
- else{
- if (GetLastError() == ERROR_IO_PENDING)
- {
- //当错误是ERROR_IO_PENDING,那意味着读文件的操作还在进行中
- //等候,直到文件读完
- WaitForSingleObject(hFile, INFINITE);
- rc = GetOverlappedResult(hFile,&overlap,&numread,FALSE);
- //上面二条语句完成的功能与下面一条语句的功能等价: GetOverlappedResult(hFile,&overlap,&numread,TRUE);
- }
- else{
- //出错了
- }
- }
- CloseHandle(hFile);
- return EXIT_SUCCESS;
- }
二、激发的Event对象
- //程序片段:
- int main()
- {
- int i;
- BOOL rc;
- char *szPath=” x://xxxx/xxxx” ;
- // 以overlapped 的方式打开文件
- ghFile = CreateFile( szPath,
- GENERIC_READ,
- FILE_SHARE_READ|FILE_SHARE_WRITE,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_OVERLAPPED,
- NULL
- );
- for (i=0; i<MAX_REQUESTS; i++){ //对同一文件按几个部分按 overlapped方式同时读
- QueueRequest(i, i*16384, READ_SIZE);//注意看QueueRequest函数是如何运做的
- }
- // 等候所有操作结束;
- //隐含条件:当一个操作完成时,其对应的 event对象会被激活
- WaitForMultipleObjects(
- MAX_REQUESTS, ghEvents, TRUE, INFINITE);
- // 收尾操作
- for (i=0; i<MAX_REQUESTS; i++){
- DWORD dwNumread;
- rc = GetOverlappedResult(
- ghFile,
- &gOverlapped[i],
- &dwNumread,
- FALSE);
- CloseHandle(gOverlapped[i].hEvent);
- }
- CloseHandle(ghFile);
- return EXIT_SUCCESS;
- }
- //当读操作完成以后,gOverlapped[nIndex].hEvent会系统被激发
- int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount){
- //构造一个MANUAL型的 event对象
- ghEvents[nIndex] = CreateEvent(NULL, TRUE, FALSE, NULL) ;
- //将此event 对象置入OVERLAPPED结构
- gOverlapped[nIndex].hEvent = ghEvents[nIndex];
- gOverlapped[nIndex].Offset = dwLocation;
- for (i=0; i<MAX_TRY_COUNT; i++){ //这里用了个循环读,P162有解释不是太理解
- //文件ghFile 唯一
- rc = ReadFile(ghFile, gBuffers[nIndex],&dwNumread,&gOverlapped[nIndex]);
- if (rc) return TRUE; //处理成功
- err = GetLastError();
- if (err == ERROR_IO_PENDING){
- //当错误是ERROR_IO_PENDING,那意味着读文件的操作还在进行中
- return TRUE;
- }
- // 处理一些可恢复的错误
- if ( err == ERROR_INVALID_USER_BUFFER ||
- err == ERROR_NOT_ENOUGH_QUOTA ||
- err == ERROR_NOT_ENOUGH_MEMORY ){
- Sleep(50);
- continue;// 重试
- }
- // 如果GetLastError() 返回的不是以上列出的错误,放弃
- break;
- }
- return -1;
- }
三、使用异步过程调用(APCs)
例子:
- //反馈函数,供overlapped I/O操作完成时调用
- VOID WINAPI FileIOCompletionRoutine(DWORD dwErrorCode, //完成码
- DWORD dwNumberOfBytesTransfered, // 被传递的字节数目
- LPOVERLAPPED lpOverlapped ) // 指向OVERLAPPED 结构的指针
- {
- //这里运用了一个技巧,因为使用 APCs技术;那么OVERLAPPED 结构的event栏位没有什么用,可以用它来传递一些参数。
- //在这里利用它传递序号,以表明是誰完成了 overlapped I/O
- int nIndex = (int )(lpOverlapped->hEvent);
- switch ( nIndex ) //针对nIndex ,做一些操作
- {
- case 1 :
- // 做一些操作。。。
- break;
- case 2 :
- // 做一些操作。。。
- break;
- //…
- }
- //如果所有overlapped I/O都处理完毕,将全局 event激发,使主程序结束
- if (++nCompletionCount == MAX_REQUESTS)
- SetEvent(ghEvent);
- }
- int main()
- {
- ghEvent=CreateEvent(NULL,TRUE,FALSE, NULL );//构造全局event
- ghFile = CreateFile( szPath,
- GENERIC_READ,
- FILE_SHARE_READ|FILE_SHARE_WRITE,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_OVERLAPPED,
- NULL
- ); // 以overlapped 的方式打开文件
- for (i=0; i<MAX_REQUESTS; i++){
- //将同一文件按几个部分按 overlapped方式同时读,注意看QueueRequest函数是如何运做的
- QueueRequest(i, i*16384, READ_SIZE);
- }
- // 等待所有操作完成
- WaitForSingleObjectEx(ghEvent, INFINITE, TRUE );
- CloseHandle(ghFile);
- return EXIT_SUCCESS;
- }
- int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
- {
- gOverlapped[nIndex].hEvent = (HANDLE)nIndex; //记录overlapped 的序号
- gOverlapped[nIndex].Offset = dwLocation; //记录文件开始点
- ReadFileEx(ghFile,gBuffers[nIndex],dwAmount,&gOverlapped[nIndex],FileIOCompletionRoutine); //当读文件完成后自动调用FileIOCompletionRoutine
- }
下面完全转载: