Win32多线程 -- 异步IO(overlapped IO)

本文详细介绍了Windows环境下利用Win32 API实现异步I/O的方法,包括使用被激发的文件句柄、事件对象及异步过程调用(APCs)进行异步I/O操作的过程与示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. 异步(overlapped) IO之被激发的File Handle

1.1 Win32 IO操作函数

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

CreateFile 可以用来打开各式各样的资源,包括(但不限制于):
    -> 文件(硬盘、软盘、光盘或其他)
    -> 串行口和并行口(serial and parallel ports)
    -> Named pipes
    -> Console

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。

1.2 异步IO的特点

-> 相同的文件 handle, 同一时间读/写文件的许多部分;
-> 没有所谓"目前的文件位置"这样的观念, 每一次读或写的操作都必须包含其文件位置;
-> 发出许多个overlapped请求, 执行次序无法保证;

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结构。

1.3 OVERLAPPED 结构

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

成员名称说 明
Internal通常它被保留. 然而当GetOverlappedResult传回FALSE并且GetLastError并非传回ERROR_IO_PENDING时, 这个栏位将内含一个视系统而定的状态
InternalHigh通常它被保留. 然而当GetOverlappedResult传回TRUE时, 这个栏位将内含"被传输数据的长度"
Offset文件之中开始被读或被写的偏移位置(以字节为单位). 该偏移位置从文件头开始起算. 如果目标设备(例如pipes)并没有支持文件位置, 此栏位将被忽略
OffsetHigh64位的文件偏移位置中, 较高的32位. 如果目标设备(例如pipes)并没有支持文件位置, 此栏位将被忽略
hEvent一个手动重置(manual-reset)的event对象, 当overlapped I/O完成时即被激发. ReadFileEx和WriteFileEx会忽略这个栏位, 彼时它可能被用来传递一个用户自定义的指针

 

1.4 二、被激发的File Handles

最简单的overlapped I/O 类型, 是使用它自己的文件 handle作为同步机制。
使用步骤:
(1) 以FILE_FLAG_OVERLAPPED 告诉Win32说你不要使用默认的同步I/O ;
(2) 设立一个OVERLAPPED结构, 其中内含I/O请求的所有必要参数;
(3) 调用 ReadFile/WriteFile 并以OVERLAPPED结构的地址作为最后一个参数, Win32会在后台处理请求;
(4) WaitForMultipleObjects等待请求结束;
(5) 调用GetOverlappedResult以确定结果如何;
调用GetOverlappedResult, 获得的结果和调用ReadFile/WriteFile而没有指overlapped I/O所传回的东西一样。

BOOL GetOverlappedResult(
	HANDLE hFile,			// 文件或设备的handle
	LPOVERLAPPED lpOverlapped,	// OVERLAPPED结构指针, 指向ReadFile设置的那个OVERLAPPED
	LPDWORD lpNumberOfBytesTransferred, //DWORD指针, 用以表示真正被传输的字节个数
	BOOL bWait 	//用以表示是否要等待操作完成. TRUE 表示要等待. 本函数具有Wait...()的效果
);

如果overlapped操作成功, 此函数传回TRUE; 失败则传回FALSE.GetLastError可获得更详细的失败信息; 如果bWait为FALSE而overlapped还是没有完成, GetLastError会传回ERROR_IO_INCOMPLETE.
实例程序代码从文件C:\WINDOWS\WINFILE.EXE 的第1500 位置处读入 300 个字节

