C# asynchronous input output binary stream. Synchronous and asynchronous I/O. select system call

An application programmer doesn't have to think about things like how system programs work with device registers. The system hides details of low-level work with devices from applications. However, the difference between organizing I/O by polling and by interrupts is also reflected at the level of system functions, in the form of functions for synchronous and asynchronous I/O.

Execute a function synchronous I/O involves starting an I/O operation and waiting for that operation to complete. Only after I/O is complete does the function return control to the calling program.

Synchronous I/O is the most familiar way for programmers to work with devices. Standard programming language input/output routines work this way.

Calling a function asynchronous I/O means only starting the corresponding operation. After this, the function immediately returns control to the calling program without waiting for the operation to complete.

Consider, for example, asynchronous data entry. It is clear that the program cannot access data until it is sure that its input is complete. But it is quite possible that the program can do other work for now, rather than stand idle waiting.

Sooner or later, the program must still start working with the entered data, but first make sure that the asynchronous operation has already completed. For this purpose, various operating systems provide tools that can be divided into three groups.

· Waiting for the operation to complete. This is like “the second half of a synchronous operation.” The program first started the operation, then performed some extraneous actions, and now waits for the operation to complete, as with synchronous input/output.

· Checking the completion of the operation. In this case, the program does not wait, but only checks the status of the asynchronous operation. If the input/output is not yet completed, then the program has the opportunity to walk for some time.

· Assignment of completion procedure. In this case, when starting an asynchronous operation, the user program indicates to the system the address of the user procedure or function that should be called by the system after the operation completes. The program itself may no longer be interested in the progress of I/O; the system will remind it of this at the right time by calling the specified function. This method is the most flexible, since the user can provide any actions in the completion procedure.

In a Windows application, all three ways to complete asynchronous operations are available. UNIX does not have asynchronous I/O functions, but the same asynchronous effect can be achieved another way, by running an additional process.

Performing I/O asynchronously can improve performance and provide additional functionality in some cases. Without such a simple form of asynchronous input as “keyboard input without waiting,” numerous computer games and simulators would be impossible. At the same time, the logic of a program using asynchronous operations is more complex than with synchronous operations.

What is the connection mentioned above between synchronous/asynchronous operations and the methods of organizing input/output discussed in the previous paragraph? Answer this question yourself.

We've waited too long for him

What could be more stupid than waiting?

B. Grebenshchikov

During this lecture you will learn

    Using the select system call

    Using the poll system call

    Some aspects of using select/poll in multi-threaded programs

    Standard asynchronous I/O facilities

select system call

If your program primarily deals with I/O operations, you can get the most important benefits of multithreading in a single-threaded program by using the select(3C) system call. On most Unix systems, select is a system call, or at least described in System Manual Section 2 (System Calls), i.e. the link to it should be select(2), but in Solaris 10 the corresponding system manual page is located in section 3C (the C standard library).

I/O devices typically operate much slower than the CPU, so the CPU usually has to wait for them to operate on them. Therefore, in all operating systems, synchronous I/O system calls are blocking operations.

This also applies to network communications - interaction via the Internet is associated with large delays and, as a rule, occurs through a not very wide and/or overloaded communication channel.

If your program operates on multiple I/O devices and/or network connections, it does not benefit it from blocking on an operation involving one of those devices, because in this state it may miss the opportunity to perform I/O from another device without blocking. This problem can be solved by creating threads that work with different devices. In previous lectures, we studied everything necessary to develop such programs. However, there are other means to solve this problem.

The select(3C) system call allows you to wait for multiple devices or network connections to be ready (indeed, most types of objects that can be identified by a file descriptor are ready). When one or more of the handles are ready to transmit data, select(3C) returns control to the program and passes lists of ready handles as output parameters.

select(3C) uses sets of descriptors as parameters. On older Unix systems, sets were implemented as 1024-bit bit masks. In modern Unix systems and other operating systems that implement select, sets are implemented as an opaque type fd_set, over which certain set-theoretic operations are defined, namely clearing a set, including a descriptor in a set, excluding a descriptor from a set, and checking for the presence of a descriptor in a set. Preprocessor directives for performing these operations are described in the select(3C) man page.

On 32-bit versions of UnixSVR4, including Solaris, fd_set is still a 1024-bit mask; in 64-bit versions of SVR4 this is a 65536-bit bit mask. The size of the mask determines not only the maximum number of file descriptors in the set, but also the maximum number of file descriptors in the set. The size of the mask in your version of the system can be determined at compile time by the value of the preprocessor symbol FD_SETSIZE. Unix file descriptor numbering starts at 0, so the maximum file descriptor number is FD_SETSIZE-1.

So if you use select(3C), you need to set limits on the number of handles your process can handle. This can be done with the ulimit(1) shell command before starting the process, or with the setrlimit(2) system call while your process is running. Of course, setrlimit(2) must be called before you start creating file descriptors.

If you need to use more than 1024 handles in a 32-bit program, Solaris10 provides a transition API. To use it you need to define

preprocessor symbol FD_SETSIZE with a numeric value greater than 1024 before including the file . At the same time in the file the necessary preprocessor directives will fire and the fd_set type will be defined as a large bitmask, and select and other system calls in this family will be redefined to use masks of this size.

Some implementations implement fd_set by other means, without using bit masks. For example, Win32 provides select as part of the so-called Winsock API. In Win32, fd_set is implemented as a dynamic array containing file descriptor values. Therefore, you should not rely on knowledge of the internal structure of the fd_set type.

Either way, changing the size of the fd_set bitmask or the internal representation of this type requires recompilation of all programs that use select(3C). In the future, when the architectural limit of 65536 handles per process is raised, a new version of the fd_set and select implementation and a new recompilation of the programs may be required. To avoid this and make it easier to migrate to a new version of the ABI, Sun Microsystems recommends that you avoid using select(3C) and use the poll(2) system call instead. The poll(2) system call is discussed later in this chapter.

The select(3C) system call has five parameters.

intnfds – a number one greater than the maximum file descriptor number in all sets passed as parameters.

fd_set*readfds – Input parameter, a set of descriptors that should be checked for readiness. The end of a file or the closing of a socket is considered a special case of ready to read. Regular files are always considered ready to be read. Also, if you want to check that a listening TCP socket is ready to accept(3SOCKET), it should be included in this set. Also, the output parameter is a set of descriptors ready to be read.

fd_set*writefds – Input parameter, a set of descriptors that should be checked for readiness for writing. A deferred write error is considered a special case of readiness to write. Regular files are always ready to be written. Also, if you want to check for completion of an asynchronousconnect(3SOCKET) operation, the socket should be included in this set. Also, the output parameter is a set of descriptors ready to be written.

fd_set*errorfds – Input parameter, a set of descriptors to check for exception conditions. The definition of an exception depends on the type of file descriptor. For TCP sockets, an exception occurs when out-of-band data arrives. Regular files are always considered to be in exceptional condition. Also, the output parameter is the set of descriptors on which exceptional conditions occurred.

structtimeval*timeout – timeout, time interval specified accurate to microseconds. If this parameter is NULL, select(3C) will wait indefinitely; if a zero time interval is specified in the structure, select(3C) operates in polling mode, that is, it returns control immediately, possibly with empty descriptor sets.

Instead of all parameters of the fd_set* type, you can pass a null pointer. This means that we are not interested in the corresponding event class. select(3C) returns the total number of ready handles in all sets on normal completion (including timeout completion), and -1 on error.

Example 1 uses select(3C) to copy data from a network connection to a terminal, and from the terminal to a network connection. This program is simplified and assumes that writing to the terminal and network connection will never be blocked. Since both the terminal and the network connection have internal buffers, this is usually the case for small data streams.

Example 1: Two-way copying of data between the terminal and the network connection. The example is taken from the book by W.R. Stevens, Unix: Network Application Development. Instead of standard system calls, “wrappers” are used, described in the file “unp.h”

#include "unp.h"