#0001 int ReadSomething()
#0002 {
#0003 	BOOL rc;
#0004 	HANDLE hFile;
#0005 	DWORD numread;
#0006 	OVERLAPPED overlap;
#0007 	char buf[512];
#0008
#0009 	// open the file for overlapped reads
#0010 	hFile = CreateFile( "C:\\WINDOWS\\WINFILE.EXE",
#0011 		GENERIC_READ,
#0012 		FILE_SHARE_READ|FILE_SHARE_WRITE,
#0013 		NULL,
#0014 		OPEN_EXISTING,
#0015 		FILE_FLAG_OVERLAPPED,
#0016 		NULL
#0017 		);
#0018 	if (hFile == INVALID_HANDLE_VALUE)
#0019 		return -1;
#0020
#0021 	// Initialize the OVERLAPPED structure
#0022 	memset(&overlap, 0, sizeof(overlap));
#0023 	overlap.Offset = 1500; // 从1500字节位置开始读取
#0024
#0025 	// Request the data
#0026 	rc = ReadFile(
#0027 		hFile,
#0028 		buf,
#0029 		300,		// 读取300字节
#0030 		&numread,
#0031 		&overlap	// 传入OVERLAPPED结构指针
#0032 	);
#0033
#0034 	if (rc)
#0035 	{// 如果数据已经被放进cache中, 或如果操作系统认为它可以很快速地取得那份数据, 
#0036 		// The data was read successfully   那么文件操作就会在ReadFile返回之前完成, 而 ReadFile将传回TRUE
#0037 	}
#0038 	else
#0039 	{
#0040 		// Was the operation queued ?
#0041 		if (GetLastError() == ERROR_IO_PENDING) // IO请求被系统放入了请求队列
#0042 		{
#0043 			// We could do something else for awhile here...
#0044
#0045 			WaitForSingleObject(hFile, INFINITE); // 等待操作完成
#0046 			rc = GetOverlappedResult( // 获取执行结果
#0047 				hFile,
#0048 				&overlap,
#0049 				&numread,
#0050 				FALSE
#0051 			);
#0052 		}
#0053 		else
#0054 		{
#0055 			// Something went wrong
#0056 		}
#0057 	}
#0058
#0059 	CloseHandle(hFile);
#0060
#0061 	return TRUE;
#0062 }

以文件 handle 作为激发机制,有一个明显的限制,那就是没办法说出到底是哪一个 overlapped 操作完成了

二. 异步(overlapped) IO之被激发的Event对象

以文件 handle 作为激发机制,有一个明显的限制,那就是没办法说出到底是哪一个 overlapped 操作完成了。
OVERLAPPED结构中的最后一个栏位, 是一个event handle。如果你使用文件handle作为激发对象, 那么此栏位可为 NULL 。当这个栏位被设定为一个 event 对象时, 系统核心会在 overlapped 操作完成的时候, 自动将此event对象给激发起来。由于每一个 overlapped 操作都有它自己独一无二的OVERLAPPED 结构, 所以每一个结构都有它自己独一无二的一个 event 对象, 用以代表该操作。所使用的 event 对象必须是手动重置。使用 event 对象搭配 overlapped I/O,你就可以对同一个文件发出多个读取操作和多个写入操作,每一个操作有自己的 event 对象;然后再调用WaitForMultipleObjects() 来等待其中之一(或全部)完成。

// overlapped I/O with signaled events
#0001 // Need to keep the events in their own array so we can wait on them.
#0002 HANDLE ghEvents[MAX_REQUESTS];
#0003 // Keep track of each individual I/O operation
#0004 OVERLAPPED gOverlapped[MAX_REQUESTS];
#0005 // Handle to the file of interest.
#0006 HANDLE ghFile;
#0007 // Need a place to put all this data
#0008 char gBuffers[MAX_REQUESTS][READ_SIZE];
#0009
#0010 int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
#0011 {
#0012 	int i;
#0013 	BOOL rc;
#0014 	DWORD dwNumread;
#0015 	DWORD err;
#0016
#0017 	MTVERIFY(
#0018 		ghEvents[nIndex] = CreateEvent(
#0019 		NULL, // No security
#0020 		TRUE, // Manual reset - extremely important!
#0021 		FALSE, // Initially set Event to non-signaled state
#0022 		NULL // No name
#0023 		)
#0024 	);
#0025 	gOverlapped[nIndex].hEvent = ghEvents[nIndex];
#0026 	gOverlapped[nIndex].Offset = dwLocation;
#0027
#0028 	for (i=0; i<MAX_TRY_COUNT; i++)
#0029 	{
#0030 		rc = ReadFile(
#0031 			ghFile,
#0032 			gBuffers[nIndex],
#0033 			dwAmount,
#0034 			&dwNumread,
#0035 			&gOverlapped[nIndex]
#0036 		);
#0037
#0038 		// Handle success
#0039 		if (rc)
#0040 		{
#0041 			printf("Read #%d completed immediately.\n", nIndex);
#0042 			return TRUE;
#0043 		}
#0044
#0045 		err = GetLastError();
#0046
#0047 		// Handle the error that isn't an error. rc is zero here.
#0048 		if (err == ERROR_IO_PENDING)
#0049 		{
#0050 			// asynchronous i/o is still in progress
#0051 			printf("Read #%d queued for overlapped I/O.\n", nIndex);
#0052 			return TRUE;
#0053 		}
#0054
#0055 		// Handle recoverable error
#0056 		if ( err == ERROR_INVALID_USER_BUFFER ||
#0057 			err == ERROR_NOT_ENOUGH_QUOTA ||
#0058 			err == ERROR_NOT_ENOUGH_MEMORY )
#0059 		{
#0060 			Sleep(50); // Wait around and try later
#0061 			continue;
#0062 		}
#0063
#0064 		// Give up on fatal error.
#0065 		break;
#0066 	}
#0067
#0068 	printf("ReadFile failed.\n");
#0069 	return -1;
#0070 }

由于操作系统的运作原理, 你为 overlapped I/O 所指定的缓冲区必须在内存中锁定。如果系统(或同一个程序中)有太多缓冲区在同一时间被锁定,可能对效率是一个很大的伤害。因此,系统有时候必须为程序"降温"(降低速度啦)。但是当然不能把它们阻塞住(blocking),因为那又使得 overlapped I/O 失去意义。 Win32 会传回一个像 ERROR_ INVALID_USER_BUFFER 那样的错误信息,表示此刻没有足够的资源来处理这个"I/O 请求"。

使用 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 数组结合起来。

三. 异步(overlapped) IO之异步过程调用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 数组结合起来。

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

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

如果线程因为以下五个函数而处于等待状态,而其 "alertable" 标记被设为 TRUE ,则该线程就是处于 "alertable" 状态:
    -> SleepEx
    -> WaitForSingleObjectEx
    -> WaitForMultipleObjectsEx
    -> MsgWaitForMultipleObjectsEx
    -> SignalObjectAndWait

I/O completion routine 应该有这样的型式:
VOID WINAPI FileIOCompletionRoutine(
    DWORD dwErrorCode,    // 0: 操作完成;  ERROR_HANDLE_EOF: 操作已经到了文件尾端
    DWORD dwNumberOfBytesTransferred, // 真正被操作的数据字节数
    LPOVERLAPPED lpOverlapped // OVERLAPPED结构指针, 指向WriteFileEx/ReadFileEx传入的重叠结构
};

使用 APCs 时, OVERLAPPED 结构中的 hEvent 栏位不需要用来放置一个 event handle 。Win32 文件上说此时 hEvent 栏位可以由程序员自由运用。那么最大的用途就是:首先配置一个结构,描述数据来自哪里,或是要对数据进行一些什么操作,然后将 hEvent 栏位设定指向该结构。

// Overlapped I/O with APCs
/*
* IoByAPC.c
*
* Sample code for Multithreading Applications in Win32
* This is from Chapter 6, Listing 6-3
*
* Demonstrates how to use APC's (asynchronous
* procedure calls) instead of signaled objects
* to service multiple outstanding overlapped
* operations on a file.
*/

#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "MtVerify.h"

//
// Constants
//
#define MAX_REQUESTS 5
#define READ_SIZE 512
#define MAX_TRY_COUNT 5

//
// Function prototypes
//
void CheckOsVersion();
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount);


//
// Global variables
//

// Need a single event object so we know when all I/O is finished
HANDLE ghEvent;
// Keep track of each individual I/O operation
OVERLAPPED gOverlapped[MAX_REQUESTS];
// Handle to the file of interest.
HANDLE ghFile;
// Need a place to put all this data
char gBuffers[MAX_REQUESTS][READ_SIZE];
int nCompletionCount;