void str_cli(FILE *fp, int sockfd) (

int maxfdp1, stdineof;

char sendline, recvline;

if (stdineof == 0) FD_SET(fileno(fp), &rset);

FD_SET(sockfd, &rset);

maxfdp1 = max(fileno(fp), sockfd) + 1;

Select(maxfdp1, &rset, NULL, NULL, NULL);

if (FD_ISSET(sockfd, &rset)) ( /* socket is readable */

if (Readline(sockfd, recvline, MAXLINE) == 0) (

if (stdineof == 1) return; /* normal termination */

else err_quit("str_cli: server terminated prematurely");

Fputs(recvline, stdout);

if (FD_ISSET(fileno(fp), &rset)) ( /* input is readable */

if (Fgets(sendline, MAXLINE, fp) == NULL) (

Shutdown(sockfd, SHUT_WR); /* send FIN */

FD_CLR(fileno(fp), &rset);

Writen(sockfd, sendline, strlen(sendline));

Note that the Example 1 program recreates the handle sets before each select(3C) call. This is necessary because select(3C) modifies its parameters on normal completion.

select(3C) is considered MT-Safe, but when using it in a multi-threaded program, you need to keep the following point in mind. Indeed, select(3C) itself does not use local data and therefore calling it from multiple threads should not lead to problems. However, if multiple threads are working with overlapping sets of file descriptors, the following scenario is possible:

    Thread 1 calls read from handle s and gets all the data from its buffer

    Thread 2 calls read from handle s and blocks.

To avoid this scenario, handling file descriptors under such conditions should be protected by mutexes or some other mutual exclusion primitives. It is important to emphasize that it is not the select that needs to be protected, but rather the sequence of operations on a specific file descriptor, starting with including the descriptor in the set for select and ending with receiving data from this descriptor, more precisely, updating the pointers in the buffer into which you read this data. If this is not done, even more exciting scenarios are possible, for example:

    Thread 1 includes handle s in the readfds set and calls select.

    select on thread 1 returns as ready to read

    Thread 2 includes handle s in the readfds set and calls select

    select on thread 2 returns as ready to read

    Thread 1 calls read from handle s and gets only part of the data from its buffer

    Thread 2 calls read from handles, receives the data and writes it over the data received by thread 1

In Chapter 10, we'll look at the architecture of an application in which multiple threads share a common pool of file descriptors - the so-called worker thread architecture. In this case, the threads, of course, must indicate to each other which descriptors they are currently working with.

From a multithreaded program development perspective, an important drawback of select(3C)—or perhaps a drawback of the POSIXThreadAPI—is the fact that POSIX synchronization primitives are not file descriptors and cannot be used in select(3C). At the same time, in actual development of multi-threaded I/O programs, it would often be useful to wait for file descriptors to be ready and other threads of its own process to be ready in one operation.

Data input/output

Most of the previous articles are devoted to optimizing computing performance. We've seen many examples of tuning garbage collection routines, parallelizing loops, and recursive algorithms, and even optimizing algorithms to reduce runtime overhead.

For some applications, optimizing the computing aspects provides only marginal performance gains because the bottleneck is I/O operations such as network transfers or disk access. From our own experience, we can say that a significant proportion of performance problems are not associated with the use of suboptimal algorithms or excessive load on the processor, but with inefficient use of I/O devices. Let's look at two situations where I/O optimization can improve overall performance:

    An application may experience severe computational overload due to inefficient I/O operations that add overhead. Even worse, congestion can be so severe that it becomes the limiting factor that prevents you from maximizing the throughput of your I/O devices.

    The I/O device may be underutilized or wasted due to inefficient programming patterns, such as transferring large amounts of data in small chunks or not using full bandwidth.

This article describes general I/O concepts and provides recommendations for improving the performance of any type of I/O. These guidelines apply equally to network applications, disk-intensive processes, and even programs that access custom, high-performance hardware devices.

Synchronous and asynchronous I/O

When executed in synchronous mode, Win32 API I/O functions (such as ReadFile, WriteFile, or DeviceloControl) block program execution until the operation completes. Although this model is very easy to use, it is not very effective. In the time between successive I/O requests, the device may be idle, that is, underutilized.

Another problem with synchronous mode is that the thread of execution wastes time doing any competing I/O operation. For example, a server application that serves many clients simultaneously might want to create a separate thread of execution for each session. These threads, which are idle most of the time, waste memory and can create situations thread thrashing, when many threads of execution simultaneously resume work upon completion of I/O and begin to compete for processor time, which leads to an increase in context switches per unit time and reduced scalability.

The Windows I/O subsystem (including device drivers) operates internally in asynchronous mode—a program can continue executing at the same time as an I/O operation. Almost all modern hardware devices are asynchronous in nature and do not require constant polling to transfer data or determine when an I/O operation has completed.

Most devices support the ability direct memory access (DMA) to transfer data between the device and the computer's RAM without requiring the processor to participate in the operation, and generate an interrupt when the data transfer is complete. Synchronous I/O mode, which is internally asynchronous, is supported only at the Windows application level.

In Win32, asynchronous I/O is called overlapped I/O, comparison of synchronous and overlapped I/O modes is given in the figure below:

When an application makes an asynchronous request to perform an I/O operation, Windows either performs the operation immediately or returns a status code indicating that the operation is pending. The execution thread can then run other I/O operations or perform some calculations. The programmer has several ways to organize the reception of notifications about the completion of I/O operations:

    Win32 Event: The operation waiting for this event will be executed when the I/O completes.

    Calling a custom function using a mechanism asynchronous procedure call (APC): The thread of execution must be in an alertable wait state.

    Receiving notifications via I/O Completion Ports (IOCP): This is usually the most efficient mechanism. We will explore it in detail further.

Some I/O devices (for example, a file opened in unbuffered mode) may provide additional benefits if the application can ensure that there are always a small number of pending I/O requests. To do this, it is recommended to first make several requests to perform I/O operations and for each completed request to issue a new request. This will ensure that the device driver initiates the next operation as quickly as possible, without waiting for the application to complete the next request. But don't go overboard with the amount of data transferred, because this will consume limited kernel memory resources.

I/O completion ports

Windows supports an efficient mechanism for notifying the completion of asynchronous I/O operations called I/O Completion Ports (IOCP). In .NET applications it is available through the method ThreadPool.BindHandle(). This mechanism is used internally by some types in .NET that perform I/O operations: FileStream, Socket, SerialPort, HttpListener, PipeStream, and some .NET Remoting pipes.

The IOCP mechanism, shown in the figure above, binds to multiple I/O handles (sockets, files, and specialized device driver objects) opened asynchronously and to a specific thread of execution. Once the I/O operation associated with such a handle completes, Windows will add a notification to the appropriate IOCP port and pass it to the associated thread of execution for processing.

Using a pool of threads that service notifications and resume execution of threads that initiated asynchronous I/O operations reduces the number of context switches per unit time and increases CPU usage. It's not surprising that high-performance servers such as Microsoft SQL Server use I/O completion ports.

The completion port is created by calling the Win32 API function CreateIoCompletionPort, which is passed the maximum concurrency value (number of threads), a termination key, and an optional I/O object handle. A termination key is a user-defined value that serves to identify various I/O descriptors. You can bind multiple handles to the same IOCP port by repeatedly calling the CreateIoCompletionPort function and passing it a handle to the existing completion port.

To establish communication with the specified IOCP port, user threads call the function GetCompletionStatus and wait for its completion. At any given time, a thread of execution can only be associated with one IOCP port.

Calling a function GetQueuedCompletionStatus blocks thread execution until notified (or a timeout limit has expired), then returns information about the completed I/O operation, such as the number of bytes transferred, the completion key, and the structure of the asynchronous I/O operation. If all threads associated with the I/O port are busy when the notification occurs (that is, there are no threads waiting on the GetQueuedCompletionStatus call), the IOCP mechanism will create a new thread of execution, up to the maximum concurrency value. If a thread calls GetQueuedCompletionStatus and the notification queue is not empty, the function will return immediately without blocking the thread in the operating system kernel.

The IOCP mechanism is able to detect that one of the "busy" threads is actually performing synchronous I/O, and start an additional thread, possibly exceeding the maximum concurrency value. Notifications can also be sent manually, without performing I/O, by calling the function PostQueuedCompletionStatus.

The following code demonstrates an example of using ThreadPool.BindHandle() with a Win32 file handle:

Using System; using System.Threading; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; public class Extensions ( internal static extern SafeFileHandle CreateFile(string lpFileName, EFileAccess dwDesiredAccess, EFileShare dwShareMode, IntPtr lpSecurityAttributes, ECreationDisposition dwCreationDisposition, EFileAttributes dwFlagsAndAttributes, IntPtr hTemplateFile); static unsafe extern bool WriteF ile(SafeFileHandle hFile, byte lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten , System.Threading.NativeOverlapped* lpOverlapped); enum EFileShare: uint ( None = 0x00000000, Read = 0x00000001, Write = 0x00000002, Delete = 0x00000004 ) enum ECreationDisposition: uint ( New = 1, CreateAlways = 2, OpenExisting = 3 , OpenAlways = 4, TruncateExisting = 5 ) enum EFileAttributes: uint ( // ... Some flags are not shown Normal = 0x00000080, Overlapped = 0x40000000, NoBuffering = 0x20000000, ) enum EFileAccess: uint ( // ... Some flags are not shown GenericRead = 0x80000000 , GenericWrite = 0x40000000, ) static long _numBytesWritten; // Brake for the recording stream static AutoResetEvent _waterMarkFullEvent; static int _pendingIosCount; const int MaxPendingIos = 10; // Completion procedure, called by I/O threads static unsafe void WriteComplete(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) ( _numBytesWritten += numBytes; Overlapped ovl = Overlapped.Unpack(pOVERLAP); Overlapped.Free(pOVERLAP); // Notify the writer thread that the number of pending I/O operations // has decreased to the allowed limit if (Interlocked.Decrement(ref _pendingIosCount) = MaxPendingIos) ( _waterMarkFullEvent.WaitOne(); ) ) ) )

Let's look at the TestIOCP method first. This calls the CreateFile() function, which is a P/Invoke engine function used to open or create a file or device. To perform I/O operations asynchronously, you must pass the EFileAttributes.Overlapped flag to the function. If successful, the CreateFile() function returns a Win32 file handle, which we bind to the I/O completion port by calling ThreadPool.BindHandle(). Next, an event object is created that is used to temporarily block the thread that initiated the I/O operation if there are too many of them (the limit is set by the MaxPendingIos constant).

Then the cycle of asynchronous writes begins. At each iteration, a buffer with data to be written is created and Overlapped structure, containing the offset within the file (in this example, writing is always done at offset 0), an event handle passed upon completion of the operation (not used by the IOCP mechanism), and an optional user object IAsyncResult, which can be used to pass state to the completion function.

Next, the Overlapped.Pack() method is called, which takes a completion function and a buffer with data. It creates an equivalent low-level structure for the I/O operation, placing it in unmanaged memory, and pinning a buffer with the data. Freeing unmanaged memory occupied by the low-level structure and detaching the buffer must be done manually.

If there won't be too many I/O operations going on at the same time, we call WriteFile(), passing it the specified low-level structure. Otherwise, we wait until an event occurs indicating that the number of pending operations has fallen below the upper limit.

The WriteComplete completion function is called by a thread from the I/O completion thread pool as soon as the operation is completed. It is passed a pointer to a low-level asynchronous I/O structure, which can be unpacked and converted into a managed Overlapped structure.

To summarize, when working with high-performance I/O devices, use asynchronous I/O with completion ports, either directly by creating and using a custom completion port in an unmanaged library, or by binding Win32 handles to a completion port in .NET using ThreadPool.BindHandle() method.

Thread pool in .NET

The thread pool in .NET can be usefully used for a variety of purposes, each of which creates different types of threads. When discussing parallel computing earlier, we were introduced to the thread pool API, where we used it to parallelize computational tasks. However, thread pools can be used to solve other types of problems:

    Worker threads can handle asynchronous calls to user delegates (such as BeginInvoke or ThreadPool.QueueUserWorkItem).

    I/O completion threads can service notifications coming from the global IOCP port.

    Wait threads can wait for registered events, allowing you to wait for multiple events on a single thread (using WaitForMultipleObjects), up to the Windows upper limit (maximum wait objects = 64). The event wait technique is used to organize asynchronous I/O without using completion ports.

    Timer threads that wait for multiple timers to expire at once.

    Gate threads monitor the CPU usage of threads from the pool, and also change the number of threads (within specified limits) to achieve the best performance.

It is possible to initiate I/O operations that appear to be asynchronous, but are not. For example, calling the ThreadPool.QueueUserWorkItem delegate and then performing a synchronous I/O operation is not a truly asynchronous operation and is no better than performing the same operation on a normal thread of execution.

Memory copy

It's not uncommon for a physical I/O device to return a buffer of data that is copied over and over again until the application finishes processing it. This type of copying can consume a significant portion of the processor's processing power and should be avoided to ensure maximum throughput. Next, we will look at several situations when it is common to copy data, and we will get acquainted with techniques to avoid this.

Unmanaged memory

Working with a buffer in unmanaged memory in .NET is much more difficult than working with a managed byte array, so programmers often copy the buffer to managed memory in search of the easiest way.

If the functions or libraries you use allow you to explicitly specify a buffer in memory or pass your own callback function to allocate a buffer, allocate a managed buffer and pin it in memory so that it can be accessed by both a pointer and a managed reference. If the buffer is large enough (>85,000 bytes), it will be created in heap of large objects (Large Object Heap), so try to reuse existing buffers. If buffer reuse is complicated by the uncertainty of the object's lifetime, use memory pools.

In other cases, where functions or libraries themselves allocate (unmanaged) memory for buffers, you can access that memory directly by pointer (from unsafe code) or by using wrapper classes such as UnmanagedMemoryStream And UnmanagedMemoryAccessor. However, if you need to pass the buffer to some code that only operates on byte arrays or string objects, copying may be unavoidable.

Even if you can't avoid memory copying and some or most of your data is filtered early on, you can avoid unnecessary copying by checking that the data is needed before copying it.

Exporting part of a buffer

Programmers sometimes assume that byte arrays contain only the necessary data, end to end, forcing the calling code to split the buffer (allocate memory for a new byte array and copy only the necessary data). This situation can often be observed in protocol stack implementations. The equivalent native code, in contrast, might take a simple pointer without even knowing whether it points to the beginning of the actual buffer or the middle of it, and a buffer length parameter to determine where the end of the data being processed is.

To avoid unnecessary memory copying, ensure that the offset and length are accepted wherever you accept a byte parameter. Use the length parameter instead of the Length property of the array, and add the offset value to the current indices.

Scatter read and merge write

Scatter read and merge write is the ability supported by the Windows operating system to read into or write data from noncontiguous areas as if they occupied a contiguous area of ​​memory. This functionality is provided in the Win32 API as functions ReadFileScatter And WriteFileGather. The Windows socket library also supports scatter read and merge write capabilities by providing its own functions: WSASend, WSARecv, and others.

Scatter read and merge write can be useful in the following situations:

    When each packet has a fixed size header preceding the actual data. Scatter read and merge write will allow you to avoid having to copy headers every time you need to get a contiguous buffer.

    When it is desirable to eliminate the unnecessary overhead of system calls when performing I/O with multiple buffers.

Compared to the ReadFileScatter and WriteFileGather functions, which require each buffer to be exactly the size of a single page and the handle to be opened in asynchronous and unbuffered mode (which is an even greater limitation), the socket-based scatter read and merge write functions seem more practical because they don't have these restrictions. The .NET Framework supports scatter read and merge write for sockets through overloaded methods Socket.Send() And Socket.Receive() without exporting generic read/write functions.

An example of using the scatter read and merge write functions can be found in the HttpWebRequest class. It combines HTTP headers with the actual data without having to create a contiguous buffer to store it.

File I/O

Typically, file I/O operations are performed through the file system cache, which provides several performance benefits: caching recently used data, read-ahead (reading data from disk in advance), write-lazy (asynchronous writes to disk), and merging writes of small chunks of data. . Prompting Windows to expect file access patterns can provide additional performance gains. If your application does asynchronous I/O and can handle some buffering issues, then avoiding the caching mechanism entirely may be a more efficient solution.

Caching management

When creating or opening files, programmers pass flags and attributes to the CreateFile function, some of which affect the behavior of the caching mechanism:

    Flag FILE_FLAG_SEQUENTIAL_SCAN indicates that the file will be accessed sequentially, possibly skipping some parts, and random access is unlikely. As a result, the cache manager will perform a read-ahead, looking further than usual.

    Flag FILE_FLAG_RANDOM_ACCESS indicates that the file will be accessed in random order. In this case, the cache manager will read ahead slightly, due to the reduced likelihood that the data read ahead will actually be needed by the application.

    Flag FILE_ATTRIBUTE_TEMPORARY indicates that the file is temporary, so that actual writes to the physical media (to prevent data loss) can be deferred.

In .NET, these options are supported (except the last one) using the FileStream constructor overload, which takes a parameter of the FileOptions enumeration type.

Random access has a negative impact on performance, especially when working with disk devices, since it requires moving heads. As technology developed, disk throughput increased only by increasing data storage density, but not by reducing latency. Modern drives are capable of reordering random access queries to reduce the overall time spent moving heads. This technique is called hardware installation of command queuing (Native Command Queuing, NCO). For this technique to be more effective, it is necessary to send several I/O requests to the disk controller at once. In other words, if possible, try to have multiple asynchronous I/O requests pending at once.

Unbuffered I/O

Unbuffered I/O operations are always performed without involving the cache. This approach has its advantages and disadvantages. As with the cache management technique, unbuffered I/O is enabled using the flags and attributes option during file creation, but .NET does not provide access to this capability.

    Flag FILE_FLAG_NO_BUFFERING disables read and write caching, but has no effect on caching performed by the disk controller. This allows you to avoid copying (from the user buffer to the cache) and cache pollution (filling the cache with unnecessary data and displacing the necessary ones). However, unbuffered reads and writes must adhere to alignment requirements.

    The following parameters must be equal to or a multiple of the disk sector size: single transfer size, file offset, and memory buffer address. Typically a disk sector is 512 bytes in size. The latest high-capacity disk drives have a sector size of 4096 bytes, but they can operate in compatibility mode by emulating 512-byte sectors (at the cost of performance).

    Flag FILE_FLAG_WRITE_THROUGH tells the cache manager that it should immediately pop written data from the cache (unless the FILE_FLAG_NO_BUFFERING flag is set) and tells the disk controller that it should write to the physical media immediately without storing the data in an intermediate hardware cache.

Read ahead improves performance by making disk utilization more efficient, even when the application is performing synchronous reads with delays between operations. It's up to Windows to correctly determine which part of the file the application will request next. By disabling buffering, you also disable read ahead, and must keep the disk device busy by performing multiple overlapping I/O operations.

Latency writes also improve the performance of applications that perform synchronous writes by creating the illusion that writes to disk are happening very quickly. The application will be able to improve CPU usage by locking for shorter periods of time. With buffering disabled, the duration of write operations will be the full amount of time required to complete writing data to disk. Therefore, using asynchronous I/O mode when buffering is disabled becomes even more important.

Input and output operations have a slower execution speed than other types of processing. The reasons for this slowdown are the following factors:

Delays caused by time spent searching for the required tracks and sectors on random access devices (disks, CDs).

Latencies caused by the relatively low speed of data exchange between physical devices and system memory.

Delays in data transfer over the network using file servers, data storages, and so on.

In all previous examples, I/O operations are performed synchronized with the flow, so the entire thread is forced to idle until they complete.

This chapter shows how you can arrange for a thread to continue executing without waiting for I/O to complete, which is consistent with thread execution. asynchronous input/output. The various techniques available in Windows are illustrated with examples.

Some of these techniques are used in wait timers, which are also described in this chapter.

Finally, and most importantly, having learned standard asynchronous I/O operations, we can use I/O completion ports, which prove extremely useful in building scalable servers that can support large numbers of clients without creating a separate thread for each of them. Program 14.4 is a modified version of a previously developed server that allows the use of I/O completion ports.

Overview of Windows Asynchronous I/O Methods

Windows handles asynchronous I/O using three techniques.

Multithreaded I/O. Each thread within a process or set of processes performs normal synchronous I/O, but other threads can continue to execute.

Overlapped I/O. Having started a read, write, or other I/O operation, the thread continues its execution. If a thread requires I/O results to continue executing, it waits until an appropriate handle becomes available or a specified event occurs. In Windows 9x, overlapping I/O is supported only for serial devices, such as named pipes.

Completion routines (extended I/O) When I/O operations complete, the system calls a special completion procedure running inside a thread. Extended I/O for disk files is not supported in Windows 9x.

Multithreaded I/O using named pipes is used in the multithreaded server discussed in Chapter 11. The grepMT program (Program 7.1) manages concurrent I/O operations involving multiple files. Thus, we already have a number of programs that perform multi-threaded I/O and thereby provide a form of asynchronous I/O.

Overlapping I/O is the subject of the next section, and the examples there, which implement file conversion (ASCII to UNICODE), use this technique to illustrate the capabilities of sequential file processing. For this purpose, a modified version of program 2.4 is used. Following overlapped I/O, extended I/O using completion routines is considered.

Note

Overlapping and extended I/O methods are often difficult to implement, rarely provide any performance benefits, sometimes even cause performance degradation, and in the case of file I/O can only work under Windows NT. These problems are overcome with the help of threads, Therefore, many readers will probably want to skip straight to the sections on wait timers and I/O completion ports. returning to this section as needed. On the other hand, elements of asynchronous I/O are present in both legacy and new technologies, so these methods are still worth exploring.

Thus, COM technology on the NT5 platform supports asynchronous method calls, so this technique may be useful to many readers who use or are planning to use COM technology. Additionally, asynchronous procedure calls (Chapter 10) have a lot in common with extended I/O, and while I personally prefer using threads, others may prefer this mechanism.

Overlapping I/O

The first thing you need to do to implement asynchronous I/O, whether overlapped or extended, is to set the overlapped attribute on the file or other descriptor. To do this, when calling CreateFile or another function that creates a file, named pipe, or other descriptor, you must specify the FILE_FLAG_OVERLAPPED flag.

In the case of sockets (Chapter 12), whether they were created using the socket or accept function, the overlap attribute is set by default in Winsock 1.1, but must be set explicitly in Winsock 2.0. Overlapping sockets can be used asynchronously on all versions of Windows.

Up to this point, we've used OVERLAPPED structures in conjunction with the LockFileEx function and as an alternative to using the SetFilePointer function (Chapter 3), but they are also an essential element of overlapped I/O. These structures act as optional parameters when calling the four functions below, which can block when operations complete.

Remember that when you specify the FILE_FLAG_OVERLAPPED flag as part of the dwAttrsAndFlags parameter (in the case of the CreateFile function) or the dwOpen-Mode parameter (in the case of the CreateNamedPipe function), the corresponding file or pipe can only be used in overlap mode. Overlapping I/O does not work with anonymous channels.

Note

The documentation for the CreateFile function mentions that using the FILE_FLAG_NO_BUFFERING flag improves the performance of overlapped I/O. Experiments show only a slight improvement in performance (about 15%, which can be verified by experimenting with Program 14.1), but you should ensure that the total size of the data read when performing a ReadFile or WriteFile operation is a multiple of the disk sector size.

Overlapping sockets

One of the most important innovations in Windows Sockets 2.0 (Chapter 12) is the standardization of overlapping I/O. In particular, sockets are no longer automatically created as overlapping file descriptors. The socket function creates a non-overlapping handle. To create an overlapping socket, you must call the WSASocket function, explicitly requesting the creation of an overlapping advice by specifying the value WSA_FLAG_OVERLAPPED for the dwFlags parameter of the WSASocket function.

SOCKET WSAAPI WSASocket(int iAddressFamily, int iSocketType, int iProtocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);

To create a socket, use the WSASocket function instead of the socket function. Any socket returned by accept will have the same properties as the argument.

Consequences of using overlapped I/O

Overlapping I/O is performed asynchronously. This has several implications.

Overlapping I/O operations are not blocked. The ReadFile, WriteFile, TransactNamedPipe, and ConnectNamedPipe functions return without waiting for the I/O operation to complete.

The value returned by a function cannot be used as a criterion for the success or failure of its execution, since the I/O operation has not yet completed at this point. Indicating the I/O progress status requires another mechanism.

Returning the number of bytes transferred is also of little use since the data transfer may not have completed completely. To obtain this kind of information, Windows must provide another mechanism.

A program may attempt to read or write multiple times using the same overlapping file handle. Therefore, the file pointer corresponding to such a descriptor is also insignificant. Therefore, an additional method must be provided to provide an indication of the position in the file for each read or write operation. With named pipes, due to their inherent sequential nature of data processing, this is not an issue.

The program must be able to wait (synchronize) for I/O to complete. If there are multiple pending I/O operations associated with the same handle, the program must be able to determine which operations have already completed. I/O operations do not necessarily complete in the same order in which they began.

To overcome the last two difficulties listed above, OVERLAPPED structures are used.

OVERLAPPED structures

Using the OVERLAPPED structure (specified, for example, by the lpOverlapped parameter of the ReadFile function), you can specify the following information:

The file position (64 bits) at which a read or write operation should begin, as discussed in Chapter 3.

An event (manually reset) that will become signaled upon completion of the associated operation.

Below is the definition of the OVERLAPPED structure.

To specify a file position (pointer), both the Offset and OffsetHigh fields must be used, although the high part of the pointer (OffsetHigh) is 0 in many cases. The Internal and InternalHigh fields, which are reserved for system needs, should not be used.

The hEvent parameter is a descriptor for the event (created using the CreateEvent function). This event can be either named or unnamed, but it is must must be manually reset (see Chapter 8) if used for overlapped I/O; the reasons for this will soon be explained. When the I/O operation completes, the event enters the signal state.

In another possible use case, the hEvent descriptor is NULL; in this case, the program may wait for the file descriptor to become signaled, which may also act as a synchronization object (see the following caveats). The system uses file descriptor signaling states to track the completion of operations if the hEvent descriptor is NULL, that is, the synchronization object in this case is the file descriptor.

Note

For convenience, we will use the term "file handle" to refer to handles specified in calls to ReadFile, WriteFile, and so on, even when referring to handles to a named pipe or device. not a file.

When an I/O function call is made, this event is immediately cleared by the system (set to a non-signaled state). When an I/O operation completes, the event is set to an alarmed state and remains there until used by another I/O operation. An event must be manually reset if multiple threads may be waiting for it to become signaled (although our examples only use one thread), and they may not be in the waiting state when the operation completes.

Even if the file handle is synchronous (that is, created without the FILE_FLAG_OVERLAPPED flag), the OVERLAPPED structure can serve as an alternative to the SetFilePointer function for specifying a position in the file. In this case, the ReadFile function call or other call does not return until the I/O operation completes. We already took advantage of this feature in Chapter 3. Also note that pending I/O operations are uniquely identified by the combination of a file descriptor and the corresponding OVERLAPPED structure.

Listed below are some cautions to take into account.

Avoid reusing an OVERLAPPED structure while the associated I/O operation, if any, has not yet completed.

Likewise, avoid reusing an event specified in the OVERLAPPED structure.

If there are multiple pending requests for the same overlapping handle, use event handles rather than file handles for synchronization.

If the OVERLAPPED structure or event acts as automatic variables within a block, ensure that the block cannot be exited before synchronizing with the I/O operation. Additionally, to avoid resource leakage, care should be taken to close the handle before exiting the block.

Overlapping I/O States

The ReadFile and WriteFile functions, as well as the two named pipe functions above, return immediately when they are used to perform overlapping I/O operations. In most cases, the I/O operation will not have completed at this point, and the return value of the read and write will be FALSE. The GetLastError function will return ERROR_IO_PENDING in this situation.

Once you've finished waiting for the synchronization object (an event or perhaps a file descriptor) to go into a signaling state indicating the operation has completed, you must find out how many bytes were transferred. This is the main purpose of the GetOverlappedResult function.

BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPWORD lpcbTransfer, BOOL bWait)

The specification of a specific I/O operation is provided by the combination of a handle and an OVERLAPPED structure. The TRUE value of bWait specifies that the GetOverlappedResult function should wait until the operation completes; otherwise, the return from the function must be immediate. In any case, this function will return TRUE only after the operation has completed successfully. If the return value of the GetOverlappedResult function is FALSE, then the GetLastError function will return the value ERROR_IO_INCOMPLETE, allowing this function to be called to poll for I/O completion.

The number of bytes transferred is stored in the *lpcbTransfer variable. Always ensure that the OVERLAPPED structure remains unchanged from the time it is used in an overlapped I/O operation.

Canceling overlapping I/O operations

The CancelIO Boolean function allows you to cancel pending overlapped I/O operations associated with the specified handle (this function has only one parameter). All operations initiated by the calling thread that use the handle are canceled. Operations initiated by other threads are not affected by calling this function. Canceled operations end with the error ERROR OPERATION ABORTED.

Example: Using a File Handle as a Synchronization Object

Overlapping I/O is very convenient and easy to implement in cases where there can only be one pending operation. Then, for synchronization purposes, the program can use not the event, but the file descriptor.

The following code snippet shows how a program can initiate a read operation to read part of a file, continue executing to perform other processing, and then enter a state waiting for the file handle to enter the signal state.

OVERLAPPED ov = ( 0, 0, 0, 0, NULL /* Events are not used. */ );
hF = CreateFile(…, FILE_FLAG_OVERLAPPED, …);
ReadFile(hF, Buffer, sizeof(Buffer), &nRead, &ov);
/* Perform other types of processing. nRead is not necessarily reliable.*/
/* Wait for the read operation to complete. */
WaitForSingleObject(hF, INFINITE);
GetOverlappedResult(hF, &ov, &nRead, FALSE);

Example: Converting Files Using Overlapping I/O and Multiple Buffering

Program 2.4 (atou) did the conversion of an ASCII file to UNICODE by file sequential processing, and Chapter 5 showed how to do the same sequential processing by file mapping. Program 14.1 (atouOV) solves the same problem using overlapping I/O and multiple buffers that store fixed-size records.

Figure 14.1 illustrates the organization of a program with four fixed-size buffers. The program is implemented so that the number of buffers can be determined using a symbolic preprocessor constant, but in the following discussion we will assume that there are four buffers.

First, the program initializes all elements of the OVERLAPPED structures that define events and positions in files. Each input and output buffer has a separate OVERLAPPED structure. After this, an overlapping read operation is initiated for each of the input buffers. Next, using the WaitForMultipleObjects function, the program waits for a single event indicating the completion of a read or write. When a read operation completes, the input buffer is copied and converted to the corresponding output buffer, and a write operation is initiated. When the write completes, the next read operation is initiated. Note that the events associated with the input and output buffers are located in a single array, which is used as an argument when calling the WaitForMultipleObjects function.

Rice. 14.1. Asynchronous file update model


Program 14.1. atouOV: File conversion using overlapped I/O
Convert a file from ASCII to Unicode using overlapped I/O. The program only works on Windows NT. */

#define MAX_OVRLP 4 /* Number of overlapping I/O operations.*/
#define REC_SIZE 0x8000 /* 32 KB: Minimum record size to provide acceptable performance. */

/* Each of the elements of the variable arrays defined below */
/* and structures corresponds to a single unfinished operation */
/* overlapping I/O. */
DWORD nin, nout, ic, i;
OVERLAPPED OverLapIn, OverLapOut;
/* The need to use a solid, two-dimensional array */
/* dictated by the WaitForMultipleObjects Function. */
/* Value 0 of the first index corresponds to reading, value 1 to writing.*/
/* In each of the two buffer arrays defined below, the first index */
/* numbers I/O operations. */
LARGE_INTEGER CurPosIn, CurPosOut, FileSize;
/* Total number of records to be processed, calculated */
/* based on the size of the input file. The entry at the end */
/* may be incomplete. */
for (ic = 0; ic< MAX_OVRLP; ic++) {
/* Create read and write events for each OVERLAPPED structure.*/
hEvents = OverLapIn.hEvent /* Read event.*/
hEvents = OverLapOut.hEvent /* Recording event. */
= CreateEvent(NULL, TRUE, FALSE, NULL);
/* Starting positions in the file for each OVERLAPPED structure. */
/* Initiate an overlapped read operation for this OVERLAPPED structure. */
if (CurPosIn.QuadPart< FileSize.QuadPart) ReadFile(hInputFile, AsRec, REC_SIZE, &nin, &OverLapIn);
/* All read operations are performed. Wait for the event to complete and reset it immediately. Read and write events are stored in an event array next to each other. */
iWaits =0; /* Number of I/O operations completed so far. */
while (iWaits< 2 * nRecord) {
ic = WaitForMultipleObjects(2 * MAX_OVRLP, hEvents, FALSE, INFINITE) – WAIT_OBJECT_0;
iWaits++; /* Increment the counter of completed I/O operations.*/
ResetEvent(hEvents);
/* Reading completed. */
GetOverlappedResult(hInputFile, &OverLapIn, &nin, FALSE);
for (i =0; i< REC_SIZE; i++) UnRec[i] = AsRec[i];
WriteFile(hOutputFile, UnRec, nin * 2, &nout, &OverLapOut);
/* Prepare for the next read, which will be initiated after the write operation started above is completed. */
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;
) else if (ic< 2 * MAX_OVRLP) { /* Операция записи завершилась. */
/* Start reading. */
ic –= MAX_OVRLP; /* Set the output buffer index. */
if (!GetOverlappedResult (hOutputFile, &OverLapOut, &nout, FALSE)) ReportError(_T("Read error."), 0, TRUE);
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
if (CurPosIn.QuadPart< FileSize.QuadPart) {
/* Start a new read operation. */
ReadFile(hInputFile, AsRec, REC_SIZE, &nin, &OverLapIn);
/* Close all events. */
for (ic = 0; ic< MAX_OVRLP; ic++) {

Program 14.1 can only run under Windows NT. Windows 9x asynchronous I/O facilities do not allow the use of disk files. Appendix B provides results and comments indicating the relatively poor performance of the atouOV program. Experiments have shown that to achieve acceptable performance, the buffer size must be at least 32 KB, but even in this case, conventional synchronous I/O is faster. In addition, the performance of this program does not improve under SMP conditions, since in this example, which is processing only two files, the CPU is not a critical resource.

Extended I/O Using Completion Routine

There is also another possible approach to using synchronization objects. Instead of having a thread wait for a termination signal from an event or handle, the system can initiate a call to a user-defined termination routine immediately after the I/O operation completes. The completion procedure can then start the next I/O operation and perform any necessary actions to account for the use of system resources. This indirect callback completion procedure is similar to the asynchronous procedure call used in Chapter 10 and requires the use of alertable wait states.

How can a termination procedure be specified in a program? Among the parameters or data structures of the ReadFile and WriteFile functions, there are no parameters that could be used to store the address of the completion procedure. However, there is a family of extended I/O functions that are designated by the suffix "Ex" and contain an additional parameter designed to pass the address of the termination routine. The read and write functions are ReadFileEx and WriteFileEx, respectively. Additionally, use of one of the following standby features is required.

Extended I/O is sometimes called duty input/output(alertable I/O). See the following sections for information on how to use advanced features.

Note

Under Windows 9x, extended I/O cannot work with disk files and communication ports. At the same time, Windows 9x Extended I/O is capable of working with named pipes, mailboxes, sockets, and serial devices.

ReadFileEx, WriteFileEx functions and completion procedures

Extended read and write functions can be used in conjunction with open file, named pipe, and mailbox handles if the corresponding object was opened (created) with the FILE_FLAG_OVERLAPPED flag set. Note that this flag sets the handle attribute, and although overlapped and extended I/O are different, the same flag applies to handles of both types of asynchronous I/O.

Overlapping sockets (Chapter 12) can be used in conjunction with the ReadFileEx and WriteFileEx functions on all versions of Windows.

BOOL ReadFileEx(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpcr)
BOOL WriteFileEx(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpcr)

You're already familiar with both functions, except that each has an additional parameter that allows you to specify the address of the termination routine.

Each function must be provided with an OVERLAPPED structure, but there is no need to provide the hEvent element of this structure; the system ignores it. However, this element is very useful for conveying information such as the sequence number used to distinguish individual input/output operations, as demonstrated in Program 14.2.

Comparing with the ReadFile and WriteFile functions, you will notice that the extended functions do not require parameters to store the number of bytes transferred. This information is passed to a completion function that must be included in the program.

The termination function provides parameters for the byte count, error code, and address of the OVERLAPPED structure. The last of these parameters is required so that the completion procedure can determine which of the outstanding operations has completed. Note that the previous caveats about reusing or destroying OVERLAPPED structures apply here as well as in the case of overlapped I/O.

VOID WINAPI FileIOCompletionRoutine(DWORD dwError, DWORD cbTransferred, LPOVERLAPPED lpo)

As in the case of the CreateThread function, when calling it, the name of some function is also specified, the name FileIOCompletionRoutine is a placeholder, not the actual name of the completion procedure.

The dwError parameter values ​​are limited to 0 (success) and ERROR_HANDLE_EOF (attempting to read out of bounds). The OVERLAPPED structure is the structure that was used by the completed ReadFileEx or WriteFileEx call.

Before the shutdown routine is called by the system, two things must happen:

1. The I/O operation must complete.

2. The calling thread should be in a standby state, notifying the system that it needs to execute the queued completion routine.

How does a thread enter the standby state? It must make an explicit call to one of the standby functions described in the next section. Thus, the thread creates conditions that make premature execution of the termination procedure impossible. A thread can remain in the standby state only for as long as the call to the standby function lasts; After this function returns, the thread exits the specified state.

If both of these conditions are satisfied, the completion procedures queued as a result of the completion of I/O operations are executed. The completion routines run on the same thread that made the initial I/O function call and is in a standby state. Therefore, a thread should enter the standby state only when safe conditions exist for execution of completion routines.

Standby functions

There are five standby functions in total, but below are prototypes of only three of them that are of immediate interest to us:

DWORD WaitForSingleObjectEx(HANDLE hObject, DWORD dwMilliseconds, BOOL bAlertable)
DWORD WaitForMultipleObjectsEx(DWORD cObjects, LPHANDLE lphObjects, BOOL fWaitAll, DWORD dwMilliseconds, BOOL bAlertable)
DWORD SleepEx(DWORD dwMilliseconds, BOOL bAlertable)

Each of the alert functions has a bAlertable flag, which must be set to TRUE in the case of asynchronous I/O. The above functions are extensions of the Wait and Sleep functions you are familiar with.

The duration of the wait intervals is indicated, as usual, in milliseconds. Each of these three functions returns as soon as any of the following situations:

The descriptor(s) transition(s) to a signal state, thereby satisfying the standard requirements of two of the wait functions.

The timeout period has expired.

All completion routines in the thread's queue stop executing and bAlertable is set to TRUE. The completion procedure is queued when its corresponding I/O operation completes (Figure 14.2).

Note that there are no events associated with the OVERLAPPED structures in the ReadFileEx and WriteFileEx functions, so none of the handles supplied when calling the wait function are associated directly with any specific I/O operation. At the same time, the SleepEx function is not associated with synchronization objects, and therefore it is the easiest to use. In the case of the SleepEx function, the timeout interval is typically set to INFINITE, so the function will return only after one or more finalizers currently in the queue have finished executing.

Executing the shutdown procedure and returning from the standby function

When an extended I/O operation finishes executing, its associated completion routine, with its arguments specifying the OVERLAPPED structure, byte count, and error code, is queued for execution.

All completion routines in the thread's queue begin executing when the thread enters the standby state. They are executed one after the other, but not necessarily in the same order in which the I/O operations completed. Return from the standby function occurs only after the completion procedure has returned. This feature is important to ensure the correct functioning of most programs because it assumes that the termination routines have a chance to prepare for the next use of the OVERLAPPED structure and perform other necessary actions to put the program into a known state before returning from the standby state.

If the return from the SleepEx function is due to the execution of one or more queued completion routines, the return value of the function will be WAIT_TO_COMPLETION, and the same value will be returned by the GetLastError function called after one of the wait functions has returned.

In conclusion, we note two points:

1. When calling any of the sleep functions, use INFINITE as the value of the sleep interval parameter. In the absence of a timeout option, functions will return only after all termination routines have completed execution or the descriptors have entered a signal state.

2. To pass information to the completion procedure, it is common to use the hEvent data element of the OVERLAPPED structure, since this field is ignored by the OS.

The interaction between the main thread, completion routines, and standby functions is illustrated in Fig. 14.2. This example starts three concurrent reads, two of which are completed by the time the standby begins executing.

Rice. 14.2. Asynchronous I/O using completion routines

Example: Converting a File Using Advanced I/O

Program 14.3 (atouEX) is a redesigned version of program 14.1. These programs illustrate the difference between the two methods of asynchronous I/O. The atouEx program is similar to Program 14.1, but it moves most of the resource organizing code into a finalizer routine and makes many variables global so that the finalizer can access them. However, Appendix B shows that in terms of performance, atouEx can compete well with other methods that do not use file mapping, while atouOV is slower.

Program 14.2. atouEx: File conversion using extended I/O
Convert a file from ASCII to Unicode using ADVANCED I/O. */
/* atouEX file1 file2 */

#define REC_SIZE 8096 /* Block size is not as critical to performance as it is with atouOV. */
#define UREC_SIZE 2 * REC_SIZE

static VOID WINAPI ReadDone(DWORD, DWORD, LPOVERLAPPED);
static VOID WINAPI WriteDone(DWORD, DWORD, LPOVERLAPPED);

/* The first OVERLAPPED structure is for reading, and the second is for writing. Structures and buffers are allocated for each upcoming operation. */
OVERLAPPED OverLapIn, OverLapOut ;
CHAR AsRec;
WCHAR UnRec;
HANDLE hInputFile, hOutputFile;

int _tmain(int argc, LPTSTR argv) (
hInputFile = CreateFile(argv, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
hOutputFile = CreateFile(argv, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
FileSize.LowPart = GetFileSize(hInputFile, &FileSize.HighPart);
nRecord = FileSize.QuadPart / REC_SIZE;
if ((FileSize.QuadPart % REC_SIZE) != 0) nRecord++;
for (ic = 0; ic< MAX_OVRLP; ic++) {
OverLapIn.hEvent = (HANDLE)ic; /* Overload the event. */
OverLapOut.hEvent = (HANDLE)ic; /* Fields. */
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;
if (CurPosIn.QuadPart< FileSize.QuadPart) ReadFileEx(hInputFile, AsRec, REC_SIZE, &OverLapIn , ReadDone);
CurPosIn.QuadPart += (LONGLONG)REC_SIZE;
/* All read operations are performed. Enter the standby state and remain in it until all records have been processed.*/
while(nDone< 2 * nRecord) SleepEx(INFINITE, TRUE);
_tprintf(_T("ASCII to Unicode conversion complete.\n"));

static VOID WINAPI ReadDone(DWORD Code, DWORD nBytes, LPOVERLAPPED pOv) (
/* Reading completed. Convert data and initiate recording. */
LARGE_INTEGER CurPosIn, CurPosOut;
/* Process the write and initiate the write operation. */
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
CurPosOut.QuadPart = (CurPosIn.QuadPart / REC_SIZE) * UREC_SIZE;
OverLapOut.Offset = CurPosOut.LowPart;
OverLapOut.OffsetHigh = CurPosOut.HighPart;
/* Convert an entry from ASCII to Unicode. */
for (i = 0; i< nBytes; i++) UnRec[i] = AsRec[i];
WriteFileEx(hOutputFile, UnRec, nBytes*2, &OverLapOut, WriteDone);
/* Prepare the OVERLAPPED structure for the next read. */
CurPosIn.QuadPart += REC_SIZE * (LONGLONG)(MAX_OVRLP);
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;

static VOID WINAPI WriteDone(DWORD Code, DWORD nBytes, LPOVERLAPPED pOv) (
/* Recording completed. Initiate the next read operation. */
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
if (CurPosIn.QuadPart< FileSize.QuadPart) {
ReadFileEx(hInputFile, AsRec, REC_SIZE, &OverLapIn, ReadDone);

Asynchronous I/O using multiple threads

Overlapping and extended I/O allow I/O to be performed asynchronously within a single thread, although the OS creates its own threads to support this functionality. In one form or another, methods of this type are often used in many early operating systems to support limited forms of performing asynchronous operations on single-threaded systems.

However, Windows provides multi-threading support, so it is possible to achieve the same effect by performing synchronous I/O operations on multiple, independently running threads. These capabilities were previously demonstrated using multithreaded servers and the grepMT program (Chapter 7). Additionally, threads provide a conceptually consistent and presumably much simpler way to perform asynchronous I/O operations. An alternative to the methods used in Programs 14.1 and 14.2 would be to give each thread its own file descriptor, so that each thread could process every fourth record synchronously.

This way of using threads is demonstrated in the atouMT program, which is not given in the book, but is included in the material posted on the Web site. not only can atouMT run on any version of Windows, but it is also simpler than either of the two asynchronous I/O programs because accounting for resource usage is less complex. Each thread simply maintains its own buffers on its own stack and loops through a sequence of synchronous read, convert, and write operations. At the same time, the program performance remains at a fairly high level.

Note

The program atouMT.c, which is located on the Web site, contains comments about several possible pitfalls that can await you when allowing multiple threads to access the same file simultaneously. In particular, all individual file handles must be created using the CreateHandle function rather than the DuplicateHandle function.

Personally, I prefer to use multi-threaded file processing rather than asynchronous I/O. Threads are easier to program and provide better performance in most cases.

There are two exceptions to this general rule. The first of these, as shown earlier in this chapter, concerns situations in which there may only be one outstanding operation, and a file descriptor can be used for synchronization purposes. A second, more important exception occurs in the case of asynchronous I/O completion ports, which will be discussed at the end of this chapter.

Wait timers

Windows NT supports waitable timers, which are a type of kernel object that waits.

You can always create your own synchronization signal by creating a synchronization thread that sets an event as a result of waking up after calling the Sleep function. In the serverNP program (Program 11.3), the server also uses a timing thread to periodically broadcast its channel name. Therefore, wait timers provide, although somewhat redundant, a convenient way to organize the execution of tasks on a periodic basis or according to a specific schedule. In particular, the wait timer can be configured so that the signal is generated at a strictly defined time.

The wait timer can be either a synchronization timer or a manual-reset notification timer. The synchronization timer is associated with an indirect call function similar to the extended I/O completion procedure, while the wait function is used to synchronize with a manually resettable notification timer.

First, you need to create a timer handle using the CreateWaitableTimer function.

HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpTimerName);

The second parameter, bManualReset, determines what type of timer should be created - synchronizing or notifying. Program 14.3 uses a sync timer, but by changing the comments and parameter setting you can easily turn it into a notification timer. Note that there is also an OpenWaitableTimer function that can use the optional name provided by the third argument.

The timer is initially created in an inactive state, but using the SetWaitableTimer function you can activate it and specify the initial time delay, as well as the length of time between periodically generated signals.

BOOL SetWaitableTimer(HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG IPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume);

hTimer is a valid handle to a timer created using the CreateWaitableTimer function.

The second parameter, pointed to by the pDueTime pointer, can take either positive values ​​corresponding to absolute time or negative values ​​​​corresponding to relative time, with the actual values ​​expressed in 100 nanosecond time units and their format described by the FILETIME structure. Variables of type FILETIME were introduced in Chapter 3 and were already used by us in Chapter 6 in the timep program (Program 6.2).

The interval between signals, specified in the third parameter, is expressed in milliseconds. If this value is set to 0, the timer is signaled only once. If this parameter is positive, the timer is periodic and fires periodically until it is terminated by calling the CancelWaitableTimer function. Negative values ​​of the specified interval are not allowed.

The fourth parameter, pfnCompletionRoutine, is used in the case of a synchronizing timer and specifies the address of the completion routine that is called when the timer enters the signaled state. and provided that the thread goes into standby state. When this procedure is called, one of the arguments is the pointer specified by the fifth parameter, plArgToComplretionRoutine.

After setting the synchronization timer, you can put the thread into a standby state by calling the SleepEx function to allow the termination routine to be called. In the case of a manually reset notification timer, you should wait for the timer handle to enter the signal state. The handle will remain in the signaled state until the next call to the SetWaitableTimer function. The full version of the 14.3 program, available on the Web site, gives you the ability to conduct your own experiments using a timer of your choice in combination with a termination procedure or waiting for the timer handle to go into the signal state, resulting in four different combinations.

The last parameter, fResume, is associated with power saving modes. For more information on this issue, please refer to the help documentation.

The CancelWaitableTimer function is used to cancel the previously called SetWaitableTimer function, but does not change the signal state of the timer. To do this, you need to call the SetWaitableTimer function again.

Example: Using a Wait Timer

Program 14.3 demonstrates the use of a wait timer to generate periodic signals.

Program 14.3. TimeBeep: generating periodic signals
/* Chapter 14. TimeBeep.s. Periodic sound notification. */
/* Usage: TimeBeep period (in milliseconds). */

static BOOL WINAPI Handler(DWORD CntrlEvent);
static VOID APIENTRY Beeper(LPVOID, DWORD, DWORD);
volatile static BOOL Exit = FALSE;

int _tmain(int argc, LPTSTR argv) (
/* Intercepting a key combination to stop the operation. See chapter 4. */
SetConsoleCtrlHandler(Handler, TRUE);
DueTime.QuadPart = –(LONGLONG)Period * 10000;
/* DueTime is negative for the first timeout period and is relative to the current time. The wait period is measured in ms (10 -3 s), and DueTime is measured in units of 100 ns (10 -7 s) to accommodate the FILETIME type. */
hTimer = CreateWaitableTimer(NULL, FALSE /* "Synchronization Timer" */, NULL);
SetWaitableTimer(hTimer, &DueTime, Period, Beeper, &Count, TRUE);
_tprintf(_T("Count = %d\n"), Count);
/* The counter value is incremented in the timer procedure. */
/* Enter the standby state. */
_tprintf(_T("Complete. Counter = %d"), Count);

static VOID APIENTRY Beeper(LPVOID lpCount, DWORD dwTimerLowValue, DWORD dwTimerHighValue) (
*(LPDWORD)lpCount = *(LPDWORD)lpCount + 1;
_tprintf(_T("Generating signal number: %d\n"), *(LPDWORD) lpCount);
Fan(1000 /* Frequency. */, 250 /* Duration (ms). */);

BOOL WINAPI Handler(DWORD CntrlEvent) (
_tprintf(_T("Shutdown\n"));

As you know, there are two main input/output modes: exchange mode with polling of the readiness of the input/output device and exchange mode with interruptions.

In the exchange mode with a readiness poll, input/output control is carried out by the central processor. The central processor sends a command to the control device to perform some action on the input/output device. The latter executes the command, translating signals understandable to the central device and the control device into signals understandable to the input/output device. But the speed of the I/O device is much lower than the speed of the central processor. Therefore, you have to wait for a ready signal for a very long time, constantly polling the corresponding interface line for the presence or absence of the desired signal. It makes no sense to send a new command without waiting for the ready signal indicating the execution of the previous command. In the readiness poll mode, the driver that controls the process of data exchange with an external device executes the “check for readiness signal” command in a loop. Until the ready signal appears, the driver does nothing else. In this case, of course, the CPU time is used irrationally. It is much more profitable to issue an I/O command, forget about the I/O device for a while and move on to executing another program. And the appearance of a readiness signal is interpreted as a request for an interruption from an I/O device. These readiness signals are the interrupt request signals.

The interrupt exchange mode is essentially an asynchronous control mode. In order not to lose connection with the device, a time countdown can be started, during which the device must execute the command and issue an interrupt request signal. The maximum amount of time that an I/O device or its controller must issue an interrupt request signal is often called the configured timeout. If this time expires after issuing the next command to the device, and the device still does not respond, then it is concluded that communication with the device is lost and it is no longer possible to control it. The user and/or task receives the appropriate diagnostic message.

Rice. 4.1. I/O Control

Drivers. operating in interrupt mode, they are a complex set of program modules and can have several sections: a start section, one or more continuation sections, and a termination section.

The startup section initiates the I/O operation. This section is run to turn on an I/O device or simply to initiate another I/O operation.

The continuation section (there may be several of them if the data exchange control algorithm is complex and several interrupts are required to perform one logical operation) carries out the main work of data transfer. The continuation section, in fact, is the main interrupt handler. The interface used may require several sequences of control commands to control I/O, and the device usually only has one interrupt signal. Therefore, after executing the next interrupt section, the interrupt supervisor must transfer control to another section at the next ready signal. This is done by changing the interrupt processing address after executing the next section; if there is only one interrupt section, then it itself transfers control to one or another processing module.

The termination section typically turns off the I/O device or simply ends the operation.

An I/O operation can be performed on the program module that requested the operation in synchronous or asynchronous modes. The meaning of these modes is the same as for the system calls discussed above - synchronous mode means that the program module suspends its work until the I/O operation is completed, and with asynchronous mode the program module continues to execute in multiprogram mode simultaneously with I/O operation. The difference is that an I/O operation can be initiated not only by a user process - in this case, the operation is performed as part of a system call - but also by kernel code, for example, code from the virtual memory subsystem to read a page that is missing from memory.

Rice. 7.1. Two modes of performing I/O operations

The I/O subsystem must provide its clients (user processes and kernel code) with the ability to perform both synchronous and asynchronous I/O operations, depending on the needs of the caller. I/O system calls are often framed as synchronous procedures due to the fact that such operations take a long time and the user process or thread will still have to wait for the results of the operation to be received in order to continue its work. Internal I/O calls from kernel modules are usually executed as asynchronous procedures, since kernel code needs freedom to choose what to do next after an I/O operation is requested. The use of asynchronous procedures leads to more flexible solutions, since based on an asynchronous call, you can always build a synchronous one by creating an additional intermediate procedure that blocks the execution of the calling procedure until the I/O is completed. Sometimes an application process also needs to perform an asynchronous I/O operation, for example, with a microkernel architecture, when part of the code runs in user mode as an application process, but performs operating system functions that require complete freedom of action even after calling the I/O operation.