/*
* I/O Completion routine gets called
* when app is alertable (in WaitForSingleObjectEx)
* and an overlapped I/O operation has completed.
*/
VOID WINAPI FileIOCompletionRoutine(
		DWORD dwErrorCode, // completion code
 		DWORD dwNumberOfBytesTransfered, //number of bytes transferred
 		LPOVERLAPPED lpOverlapped // pointer to structure with I/O information
 		)
{
	// The event handle is really the user defined data
	int nIndex = (int)(lpOverlapped->hEvent);
	printf("Read #%d returned %d. %d bytes were read.\n",
			nIndex,
			dwErrorCode,
			dwNumberOfBytesTransfered);

	if (++nCompletionCount == MAX_REQUESTS)
		SetEvent(ghEvent); // Cause the wait to terminate
}


int main()
{
	int i;
	char szPath[MAX_PATH];
	CheckOsVersion();
	// Need to know when to stop
	MTVERIFY(
		ghEvent = CreateEvent(
			NULL,// No security
			TRUE,// Manual reset-extremely important!
			FALSE,// Initially set Event to non-signaled state
			NULL // No name
		)
	);

 	GetWindowsDirectory(szPath, sizeof(szPath));
 	strcat(szPath, "\\WINHLP32.EXE");
 	// Open the file for overlapped reads
 	ghFile = CreateFile( szPath,
 		GENERIC_READ,
 		FILE_SHARE_READ|FILE_SHARE_WRITE,
		NULL,
 		OPEN_EXISTING,
 		FILE_FLAG_OVERLAPPED,
 		NULL
 		);
 	if (ghFile == INVALID_HANDLE_VALUE)
 	{
 		printf("Could not open %s\n", szPath);
 		return -1;
 	}

 	// Queue up a few requests
 	for (i=0; i<MAX_REQUESTS; i++)
 	{
		// Read some bytes every few K
 		QueueRequest(i, i*16384, READ_SIZE);
 	}

 	printf("QUEUED!!\n");

		// Wait for all the operations to complete.
 	for (;;)
 	{
		DWORD rc;
 		rc = WaitForSingleObjectEx(ghEvent, INFINITE, TRUE ); // 使得线程进入alertable状态, APCs可以执行
 		if (rc == WAIT_OBJECT_0)
			break;
 		MTVERIFY(rc == WAIT_IO_COMPLETION); // 在处理完 APCs 之后, WaitForSingleObjectEx会传回WAIT_IO_COMPLETION 
 	}

 	CloseHandle(ghFile);

 	return EXIT_SUCCESS;
 }

/*
* Call ReadFileEx to start an overlapped request.
* Make sure we handle errors that are recoverable.
*/
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
{
	int i;
 	BOOL rc;
	DWORD err;

 	gOverlapped[nIndex].hEvent = (HANDLE)nIndex;
	gOverlapped[nIndex].Offset = dwLocation;

	for (i=0; i<MAX_TRY_COUNT; i++)
 	{
 		rc = ReadFileEx(
 			ghFile,
 			gBuffers[nIndex],
 			dwAmount,
			&gOverlapped[nIndex],
 			FileIOCompletionRoutine
 		);

		// Handle success
		if (rc)
		{
 			// asynchronous i/o is still in progress
 			printf("Read #%d queued for overlapped I/O.\n",nIndex);
			return TRUE;
 		}

 		err = GetLastError();

		// Handle recoverable error
 		if ( err == ERROR_INVALID_USER_BUFFER ||
 			err == ERROR_NOT_ENOUGH_QUOTA ||
 			err == ERROR_NOT_ENOUGH_MEMORY )
 		{
 			Sleep(50); // Wait around and try later
 			continue;
		}

 		// Give up on fatal error.
 		break;
 	}

 	printf("ReadFileEx failed.\n");
 	return -1;
}

//
// Make sure we are running under an operating
// system that supports overlapped I/O to files.
//
void CheckOsVersion()
{
	OSVERSIONINFO ver;
	BOOL bResult;

 	ver.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

 	bResult = GetVersionEx((LPOSVERSIONINFO) &ver);

 	if ( (!bResult) ||
 		(ver.dwPlatformId != VER_PLATFORM_WIN32_NT) )
 	{
		fprintf(stderr, "IoByAPC must be run under Windows NT.\n");
 		exit(EXIT_FAILURE);
	}
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值