Flux binar de ieșire de intrare asincronă C#. I/O sincron și asincron. selectați apelul de sistem

Un programator de aplicații nu trebuie să se gândească la lucruri precum modul în care programele de sistem funcționează cu registrele dispozitivului. Sistemul ascunde detalii despre lucrul la nivel scăzut cu dispozitivele din aplicații. Totuși, diferența dintre organizarea I/O prin interogare și prin întreruperi se reflectă și la nivelul funcțiilor sistemului, sub forma funcțiilor pentru I/O sincron și asincron.

Executați o funcție I/O sincron implică pornirea unei operațiuni I/O și așteptarea finalizării acelei operațiuni. Numai după ce I/O este completă, funcția returnează controlul programului apelant.

I/O sincron este cel mai familiar mod pentru programatori de a lucra cu dispozitive. Rutinele standard de intrare/ieșire a limbajului de programare funcționează în acest fel.

Apelarea unei funcții I/O asincron înseamnă doar începerea operației corespunzătoare. După aceasta, funcția returnează imediat controlul programului apelant, fără a aștepta finalizarea operației.

Luați în considerare, de exemplu, introducerea asincronă a datelor. Este clar că programul nu poate accesa datele până când nu este sigur că introducerea sa este completă. Dar este foarte posibil ca programul să poată face alte lucrări deocamdată, mai degrabă decât să aștepte inactiv.

Mai devreme sau mai târziu, programul trebuie să înceapă să lucreze cu datele introduse, dar mai întâi asigurați-vă că operația asincronă s-a încheiat deja. În acest scop, diverse sisteme de operare oferă instrumente care pot fi împărțite în trei grupuri.

· Se așteaptă finalizarea operațiunii. Aceasta este ca „a doua jumătate a unei operații sincrone”. Programul a început mai întâi operațiunea, apoi a efectuat unele acțiuni străine și acum așteaptă sfârșitul operației, ca și în cazul intrării/ieșirii sincrone.

· Verificarea finalizării operațiunii. În acest caz, programul nu așteaptă, ci doar verifică starea operației asincrone. Dacă intrarea/ieșirea nu este încă finalizată, atunci programul are posibilitatea de a merge ceva timp.

· Atribuirea procedurii de finalizare. În acest caz, la pornirea unei operații asincrone, programul utilizator indică sistemului adresa procedurii sau funcției utilizator care ar trebui apelată de sistem după finalizarea operației. Este posibil ca programul în sine să nu mai fie interesat de progresul I/O, sistemul îi va reaminti acest lucru la momentul potrivit apelând funcția specificată. Această metodă este cea mai flexibilă, deoarece utilizatorul poate furniza orice acțiuni în procedura de finalizare.

Într-o aplicație Windows, sunt disponibile toate cele trei moduri de a finaliza operațiunile asincrone. UNIX nu are funcții I/O asincrone, dar același efect asincron poate fi obținut în alt mod, prin rularea unui proces suplimentar.

Efectuarea I/O asincron poate îmbunătăți performanța și poate oferi funcționalități suplimentare în unele cazuri. Fără o formă atât de simplă de intrare asincronă precum „introducerea de la tastatură fără așteptare”, numeroase jocuri și simulatoare pe calculator ar fi imposibile. În același timp, logica unui program care utilizează operații asincrone este mai complexă decât cu operațiile sincrone.

Care este legătura menționată mai sus între operațiile sincrone/asincrone și metodele de organizare a intrărilor/ieșirilor discutate în paragraful anterior? Răspundeți singuri la această întrebare.

L-am așteptat prea mult

Ce poate fi mai prost decât așteptarea?

B. Grebenshchikov

În timpul acestei prelegeri vei învăța

    Folosind apelul de sistem select

    Folosind apelul de sistem de sondare

    Unele aspecte ale utilizării select/poll în programe cu mai multe fire

    Facilități standard de I/O asincrone

selectați apelul de sistem

Dacă programul dvs. se ocupă în principal de operațiuni I/O, puteți obține cele mai importante beneficii ale multithreading-ului într-un program cu un singur thread folosind apelul de sistem select(3C). Pe majoritatea sistemelor Unix, select este un apel de sistem, sau cel puțin descris în Manualul de sistem Secțiunea 2 (Apeluri de sistem), de exemplu. linkul către acesta ar trebui să fie select(2), dar în Solaris 10 pagina de manual de sistem corespunzătoare se află în secțiunea 3C (biblioteca standard C).

Dispozitivele I/O funcționează de obicei mult mai lent decât CPU, așa că CPU-ul trebuie de obicei să aștepte ca acestea să efectueze operațiuni pe ele. Prin urmare, în toate sistemele de operare, apelurile de sistem I/O sincrone blochează operațiunile.

Acest lucru se aplică și comunicațiilor în rețea - interacțiunea prin Internet este asociată cu întârzieri mari și, de regulă, are loc printr-un canal de comunicare nu foarte larg și/sau supraîncărcat.

Dacă programul dumneavoastră funcționează pe mai multe dispozitive I/O și/sau conexiuni de rețea, nu îl beneficiază de blocarea unei operațiuni care implică unul dintre acele dispozitive, deoarece în această stare poate rata oportunitatea de a efectua I/O de pe alt dispozitiv fără blocare. Această problemă poate fi rezolvată prin crearea de fire care funcționează cu diferite dispozitive. În prelegerile anterioare, am studiat tot ceea ce era necesar pentru a dezvolta astfel de programe. Cu toate acestea, există și alte mijloace pentru a rezolva această problemă.

Apelul de sistem select(3C) vă permite să așteptați ca mai multe dispozitive sau conexiuni de rețea să fie gata (într-adevăr, majoritatea tipurilor de obiecte care pot fi identificate printr-un descriptor de fișier sunt gata). Când unul sau mai multe dintre mânere sunt gata să transmită date, select(3C) returnează controlul programului și transmite liste de mânere gata ca parametri de ieșire.

select(3C) folosește seturi de descriptori ca parametri. Pe sistemele Unix mai vechi, seturile au fost implementate ca măști de biți de 1024 de biți. În sistemele Unix moderne și alte sisteme de operare care implementează select, seturile sunt implementate ca un tip opac fd_set, peste care sunt definite anumite operații teoretice, și anume ștergerea unui set, inclusiv un descriptor într-un set, excluderea unui descriptor dintr-un set, și verificarea prezenței unui descriptor într-un set. Directivele de preprocesor pentru efectuarea acestor operațiuni sunt descrise în pagina de manual select(3C).

Pe versiunile pe 32 de biți ale UnixSVR4, inclusiv Solaris, fd_set este încă o mască de 1024 de biți; în versiunile pe 64 de biți ale SVR4, aceasta este o mască de 65536 de biți. Mărimea măștii determină nu numai numărul maxim de descriptori de fișiere din set, ci și numărul maxim de descriptori de fișiere din set. Mărimea măștii din versiunea dumneavoastră a sistemului poate fi determinată în timpul compilării prin valoarea simbolului preprocesorului FD_SETSIZE. Numerotarea descriptorului de fișiere Unix începe de la 0, deci numărul maxim de descriptor de fișier este FD_SETSIZE-1.

Deci, dacă utilizați select(3C), trebuie să setați limite pentru numărul de handle pe care procesul dumneavoastră le poate gestiona. Acest lucru se poate face cu comanda shell ulimit(1) înainte de a începe procesul sau cu apelul de sistem setrlimit(2) în timp ce procesul dumneavoastră rulează. Desigur, setrlimit(2) trebuie apelat înainte de a începe să creați descriptori de fișiere.

Dacă trebuie să utilizați mai mult de 1024 de puncte într-un program pe 32 de biți, Solaris10 oferă un API de tranziție. Pentru a-l folosi trebuie să definiți

simbolul preprocesorului FD_SETSIZE cu o valoare numerică mai mare de 1024 înainte de a include fișierul . În același timp în dosar directivele de preprocesor necesare se vor declanșa și tipul fd_set va fi definit ca o mască de biți mare, iar apelurile select și alte sistem din această familie vor fi redefinite pentru a utiliza măști de această dimensiune.

Unele implementări implementează fd_set prin alte mijloace, fără a utiliza măști de biți. De exemplu, Win32 oferă select ca parte a așa-numitului Winsock API. În Win32, fd_set este implementat ca o matrice dinamică care conține valori de descriptor de fișiere. Prin urmare, nu ar trebui să vă bazați pe cunoașterea structurii interne a tipului fd_set.

În orice caz, modificările mărimii măștii de biți fd_set sau reprezentarea internă de acest tip necesită o recompilare a tuturor programelor care folosesc select(3C). În viitor, când limita arhitecturală de 65536 de mânere per proces este ridicată, poate fi necesară o nouă versiune a implementării fd_set și select și o nouă recompilare a programelor. Pentru a evita acest lucru și pentru a facilita migrarea la o nouă versiune a ABI, Sun Microsystems vă recomandă să evitați să utilizați select(3C) și să utilizați apelul de sistem poll(2). Apelul de sistem poll(2) este discutat mai târziu în acest capitol.

Apelul de sistem select(3C) are cinci parametri.

intnfds – un număr cu unu mai mare decât numărul maxim de descriptor de fișier în toate seturile trecute ca parametri.

fd_set*readfds – Parametru de intrare, un set de descriptori care ar trebui verificați pentru lizibilitate. Sfârșitul unui fișier sau închiderea unui socket este considerat un caz special de gata de citit. Fișierele obișnuite sunt întotdeauna considerate gata pentru a fi citite. De asemenea, dacă doriți să verificați dacă un socket TCP care ascultă este gata de acceptat (3SOCKET), acesta ar trebui inclus în acest set. De asemenea, parametrul de ieșire este un set de descriptori gata de citire.

fd_set*writefds – Parametru de intrare, un set de descriptori care ar trebui verificați pentru a fi pregătiți pentru scriere. O eroare de scriere amânată este considerată un caz special de disponibilitate pentru scriere. Fișierele obișnuite sunt întotdeauna gata pentru a fi scrise. De asemenea, dacă doriți să verificați finalizarea unei operațiuni de conectare asincronă (3SOCKET), soclul ar trebui să fie inclus în acest set. De asemenea, parametrul de ieșire este un set de descriptori gata de scris.

fd_set*errorfds – Parametru de intrare, un set de descriptori pentru a verifica condițiile de excepție. Definiția unei excepții depinde de tipul de descriptor al fișierului. Pentru socket-urile TCP, apare o excepție când sosesc date în afara benzii. Fișierele obișnuite sunt întotdeauna considerate a fi în stare excepțională. De asemenea, parametrul de ieșire este setul de descriptori în care au apărut condiții excepționale.

structtimeval*timeout – timeout, interval de timp specificat cu precizie la microsecunde. Dacă acest parametru este NULL, select(3C) va aștepta nelimitat; dacă în structură este specificat un interval de timp zero, select(3C) operează în modul polling, adică returnează imediat controlul, eventual cu seturi de descriptori goale.

În loc de toți parametrii de tip fd_set*, puteți trece un pointer nul. Aceasta înseamnă că nu suntem interesați de clasa de eveniment corespunzătoare select(3C) returnează numărul total de mânere gata în toate seturile la finalizarea normală (inclusiv la finalizarea timeout) și -1 la eroare.

Exemplul 1 folosește select(3C) pentru a copia date de la o conexiune de rețea la un terminal și de la terminal la o conexiune de rețea. Acest program este simplificat și presupune că scrierea la terminal și conexiunea la rețea nu vor fi niciodată blocate. Deoarece atât terminalul, cât și conexiunea la rețea au tampon interne, acesta este de obicei cazul fluxurilor de date mici.

Exemplul 1: Copiere bidirecțională a datelor între terminal și conexiunea la rețea. Exemplul este preluat din cartea lui W.R. Stevens, Unix: Dezvoltarea aplicațiilor de rețea. În loc de apeluri standard de sistem, sunt folosite „wrappers”, descrise în fișierul „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;

Selectați(maxfdp1, &rset, NULL, NULL, NULL);

if (FD_ISSET(sockfd, &rset)) ( /* socket-ul este lizibil */

dacă (Readline(sockfd, recvline, MAXLINE) == 0) (

if (stdineof == 1) return; /* terminare normală */

else err_quit("str_cli: server terminat prematur");

Fputs(recvline, stdout);

if (FD_ISSET(fileno(fp), &rset)) ( /* intrarea este lizibilă */

dacă (Fgets(sendline, MAXLINE, fp) == NULL) (

Închidere (sockfd, SHUT_WR); /* trimite FIN */

FD_CLR(fileno(fp), &rset);

Scris(sockfd, sendline, strlen(sendline));

Rețineți că programul Exemplul 1 recreează seturile de mânere înainte de fiecare apel select(3C). Acest lucru este necesar deoarece select(3C) își modifică parametrii la finalizarea normală.

select(3C) este considerat MT-Safe, dar atunci când îl utilizați într-un program cu mai multe fire, trebuie să aveți în vedere următorul punct. Într-adevăr, select(3C) în sine nu utilizează date locale și, prin urmare, apelarea acesteia din mai multe fire de execuție nu ar trebui să ducă la probleme. Cu toate acestea, dacă mai multe fire de execuție funcționează cu seturi suprapuse de descriptori de fișiere, este posibil următorul scenariu:

    Thread-ul 1 citește de la mânerul s și primește toate datele din buffer-ul său

    Apelurile Thread 2 citite din mânere și blocuri.

Pentru a evita acest scenariu, lucrul cu descriptori de fișiere în astfel de condiții ar trebui protejat de mutexuri sau de alte primitive de excludere reciprocă. Este important de subliniat că nu selectia trebuie protejată, ci mai degrabă succesiunea operațiunilor pe un anumit descriptor de fișier, începând cu includerea descriptorului în setul pentru select și terminând cu primirea datelor de la acest descriptor, mai precis. , actualizând pointerii din memoria tampon în care citiți aceste date. Dacă nu se face acest lucru, sunt posibile scenarii și mai interesante, de exemplu:

    Thread-ul 1 include mânerele în setul readfds și apelurile select.

    selectați pe firul 1 revine ca fiind gata de citit

    Thread-ul 2 include mânerele în setul readfds și apelurile select

    selectați pe firul 2 returnează ca fiind gata de citit

    Thread-ul 1 apelează citit din mânerul s și primește doar o parte din date din buffer-ul său

    Thread 2 apelează citit de la handle, primește datele și le scrie peste datele primite de thread 1

În Capitolul 10, ne vom uita la arhitectura unei aplicații în care mai multe fire de execuție împărtășesc un grup comun de descriptori de fișiere - așa-numita arhitectură worker thread. În acest caz, firele, desigur, trebuie să indice reciproc cu ce descriptori lucrează în prezent.

Din perspectiva dezvoltării programelor cu mai multe fire, un dezavantaj important al select(3C) - sau poate un dezavantaj al POSIXthreadAPI - este faptul că primitivele de sincronizare POSIX nu sunt descriptori de fișier și nu pot fi utilizate în select(3C). În același timp, în dezvoltarea actuală a programelor I/O cu mai multe fire de execuție, ar fi adesea util să așteptați ca descriptorii de fișiere să fie gata și alte fire ale propriului proces să fie gata într-o singură operație.

Intrare/ieșire date

Majoritatea articolelor anterioare sunt dedicate optimizării performanței de calcul. Am văzut multe exemple de reglare a rutinelor de colectare a gunoiului, paralelizare a buclelor și algoritmi recursivi și chiar optimizarea algoritmilor pentru a reduce supraîncărcarea timpului de rulare.

Pentru unele aplicații, optimizarea aspectelor de calcul oferă doar câștiguri marginale de performanță, deoarece blocajul îl reprezintă operațiunile I/O, cum ar fi transferurile de rețea sau accesul la disc. Din propria noastră experiență, putem spune că o proporție semnificativă a problemelor de performanță nu sunt asociate cu utilizarea unor algoritmi suboptimi sau cu încărcare excesivă pe procesor, ci cu utilizarea ineficientă a dispozitivelor I/O. Să ne uităm la două situații în care optimizarea I/O poate îmbunătăți performanța generală:

    O aplicație poate avea o suprasarcină de calcul severă din cauza operațiunilor I/O ineficiente care adaugă supraîncărcare. Și mai rău, congestia poate fi atât de gravă încât devine factorul limitator care vă împiedică să maximizați debitul dispozitivelor dvs. I/O.

    Dispozitivul I/O poate fi subutilizat sau irosit din cauza modelelor de programare ineficiente, cum ar fi transferul de cantități mari de date în bucăți mici sau neutilizarea lățimii de bandă completă.

Acest articol descrie concepte generale de I/O și oferă recomandări pentru îmbunătățirea performanței oricărui tip de I/O. Aceste linii directoare se aplică în mod egal aplicațiilor de rețea, proceselor care folosesc intens disc și chiar programelor care accesează dispozitive hardware personalizate, de înaltă performanță.

I/O sincron și asincron

Când sunt executate în modul sincron, funcțiile Win32 API I/O (cum ar fi ReadFile, WriteFile sau DeviceloControl) blochează execuția programului până la finalizarea operației. Deși acest model este foarte ușor de utilizat, nu este foarte eficient. În timpul dintre solicitările I/O succesive, dispozitivul poate fi inactiv, adică subutilizat.

O altă problemă cu modul sincron este că firul de execuție pierde timp făcând orice operație I/O concurentă. De exemplu, o aplicație server care deservește mai mulți clienți simultan ar putea dori să creeze un fir de execuție separat pentru fiecare sesiune. Aceste fire, care sunt inactiv de cele mai multe ori, irosesc memorie și pot crea situații baterea firului, când multe fire de execuție reiau simultan funcționarea la finalizarea I/O și încep să concureze pentru timpul procesorului, ceea ce duce la o creștere a comutărilor de context pe unitatea de timp și la o scalabilitate redusă.

Subsistemul Windows I/O (inclusiv driverele de dispozitiv) funcționează intern în modul asincron — un program poate continua să se execute în același timp cu o operație I/O. Aproape toate dispozitivele hardware moderne sunt de natură asincronă și nu necesită interogare constantă pentru a transfera date sau pentru a determina când s-a încheiat o operațiune I/O.

Majoritatea dispozitivelor acceptă această capacitate acces direct la memorie (DMA) pentru a transfera date între dispozitiv și memoria RAM a computerului fără a solicita procesorului să participe la operație și a genera o întrerupere când transferul de date este complet. Modul I/O sincron, care este asincron intern, este acceptat numai la nivel de aplicație Windows.

În Win32, se apelează I/O asincron I/O suprapuse, compararea modurilor I/O sincrone și suprapuse este dată în figura de mai jos:

Când o aplicație face o solicitare asincronă pentru a efectua o operație I/O, Windows fie efectuează operația imediat, fie returnează un cod de stare care indică faptul că operația este în așteptare. Firul de execuție poate rula apoi alte operațiuni I/O sau poate efectua unele calcule. Programatorul are mai multe moduri de a organiza primirea notificărilor despre finalizarea operațiunilor I/O:

    Eveniment Win32: Operația care așteaptă acest eveniment va fi executată când I/O se finalizează.

    Apelarea unei funcții personalizate folosind un mecanism apel de procedură asincronă (APC): Firul de execuție trebuie să fie într-o stare de așteptare alertabilă.

    Primirea notificărilor prin Porturi de completare I/O (IOCP): Acesta este de obicei cel mai eficient mecanism. O vom explora în detaliu în continuare.

Unele dispozitive I/O (de exemplu, un fișier deschis în modul fără tampon) pot oferi beneficii suplimentare dacă aplicația se poate asigura că există întotdeauna un număr mic de solicitări I/O în așteptare. Pentru a face acest lucru, este recomandat să faceți mai întâi mai multe solicitări pentru a efectua operațiuni I/O și pentru fiecare cerere finalizată să emitați o nouă cerere. Acest lucru va asigura că driverul dispozitivului inițiază următoarea operațiune cât mai repede posibil, fără a aștepta ca aplicația să finalizeze următoarea solicitare. Dar nu exagerați cu cantitatea de date transferate, deoarece acest lucru va consuma resurse limitate de memorie kernel.

Porturi de completare I/O

Windows acceptă un mecanism eficient de notificare a finalizării operațiunilor I/O asincrone numite I/O Completion Ports (IOCP). În aplicațiile .NET este disponibil prin metoda ThreadPool.BindHandle(). Acest mecanism este utilizat intern de unele tipuri în .NET care efectuează operațiuni I/O: FileStream, Socket, SerialPort, HttpListener, PipeStream și unele conducte .NET Remoting.

Mecanismul IOCP, prezentat în figura de mai sus, se leagă de mai multe mânere I/O (socket-uri, fișiere și obiecte specializate ale driverului de dispozitiv) deschise asincron și la un anumit fir de execuție. Odată ce operațiunea I/O asociată cu un astfel de mâner se finalizează, Windows va adăuga o notificare la portul IOCP corespunzător și o va transmite firului de execuție asociat pentru procesare.

Utilizarea unui grup de fire de execuție care deservesc notificările și reia execuția firelor de execuție care au inițiat operațiuni I/O asincrone reduce numărul de comutări de context pe unitatea de timp și crește utilizarea CPU. Nu este surprinzător faptul că serverele de înaltă performanță, cum ar fi Microsoft SQL Server, folosesc porturi de completare I/O.

Portul de completare este creat prin apelarea funcției Win32 API CreateIoCompletionPort, căruia i se transmite valoarea maximă de concurență (numărul de fire), o cheie de terminare și un mâner de obiect I/O opțional. O cheie de terminare este o valoare definită de utilizator care servește la identificarea diferiților descriptori I/O. Puteți lega mai multe mânere la același port IOCP apelând în mod repetat funcția CreateIoCompletionPort și transmițându-i un mâner la portul de completare existent.

Pentru a stabili comunicarea cu portul IOCP specificat, firele de execuție apelează funcția GetCompletionStatusși așteptați finalizarea lui. În orice moment, un fir de execuție poate fi asociat doar cu un port IOCP.

Apelarea unei funcții GetQueuedCompletionStatus blochează execuția firului de execuție până la notificare (sau o limită de expirare a expirat), apoi returnează informații despre operațiunea I/O finalizată, cum ar fi numărul de octeți transferați, cheia de finalizare și structura operațiunii I/O asincrone. Dacă toate firele asociate cu portul I/O sunt ocupate când apare notificarea (adică nu există fire de execuție în așteptare la apelul GetQueuedCompletionStatus), mecanismul IOCP va crea un nou fir de execuție, până la valoarea maximă de concurență. Dacă un fir de execuție apelează GetQueuedCompletionStatus și coada de notificare nu este goală, funcția va reveni imediat fără a bloca firul de execuție în nucleul sistemului de operare.

Mecanismul IOCP este capabil să detecteze că unul dintre firele de execuție „ocupate” efectuează de fapt I/O sincron și să înceapă un fir suplimentar, eventual depășind valoarea maximă de concurență. Notificările pot fi trimise și manual, fără a efectua I/O, prin apelarea funcției PostQueuedCompletionStatus.

Următorul cod demonstrează un exemplu de utilizare a ThreadPool.BindHandle() cu un handle de fișier Win32:

Utilizarea sistemului; folosind System.Threading; folosind Microsoft.Win32.SafeHandles; folosind System.Runtime.InteropServices; Extensii de clasă publică ( intern static extern SafeFileHandle CreateFile(string lpFileName, EFileAccess dwDesiredAccess, EFileShare dwShareMode, IntPtr lpSecurityAttributes, ECreationDisposition dwCreationDisposition, EFileAttributes, EFileAttributes dwShareMode); extern bool WriteF ile(SafeFileHandle hFile, byte lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten , System.Threading.NativeOverlapped* lpOverlapped); enumerare EFileShare: uint ( None = 0x00000000, Read = 0x00000001, Write = 0x00000002, Delete = 0x00000002, Delete = 0x0000000000int) , CreateAlways = 2, OpenExisting = 3 , OpenAlways = 4, TruncateExisting = 5 ) enum EFileAttributes: uint ( // ... Unele steaguri nu sunt afișate Normal = 0x00000080, Overlapped = 0x40000000, NoBuffering = 0x20000000, ) enum EFileAccess (nu sunt afișate / uint/: ... GenericRead = 0x80000000 , GenericWrite = 0x40000000, ) static long _numBytesWritten; // Frână pentru fluxul de înregistrare static AutoResetEvent _waterMarkFullEvent; static int _pendingIosCount; const int MaxPendingIos = 10; // Procedura de finalizare, apelată de firele I/O static unsafe void WriteComplete(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) ( _numBytesWritten += numBytes; Overlapped ovl = Overlapped.Unpack(pOVERLAP); Overlapped.Free); / Notificați firul de scriere că numărul de operațiuni I/O în așteptare // a scăzut la limita permisă dacă (Interlocked.Decrement(ref _pendingIosCount) = MaxPendingIos) ( _waterMarkFullEvent.WaitOne(); ) ) ) )

Să ne uităm mai întâi la metoda TestIOCP. Aceasta apelează funcția CreateFile(), care este o funcție de motor P/Invoke folosită pentru a deschide sau a crea un fișier sau un dispozitiv. Pentru a efectua operațiuni I/O în mod asincron, trebuie să transmiteți indicatorul EFileAttributes.Overlapped funcției. Dacă are succes, funcția CreateFile() returnează un handle de fișier Win32, pe care îl legăm la portul de completare I/O apelând ThreadPool.BindHandle(). În continuare, este creat un obiect eveniment care este utilizat pentru a bloca temporar firul de execuție care a inițiat operația I/O dacă sunt prea multe (limita este stabilită de constanta MaxPendingIos).

Apoi începe ciclul de scrieri asincrone. La fiecare iterație se creează un buffer cu date de scris și Structură suprapusă, care conține offset-ul în fișier (în acest exemplu, scrierea se face întotdeauna la offset 0), un handle de eveniment transmis la finalizarea operației (neutilizat de mecanismul IOCP) și un obiect utilizator opțional IAsyncResult, care poate fi folosit pentru a trece starea funcției de finalizare.

Apoi, este apelată metoda Overlapped.Pack(), care necesită o funcție de completare și un buffer cu date. Acesta creează o structură echivalentă de nivel scăzut pentru operațiunea I/O, plasându-l în memorie neadministrată și fixând un buffer cu datele. Eliberarea memoriei neadministrate ocupată de structura de nivel scăzut și detașarea buffer-ului trebuie făcute manual.

Dacă nu vor avea loc prea multe operațiuni I/O în același timp, apelăm WriteFile(), trecându-i structura de nivel scăzut specificată. În caz contrar, așteptăm până când apare un eveniment care indică faptul că numărul de operațiuni în așteptare a scăzut sub limita superioară.

Funcția de completare WriteComplete este apelată de un fir de execuție din pool-ul de fire de completare I/O imediat ce operațiunea este finalizată. I se transmite un pointer către o structură I/O asincronă de nivel scăzut, care poate fi dezambalată și convertită într-o structură suprapusă gestionată.

Pentru a rezuma, atunci când lucrați cu dispozitive I/O de înaltă performanță, utilizați I/O asincron cu porturi de completare, fie direct prin crearea și utilizarea unui port de completare personalizat într-o bibliotecă negestionată, fie legând mânerele Win32 la un port de completare în .NET folosind metoda ThreadPool.BindHandle().

Pool de fire în .NET

Pool-ul de fire din .NET poate fi utilizat în mod util pentru o varietate de scopuri, fiecare dintre acestea creând diferite tipuri de fire. Când am discutat mai devreme despre calculul paralel, am fost introduși în API-ul pool-ului de fire, unde l-am folosit pentru a paraleliza sarcinile de calcul. Cu toate acestea, grupurile de fire pot fi folosite pentru a rezolva alte tipuri de probleme:

    Firele de lucru pot gestiona apeluri asincrone către delegații utilizatori (cum ar fi BeginInvoke sau ThreadPool.QueueUserWorkItem).

    Firele de completare I/O pot deservi notificările care provin de la portul global IOCP.

    Firele Wait pot aștepta evenimentele înregistrate, permițându-vă să așteptați mai multe evenimente pe un singur fir (folosind WaitForMultipleObjects), până la limita superioară Windows (maximum de obiecte de așteptare = 64). Tehnica de așteptare a evenimentelor este utilizată pentru a organiza I/O asincron fără a utiliza porturile de finalizare.

    Fire timer care așteaptă ca mai multe temporizatoare să expire simultan.

    Fire de poartă monitorizați utilizarea CPU a firelor din pool și, de asemenea, modificați numărul de fire (în limitele specificate) pentru a obține cea mai bună performanță.

Este posibil să inițiezi operațiuni I/O care par a fi asincrone, dar nu sunt. De exemplu, apelarea delegatului ThreadPool.QueueUserWorkItem și apoi efectuarea unei operații I/O sincrone nu este o operație cu adevărat asincronă și nu este mai bună decât efectuarea aceleiași operații pe un fir normal de execuție.

Copierea memoriei

Nu este neobișnuit ca un dispozitiv I/O fizic să returneze un buffer de date care este copiat din nou și din nou până când aplicația termină procesarea acestuia. Acest tip de copiere poate consuma o parte semnificativă din puterea de procesare a procesorului și ar trebui evitat pentru a asigura un randament maxim. În continuare, vom analiza mai multe situații în care este obișnuit să copiați date și ne vom familiariza cu tehnici pentru a evita acest lucru.

Memorie negestionată

Lucrul cu un buffer în memoria neadministrată în .NET este mult mai dificil decât lucrul cu o matrice de octeți gestionați, astfel încât programatorii copiază adesea tamponul în memoria gestionată în căutarea modului cel mai simplu.

Dacă funcțiile sau bibliotecile pe care le utilizați vă permit să specificați în mod explicit un buffer în memorie sau să transmiteți propria funcție de apel invers pentru a aloca un buffer, alocați un buffer gestionat și fixați-l în memorie, astfel încât să poată fi accesat atât de un pointer, cât și de o referință gestionată . Dacă tamponul este suficient de mare (>85.000 de octeți), acesta va fi creat în grămada de obiecte mari (Large Object Heap), deci încercați să reutilizați tamponurile existente. Dacă reutilizarea tamponului este complicată de incertitudinea duratei de viață a obiectului, utilizați pool-uri de memorie.

În alte cazuri, în care funcțiile sau bibliotecile însele alocă memorie (negestionată) pentru buffere, puteți accesa acea memorie direct prin pointer (din cod nesigur) sau utilizând clase de wrapper, cum ar fi UnmanagedMemoryStreamȘi UnmanagedMemoryAccessor. Cu toate acestea, dacă trebuie să transmiteți tamponul unui cod care funcționează numai pe matrice de octeți sau obiecte șir, copierea poate fi inevitabil.

Chiar dacă nu puteți evita copierea din memorie și unele sau majoritatea datelor dvs. sunt filtrate devreme, puteți evita copierea inutilă verificând dacă datele sunt necesare înainte de a le copia.

Exportarea unei părți a unui buffer

Programatorii presupun uneori că matricele de octeți conțin doar datele necesare, de la capăt la capăt, forțând codul de apelare să împartă memoria tampon (alocați memorie pentru o nouă matrice de octeți și copiați doar datele necesare). Această situație poate fi adesea observată în implementările stivei de protocol. Codul nativ echivalent, dimpotrivă, ar putea lua un indicator simplu, fără să știe măcar dacă indică la începutul bufferului real sau la mijlocul acestuia și un parametru de lungime a tamponului pentru a determina unde este sfârșitul datelor care sunt procesate.

Pentru a evita copierea inutilă a memoriei, asigurați-vă că decalajul și lungimea sunt acceptate oriunde acceptați un parametru de octet. Utilizați parametrul lungime în loc de proprietatea Length a matricei și adăugați valoarea offset-ului la indicii curenti.

Scatter citire și îmbinare scriere

Citirea dispersată și scrierea de îmbinare este capacitatea suportată de sistemul de operare Windows de a citi sau scrie date din zone necontigue ca și cum ar ocupa o zonă adiacentă de memorie. Această funcționalitate este furnizată în API-ul Win32 ca funcții CitițiFileScatterȘi WriteFileGather. Biblioteca socket Windows acceptă, de asemenea, capacități de citire dispersată și scriere de îmbinare, oferind propriile funcții: WSASend, WSARecv și altele.

Citirea dispersată și scrierea de îmbinare pot fi utile în următoarele situații:

    Când fiecare pachet are un antet de dimensiune fixă ​​care precede datele reale. Citirea dispersată și scrierea de îmbinare vă vor permite să evitați să copiați anteturile de fiecare dată când trebuie să obțineți un buffer contigu.

    Când este de dorit să se elimine supraîncărcarea inutilă a apelurilor de sistem atunci când se efectuează I/O cu mai multe buffere.

În comparație cu funcțiile ReadFileScatter și WriteFileGather, care necesită ca fiecare buffer să aibă exact dimensiunea unei pagini și ca mânerul să fie deschis în modul asincron și fără tampon (care este o limitare și mai mare), funcțiile de citire și scriere de tip scatter bazate pe socket. par mai practice pentru că nu au aceste restricții. .NET Framework acceptă citirea dispersată și scrierea combinată pentru socketuri prin metode supraîncărcate Socket.Send()Și Socket.Receive() fără a exporta funcții generice de citire/scriere.

Un exemplu de utilizare a funcțiilor de citire scatter și de scriere de îmbinare poate fi găsit în clasa HttpWebRequest. Combină anteturile HTTP cu datele reale, fără a fi nevoie să creeze un buffer contigu pentru a le stoca.

I/O fișier

De obicei, operațiunile de I/O de fișiere sunt efectuate prin intermediul cache-ului sistemului de fișiere, care oferă mai multe beneficii de performanță: stocarea în cache a datelor utilizate recent, citirea anticipată (citirea datelor de pe disc în avans), scriere lenenă (scrieri asincrone pe disc) și îmbinare scrie bucăți mici de date. A cere Windows să se aștepte la modele de acces la fișiere poate oferi câștiguri suplimentare de performanță. Dacă aplicația dvs. face I/O asincron și poate gestiona unele probleme de buffering, atunci evitarea completă a mecanismului de cache poate fi o soluție mai eficientă.

Gestionarea stocării în cache

Când creează sau deschid fișiere, programatorii trec steaguri și atribute funcției CreateFile, dintre care unele afectează comportamentul mecanismului de stocare în cache:

    Steag FILE_FLAG_SEQUENTIAL_SCAN indică faptul că fișierul va fi accesat secvențial, posibil sărind unele părți, iar accesul aleatoriu este puțin probabil. Ca rezultat, managerul de cache va efectua o citire înainte, uitând mai departe decât de obicei.

    Steag FILE_FLAG_RANDOM_ACCESS indică faptul că fișierul va fi accesat în ordine aleatorie. În acest caz, managerul de cache va citi ușor înainte, din cauza probabilității reduse ca datele citite înainte să fie de fapt necesare aplicației.

    Steag FILE_ATTRIBUTE_TEMPORARY indică faptul că fișierul este temporar, astfel încât scrierile reale pe suportul fizic (pentru a preveni pierderea datelor) pot fi amânate.

În .NET, aceste opțiuni sunt acceptate (cu excepția ultimei) folosind supraîncărcarea constructorului FileStream, care ia un parametru de tipul de enumerare FileOptions.

Accesul aleatoriu are un impact negativ asupra performanței, mai ales atunci când lucrați cu dispozitive de disc, deoarece necesită capete în mișcare. Pe măsură ce tehnologia s-a dezvoltat, debitul discului a crescut doar prin creșterea densității de stocare a datelor, dar nu prin reducerea latenței. Unitățile moderne sunt capabile să reordoneze interogări de acces aleatoriu pentru a reduce timpul total petrecut în mișcarea capetelor. Această tehnică se numește instalarea hardware a cozii de comandă (Native Command Queuing, NCO). Pentru ca această tehnică să fie mai eficientă, este necesar să trimiteți simultan mai multe solicitări I/O către controlerul de disc. Cu alte cuvinte, dacă este posibil, încercați să aveți mai multe solicitări I/O asincrone în așteptare simultan.

I/O fără tampon

Operațiunile I/O fără tampon sunt întotdeauna efectuate fără a implica memoria cache. Această abordare are avantajele și dezavantajele sale. Ca și în cazul tehnicii de gestionare a memoriei cache, I/O fără buffer este activat utilizând opțiunea de steaguri și atribute în timpul creării fișierului, dar .NET nu oferă acces la această caracteristică.

    Steag FILE_FLAG_NO_BUFFERING dezactivează stocarea în cache de citire și scriere, dar nu are niciun efect asupra stocării în cache efectuate de controlerul de disc. Acest lucru vă permite să evitați copierea (din buffer-ul utilizatorului în cache) și poluarea cache-ului (umplerea cache-ului cu date inutile și înlocuirea celor necesare). Cu toate acestea, citirile și scrierile fără tampon trebuie să respecte cerințele de aliniere.

    Următorii parametri trebuie să fie egali cu sau un multiplu al dimensiunii sectorului de disc: dimensiunea unică de transfer, decalajul fișierului și adresa tamponului de memorie. De obicei, un sector de disc are o dimensiune de 512 octeți. Cele mai recente unități de disc de mare capacitate au o dimensiune a sectorului de 4096 de octeți, dar pot funcționa în modul de compatibilitate prin emularea sectoarelor de 512 de octeți (cu prețul performanței).

    Steag FILE_FLAG_WRITE_THROUGHîi spune managerului de cache că ar trebui să scoată imediat datele scrise din cache (cu excepția cazului în care este setat indicatorul FILE_FLAG_NO_BUFFERING) și îi spune controlerului de disc că ar trebui să scrie imediat pe suportul fizic fără a stoca datele într-un cache hardware intermediar.

Citirea anticipată îmbunătățește performanța făcând utilizarea discului mai eficientă, chiar și atunci când aplicația efectuează citiri sincrone cu întârzieri între operații. Depinde de Windows să determine corect ce parte a fișierului va solicita în continuare aplicația. Prin dezactivarea tamponării, dezactivați, de asemenea, citirea anticipată și trebuie să mențineți dispozitivul de disc ocupat, efectuând mai multe operațiuni I/O suprapuse.

Scrierile cu latență îmbunătățesc, de asemenea, performanța aplicațiilor care efectuează scrieri sincrone, creând iluzia că scrierile pe disc au loc foarte rapid. Aplicația va putea îmbunătăți utilizarea procesorului prin blocarea pentru perioade mai scurte de timp. Cu tamponarea dezactivată, durata operațiunilor de scriere va fi întreaga cantitate de timp necesară pentru a finaliza scrierea datelor pe disc. Prin urmare, utilizarea modului I/O asincron atunci când tamponarea este dezactivată devine și mai importantă.

Operațiile de intrare și de ieșire au o viteză de execuție mai lentă decât alte tipuri de procesare. Motivele acestei încetiniri sunt următorii factori:

Întârzieri cauzate de timpul petrecut căutând pistele și sectoarele necesare pe dispozitive cu acces aleatoriu (discuri, CD-uri).

Latențe cauzate de viteza relativ scăzută a schimbului de date între dispozitivele fizice și memoria sistemului.

Întârzieri în transferul de date prin rețea folosind servere de fișiere, stocări de date și așa mai departe.

În toate exemplele anterioare, sunt efectuate operațiuni I/O sincronizat cu fluxul, astfel încât întregul fir este forțat să funcționeze în gol până când se completează.

Acest capitol arată cum puteți aranja ca un fir să continue execuția fără a aștepta finalizarea I/O, ceea ce este în concordanță cu execuția firului. asincron intrare ieșire. Diferitele tehnici disponibile în Windows sunt ilustrate cu exemple.

Unele dintre aceste tehnici sunt folosite în temporizatoarele de așteptare, care sunt, de asemenea, descrise în acest capitol.

În cele din urmă, și cel mai important, după ce am învățat operațiunile I/O asincrone standard, putem folosi Porturi de completare I/O, care sunt extrem de utile în construirea de servere scalabile care pot suporta un număr mare de clienți fără a crea un fir separat pentru fiecare dintre ei. Programul 14.4 este o versiune modificată a unui server dezvoltat anterior, care permite utilizarea porturilor de completare I/O.

Prezentare generală a metodelor I/O asincrone Windows

Windows gestionează I/O asincron folosind trei tehnici.

I/O cu mai multe fire. Fiecare fir dintr-un proces sau set de procese efectuează I/O sincron normal, dar alte fire pot continua să se execute.

I/O suprapuse. După ce a început o operație de citire, scriere sau altă operație I/O, firul de execuție își continuă execuția. Dacă un fir de execuție necesită rezultate I/O pentru a continua execuția, acesta așteaptă până când un handle adecvat devine disponibil sau apare un eveniment specificat. În Windows 9x, suprapunerea I/O este acceptată numai pentru dispozitivele seriale, cum ar fi conductele numite.

Rutine de finalizare (I/O extins) Când operațiunile de I/O sunt finalizate, sistemul apelează un special procedura de finalizare curgând în interiorul unui fir. I/O extins pentru fișierele disc nu este acceptat în Windows 9x.

I/O multithreaded folosind conducte numite este folosit în serverul multithreaded discutat în Capitolul 11. Programul grepMT (Programul 7.1) gestionează operațiuni I/O concurente care implică mai multe fișiere. Astfel, avem deja o serie de programe care efectuează I/O multi-threaded și, prin urmare, oferă o formă de I/O asincron.

Suprapunerea I/O este subiectul secțiunii următoare, iar exemplele de acolo, care implementează conversia fișierelor (ASCII în UNICODE), folosesc această tehnică pentru a ilustra capabilitățile procesării secvențiale ale fișierelor. În acest scop, se utilizează o versiune modificată a programului 2.4. În urma I/O suprapuse, este luată în considerare I/E extinsă folosind rutine de finalizare.

Notă

Metodele I/O suprapuse și extinse sunt adesea dificil de implementat, rareori oferă beneficii de performanță, uneori chiar provoacă degradarea performanței, iar în cazul I/O fișierelor pot funcționa numai sub Windows NT. Aceste probleme sunt depășite cu ajutorul firelor, Prin urmare, mulți cititori vor dori probabil să treacă direct la secțiunile despre temporizatoarele de așteptare și porturile de finalizare a I/O. revenind la această secțiune după cum este necesar. Pe de altă parte, elementele I/O asincrone sunt prezente atât în ​​tehnologiile moștenite, cât și în noile tehnologii, așa că aceste metode merită încă explorate.

Astfel, tehnologia COM de pe platforma NT5 acceptă apeluri de metodă asincrone, așa că această tehnică poate fi utilă multor cititori care folosesc sau intenționează să folosească tehnologia COM. În plus, apelurile de procedură asincronă (Capitolul 10) au multe în comun cu I/O extins și, deși eu personal prefer să folosesc fire de execuție, alții pot prefera acest mecanism.

I/O suprapuse

Primul lucru pe care trebuie să-l faceți pentru a implementa I/O asincron, indiferent dacă sunt suprapuse sau extinse, este să setați atributul suprapus pe fișier sau alt descriptor. Pentru a face acest lucru, atunci când apelați CreateFile sau altă funcție care creează un fișier, conductă numită sau alt descriptor, trebuie să specificați steag-ul FILE_FLAG_OVERLAPPED.

În cazul socket-urilor (Capitolul 12), indiferent dacă au fost create folosind funcția socket sau accept, atributul de suprapunere este setat implicit în Winsock 1.1, dar trebuie setat explicit în Winsock 2.0. Prizele suprapuse pot fi utilizate asincron pe toate versiunile de Windows.

Până în acest moment, am folosit structuri OVERLAPPED împreună cu funcția LockFileEx și ca alternativă la utilizarea funcției SetFilePointer (Capitolul 3), dar ele sunt, de asemenea, un element esențial al I/O suprapuse. Aceste structuri acționează ca parametri opționali la apelarea celor patru funcții de mai jos, care se pot bloca la finalizarea operațiunilor.

Amintiți-vă că atunci când specificați indicatorul FILE_FLAG_OVERLAPPED ca parte a parametrului dwAttrsAndFlags (în cazul funcției CreateFile) sau a parametrului dwOpen-Mode (în cazul funcției CreateNamedPipe), fișierul sau conducta corespunzătoare poate fi utilizată numai în suprapunere. modul. I/O suprapuse nu funcționează cu canale anonime.

Notă

Documentația pentru funcția CreateFile menționează că utilizarea flag-ului FILE_FLAG_NO_BUFFERING îmbunătățește performanța I/O suprapuse. Experimentele arată doar o ușoară îmbunătățire a performanței (aproximativ 15%, care poate fi verificată prin experimentarea cu Programul 14.1), dar ar trebui să vă asigurați că dimensiunea totală a datelor citite atunci când efectuați o operație ReadFile sau WriteFile este un multiplu al mărimii sectorului de disc. .

Prize suprapuse

Una dintre cele mai importante inovații din Windows Sockets 2.0 (Capitolul 12) este standardizarea I/O suprapuse. În special, socket-urile nu mai sunt create automat ca descriptori de fișier suprapusi. Funcția priză creează un mâner care nu se suprapune. Pentru a crea un socket suprapus, trebuie să apelați funcția WSASocket, solicitând în mod explicit crearea unui sfat de suprapunere prin specificarea valorii WSA_FLAG_OVERLAPPED pentru parametrul dwFlags al funcției WSASocket.

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

Pentru a crea un socket, utilizați funcția WSASocket în loc de funcția socket. Orice socket returnat de accept va avea aceleași proprietăți ca și argumentul.

Consecințele utilizării I/O suprapuse

Suprapunerea I/O este realizată asincron. Acest lucru are mai multe implicații.

Operațiile I/O suprapuse nu sunt blocate. Funcțiile ReadFile, WriteFile, TransactNamedPipe și ConnectNamedPipe revin fără a aștepta finalizarea operațiunii I/O.

Valoarea returnată de o funcție nu poate fi folosită ca criteriu pentru succesul sau eșecul execuției acesteia, deoarece operația I/O nu s-a finalizat încă în acest moment. Indicarea stării de progres I/O necesită un alt mecanism.

Returnarea numărului de octeți transferați este, de asemenea, de puțin folos, deoarece este posibil ca transferul de date să nu se fi finalizat complet. Pentru a obține acest tip de informații, Windows trebuie să ofere un alt mecanism.

Un program poate încerca să citească sau să scrie de mai multe ori folosind același handle de fișier suprapus. Prin urmare, indicatorul de fișier corespunzător unui astfel de descriptor este, de asemenea, nesemnificativ. Prin urmare, trebuie furnizată o metodă suplimentară pentru a oferi o indicație a poziției în fișier pentru fiecare operație de citire sau scriere. Cu conductele numite, datorită naturii lor secvenţiale inerente a procesării datelor, aceasta nu este o problemă.

Programul trebuie să poată aștepta (sincronizează) până la finalizarea I/O. Dacă există mai multe operațiuni I/O în așteptare asociate cu același mâner, programul trebuie să poată determina care operațiuni au fost deja finalizate. Operațiunile I/O nu se finalizează neapărat în aceeași ordine în care au început.

Pentru a depăși ultimele două dificultăți enumerate mai sus, se folosesc structuri SUPRAPUSE.

Structuri suprapuse

Folosind structura OVERLAPPED (specificată, de exemplu, de parametrul lpOverlapped al funcției ReadFile), puteți specifica următoarele informații:

Poziția fișierului (64 de biți) la care ar trebui să înceapă o operație de citire sau scriere, așa cum este discutat în Capitolul 3.

Un eveniment (resetat manual) care va fi semnalat la finalizarea operațiunii asociate.

Mai jos este definiția structurii OVERLAPPED.

Pentru a specifica o poziție de fișier (pointer), trebuie utilizate atât câmpurile Offset, cât și OffsetHigh, deși partea superioară a indicatorului (OffsetHigh) este 0 în multe cazuri Câmpurile Internal și InternalHigh, care sunt rezervate nevoilor sistemului, nu ar trebui fi folosit.

Parametrul hEvent este un descriptor pentru eveniment (creat folosind funcția CreateEvent). Acest eveniment poate fi fie numit, fie nenumit, dar este trebuie sa trebuie resetat manual (vezi capitolul 8) dacă este utilizat pentru I/O suprapuse; motivele pentru aceasta vor fi în curând explicate. Când operațiunea I/O se încheie, evenimentul intră în starea semnal.

Într-un alt caz posibil de utilizare, descriptorul hEvent este NULL; în acest caz, programul poate aștepta ca descriptorul de fișier să fie semnalizat, care poate acționa și ca un obiect de sincronizare (vezi următoarele avertismente). Sistemul folosește stările de semnalizare a descriptorului de fișier pentru a urmări finalizarea operațiunilor dacă descriptorul hEvent este NULL, adică obiectul de sincronizare în acest caz este un descriptor de fișier.

Notă

Pentru comoditate, vom folosi termenul „mâner de fișier” pentru a face referire la mânerele specificate în apelurile la ReadFile, WriteFile și așa mai departe, chiar și atunci când ne referim la mânere la o conductă sau un dispozitiv numit, nu la un fișier.

Când este efectuat un apel de funcție I/O, acest eveniment este imediat șters de către sistem (setat la o stare nesemnalizată). Când se termină o operațiune I/O, evenimentul este setat într-o stare de alarmă și rămâne acolo până când este utilizat de o altă operațiune I/O. Un eveniment trebuie resetat manual dacă mai multe fire de execuție pot aștepta ca acesta să fie semnalat (deși exemplele noastre folosesc doar un fir de execuție) și este posibil să nu fie în starea de așteptare când operațiunea se încheie.

Chiar dacă mânerul fișierului este sincron (adică, creat fără indicatorul FILE_FLAG_OVERLAPPED), structura OVERLAPPED poate servi ca alternativă la funcția SetFilePointer pentru specificarea unei poziții în fișier. În acest caz, apelul funcției ReadFile sau alt apel nu revine până la finalizarea operațiunii I/O. Am profitat deja de această caracteristică în Capitolul 3. De asemenea, rețineți că operațiunile I/O în așteptare sunt identificate în mod unic prin combinația dintre un descriptor de fișier și structura OVERLAPPED corespunzătoare.

Mai jos sunt enumerate câteva precauții de luat în considerare.

Evitați reutilizarea unei structuri SUPRAPUTE în timp ce operațiunea de I/O asociată, dacă există, nu s-a finalizat încă.

De asemenea, evitați reutilizarea unui eveniment specificat în structura OVERLAPPED.

Dacă există mai multe solicitări în așteptare pentru același mâner suprapus, utilizați mânerele de eveniment mai degrabă decât mânerele de fișier pentru sincronizare.

Dacă structura sau evenimentul OVERLAPPED acţionează ca variabile automate în cadrul unui bloc, asiguraţi-vă că blocul nu poate fi ieşit înainte de sincronizarea cu operaţia I/O. În plus, pentru a evita scurgerea de resurse, trebuie avut grijă să închideți mânerul înainte de a ieși din bloc.

State I/O suprapuse

Funcțiile ReadFile și WriteFile, precum și cele două funcții pipe denumite de mai sus, revin imediat când sunt utilizate pentru a efectua operațiuni I/O suprapuse. În cele mai multe cazuri, operația I/O nu se va fi finalizată în acest moment, iar valoarea returnată a citirii și scrierii va fi FALS. Funcția GetLastError va returna ERROR_IO_PENDING în această situație.

Odată ce ați terminat de așteptat ca obiectul de sincronizare (un eveniment sau poate un descriptor de fișier) să intre într-o stare de semnalizare care indică finalizarea operației, trebuie să aflați câți octeți au fost transferați. Acesta este scopul principal al funcției GetOverlappedResult.

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

Specificația unei anumite operațiuni I/O este furnizată de combinația dintre un mâner și o structură SUPRAPUȚĂ. Valoarea TRUE a lui bWait specifică faptul că funcția GetOverlappedResult ar trebui să aștepte până la finalizarea operației; in caz contrar, revenirea din functie trebuie sa fie imediata. În orice caz, această funcție va returna TRUE numai după ce operația s-a încheiat cu succes. Dacă valoarea returnată a funcției GetOverlappedResult este FALSE, atunci funcția GetLastError va returna valoarea ERROR_IO_INCOMPLETE, permițând apelării acestei funcție pentru a interoga finalizarea I/O.

Numărul de octeți transferați este stocat în variabila *lpcbTransfer. Asigurați-vă întotdeauna că structura OVERLAPPED rămâne neschimbată din momentul în care este utilizată într-o operație I/O suprapusă.

Anularea operațiunilor I/O suprapuse

Funcția booleană CancelIO vă permite să anulați operațiunile I/O suprapuse în așteptare asociate cu mânerul specificat (această funcție are un singur parametru). Toate operațiunile inițiate de firul apelant care utilizează mânerul sunt anulate. Operațiile inițiate de alte fire de execuție nu sunt afectate de apelarea acestei funcții. Operațiunile anulate se termină cu eroarea EROARE OPERATION ABORTED.

Exemplu: Utilizarea unui mâner de fișier ca obiect de sincronizare

Suprapunerea I/O este foarte convenabilă și ușor de implementat în cazurile în care poate fi o singură operație în așteptare. Apoi, în scopuri de sincronizare, programul poate folosi nu evenimentul, ci descriptorul de fișier.

Următorul fragment de cod arată cum un program poate iniția o operațiune de citire pentru a citi o parte a unui fișier, poate continua executarea pentru a efectua alte procesări și apoi poate intra într-o stare de așteptare ca mânerul fișierului să intre în starea semnal.

OVERLAPPED ov = ( 0, 0, 0, 0, NULL /* Evenimentele nu sunt folosite. */ );
hF = CreateFile(…, FILE_FLAG_OVERLAPPED, …);
ReadFile(hF, Buffer, sizeof(Buffer), &nRead, &ov);
/* Efectuați alte tipuri de procesare. nRead nu este neapărat de încredere.*/
/* Așteptați finalizarea operației de citire. */
WaitForSingleObject(hF, INFINIT);
GetOverlappedResult(hF, &ov, &nRead, FALSE);

Exemplu: conversia fișierelor utilizând I/O suprapus și tamponarea multiplă

Programul 2.4 (atou) a făcut conversia unui fișier ASCII în UNICODE prin procesare secvențială a fișierelor, iar Capitolul 5 a arătat cum să se facă aceeași procesare secvențială prin maparea fișierelor. Programul 14.1 (atouOV) rezolvă aceeași problemă folosind I/O suprapuse și mai multe buffer-uri care stochează înregistrări de dimensiune fixă.

Figura 14.1 ilustrează organizarea unui program cu patru buffer-uri de dimensiune fixă. Programul este implementat astfel încât numărul de buffere să poată fi determinat folosind o constantă simbolică de preprocesor, dar în discuția următoare vom presupune că există patru buffere.

În primul rând, programul inițializează toate elementele structurilor OVERLAPPED care definesc evenimente și poziții în fișiere. Fiecare buffer de intrare și de ieșire are o structură SUPRAPUTĂ separată. După aceasta, o operație de citire suprapusă este inițiată pentru fiecare dintre bufferele de intrare. Apoi, folosind funcția WaitForMultipleObjects, programul așteaptă un singur eveniment care indică finalizarea unei citiri sau scrieri. Când se finalizează o operație de citire, tamponul de intrare este copiat și convertit în tamponul de ieșire corespunzător și este inițiată o operație de scriere. Când scrierea se încheie, următoarea operație de citire este inițiată. Rețineți că evenimentele asociate cu buffer-urile de intrare și de ieșire sunt situate într-o singură matrice, care este folosită ca argument la apelarea funcției WaitForMultipleObjects.

Orez. 14.1. Model de actualizare a fișierelor asincrone


Programul 14.1. atouOV: Conversie de fișiere folosind I/O suprapuse
Convertiți un fișier din ASCII în Unicode utilizând I/O suprapuse. Programul funcționează doar pe Windows NT. */

#define MAX_OVRLP 4 /* Numărul de operațiuni I/O suprapuse.*/
#define REC_SIZE 0x8000 /* 32 KB: dimensiunea minimă a înregistrării pentru a oferi performanțe acceptabile. */

/* Fiecare dintre elementele tablourilor variabile definite mai jos */
/* și structurile corespund unei singure operații neterminate */
/* I/O suprapus. */
DWORD nin, nout, ic, i;
OVERLAPPED OverLapIn, OverLapOut;
/* Necesitatea de a folosi o matrice solidă, bidimensională */
/* dictat de Funcția WaitForMultipleObjects. */
/* Valoarea 0 a primului index corespunde citirii, valoarea 1 scrierii.*/
/* În fiecare dintre cele două matrice de buffer definite mai jos, primul index */
/* numere operațiuni I/O. */
LARGE_INTEGER CurPosIn, CurPosOut, FileSize;
/* Numărul total de înregistrări de procesat, calculat */
/* bazat pe dimensiunea fișierului de intrare. Intrarea de la sfarsit */
/* poate fi incomplet. */
pentru (ic = 0; ic< MAX_OVRLP; ic++) {
/* Creați evenimente de citire și scriere pentru fiecare structură OVERLAPPED.*/
hEvents = OverLapIn.hEvent /* Citiți eveniment.*/
hEvents = OverLapOut.hEvent /* Înregistrare eveniment. */
= CreateEvent(NULL, TRUE, FALSE, NULL);
/* Poziții de pornire în fișier pentru fiecare structură OVERPUSE. */
/* Inițiază o operație de citire suprapusă pentru această structură OVERLAPPED. */
dacă (CurPosIn.QuadPart< FileSize.QuadPart) ReadFile(hInputFile, AsRec, REC_SIZE, &nin, &OverLapIn);
/* Toate operațiunile de citire sunt efectuate. Așteptați finalizarea evenimentului și resetați-l imediat. Evenimentele de citire și scriere sunt stocate într-o matrice de evenimente unul lângă celălalt. */
iWaits =0; /* Numărul de operațiuni I/O finalizate până acum. */
în timp ce (așteaptă< 2 * nRecord) {
ic = WaitForMultipleObjects(2 * MAX_OVRLP, hEvents, FALSE, INFINITE) – WAIT_OBJECT_0;
iWaits++; /* Crește contorul de operațiuni I/O finalizate.*/
ResetEvent(evenimente);
/* Citirea finalizată. */
GetOverlappedResult(hInputFile, &OverLapIn, &nin, FALSE);
pentru (i =0; i< REC_SIZE; i++) UnRec[i] = AsRec[i];
WriteFile(hOutputFile, UnRec, nin * 2, &nout, &OverLapOut);
/* Pregătiți-vă pentru următoarea citire, care va fi inițiată după finalizarea operațiunii de scriere începută mai sus. */
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;
) altfel dacă (ic< 2 * MAX_OVRLP) { /* Операция записи завершилась. */
/* Începeți să citiți. */
ic –= MAX_OVRLP; /* Setați indexul bufferului de ieșire. */
if (!GetOverlappedResult (hOutputFile, &OverLapOut, &nout, FALSE)) ReportError(_T(„Eroare de citire.”), 0, TRUE);
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
dacă (CurPosIn.QuadPart< FileSize.QuadPart) {
/* Începe o nouă operație de citire. */
ReadFile(hInputFile, AsRec, REC_SIZE, &nin, &OverLapIn);
/* Închide toate evenimentele. */
pentru (ic = 0; ic< MAX_OVRLP; ic++) {

Programul 14.1 poate rula numai sub Windows NT. Facilități I/O asincrone Windows 9x nu permit utilizarea fișierelor de disc. Anexa B oferă rezultate și comentarii care indică performanța relativ slabă a programului atouOV. Experimentele au arătat că pentru a obține performanțe acceptabile, dimensiunea bufferului trebuie să fie de cel puțin 32 KB, dar chiar și în acest caz, I/O sincron convențional este mai rapid. În plus, performanța acestui program nu se îmbunătățește în condiții SMP, deoarece în acest exemplu, care procesează doar două fișiere, CPU nu este o resursă critică.

I/O extins folosind rutina de finalizare

Există, de asemenea, o altă abordare posibilă pentru utilizarea obiectelor de sincronizare. În loc să aibă un fir să aștepte un semnal de terminare de la un eveniment sau un handle, sistemul poate iniția un apel către o rutină de terminare definită de utilizator imediat după finalizarea operațiunii I/O. Procedura de finalizare poate începe apoi următoarea operație I/O și poate efectua orice acțiuni necesare pentru a ține cont de utilizarea resurselor sistemului. Această procedură indirectă de finalizare a apelului invers este similară cu apelul de procedură asincronă utilizat în Capitolul 10 și necesită utilizarea stărilor de așteptare alertabile.

Cum poate fi specificată o procedură de terminare într-un program? Printre parametrii sau structurile de date ale funcțiilor ReadFile și WriteFile, nu există parametri care ar putea fi folosiți pentru a stoca adresa procedurii de finalizare. Cu toate acestea, există o familie de funcții I/O extinse care sunt desemnate prin sufixul „Ex” și conțin un parametru suplimentar conceput pentru a transmite adresa rutinei de terminare. Funcțiile de citire și scriere sunt ReadFileEx și, respectiv, WriteFileEx. În plus, este necesară utilizarea uneia dintre următoarele caracteristici de așteptare.

I/O extins este uneori numit taxă de intrare/ieșire(I/O alertabilă). Consultați următoarele secțiuni pentru informații despre cum să utilizați funcțiile avansate.

Notă

În Windows 9x, I/O extins nu poate funcționa cu fișiere de disc și porturi de comunicație. În același timp, Windows 9x Extended I/O este capabil să lucreze cu conducte denumite, cutii poștale, prize și dispozitive seriale.

Funcții ReadFileEx, WriteFileEx și proceduri de finalizare

Funcțiile extinse de citire și scriere pot fi utilizate împreună cu fișierul deschis, canalul numit și mânerele de cutie poștală dacă obiectul corespunzător a fost deschis (creat) cu indicatorul FILE_FLAG_OVERLAPPED setat. Rețineți că acest flag setează atributul de mâner și, deși I/O suprapuse și extinse sunt diferite, același flag se aplică mânerelor ambelor tipuri de I/O asincrone.

Prizele suprapuse (Capitolul 12) pot fi utilizate împreună cu funcțiile ReadFileEx și WriteFileEx pe toate versiunile de 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)

Sunteți deja familiarizat cu ambele funcții, cu excepția faptului că fiecare are un parametru suplimentar care vă permite să specificați adresa rutinei de terminare.

Fiecare funcție trebuie să fie prevăzută cu o structură OVERLAPPED, dar nu este necesar să se furnizeze elementul hEvent al acestei structuri; sistemul îl ignoră. Cu toate acestea, acest element este foarte util pentru transmiterea de informații, cum ar fi numărul de secvență utilizat pentru a distinge operațiile individuale de intrare/ieșire, așa cum este demonstrat în Programul 14.2.

Comparând cu funcțiile ReadFile și WriteFile, veți observa că funcțiile extinse nu necesită parametri pentru a stoca numărul de octeți transferați. Aceste informații sunt transmise unei funcții de completare care trebuie să fie incluse în program.

Funcția de terminare oferă parametri pentru numărul de octeți, codul de eroare și adresa structurii OVERLAPPED. Ultimul dintre acești parametri este necesar pentru ca procedura de finalizare să poată determina care dintre operațiunile restante a fost finalizată. Rețineți că avertismentele anterioare cu privire la reutilizarea sau distrugerea structurilor OVERPUSE se aplică aici, precum și în cazul I/O suprapuse.

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

Ca și în cazul funcției CreateThread, la apelarea acesteia se specifică și numele unei funcții, numele FileIOCompletionRoutine este un substituent, nu numele real al procedurii de finalizare.

Valorile parametrului dwError sunt limitate la 0 (succes) și ERROR_HANDLE_EOF (încercarea de a citi în afara limitelor). Structura OVERLAPPED este structura care a fost utilizată de apelul finalizat ReadFileEx sau WriteFileEx.

Înainte ca rutina de oprire să fie apelată de sistem, trebuie să se întâmple două lucruri:

1. Operația I/O trebuie să se finalizeze.

2. Firul care apelează ar trebui să fie într-o stare de așteptare, notificând sistemul că trebuie să execute rutina de finalizare în coadă.

Cum intră un fir în starea de așteptare? Trebuie să efectueze un apel explicit către una dintre funcțiile de așteptare descrise în secțiunea următoare. Astfel, thread-ul creează condiții care fac imposibilă executarea prematură a procedurii de terminare. Un fir poate rămâne în starea de așteptare doar atât timp cât durează apelul la funcția de așteptare; După ce această funcție revine, firul de execuție iese din starea specificată.

Dacă ambele condiții sunt îndeplinite, procedurile de finalizare puse în coadă ca rezultat al finalizării operațiunilor I/O sunt executate. Rutinele de finalizare rulează pe același fir care a efectuat apelul inițial al funcției I/O și este într-o stare de așteptare. Prin urmare, un fir ar trebui să intre în starea de așteptare numai atunci când există condiții sigure pentru executarea rutinelor de finalizare.

Funcții de așteptare

Există cinci funcții de așteptare în total, dar mai jos sunt prototipuri a doar trei dintre ele care ne interesează imediat:

DWORD WaitForSingleObjectEx(HANDLE hObject, DWORD dwMilliseconds, BOOL balertable)
DWORD WaitForMultipleObjectsEx(DWORD cObjects, LPHANDLE lphObjects, BOOL fWaitAll, DWORD dwMilliseconds, BOOL balertable)
DWORD SleepEx(DWORD dwMilisecunde, BOOL balAlertable)

Fiecare dintre funcțiile de alertă are un flag bAlertable, care trebuie setat la TRUE în cazul I/O asincron. Funcțiile de mai sus sunt extensii ale funcțiilor Wait și Sleep cu care sunteți familiarizat.

Durata intervalelor de așteptare este indicată, ca de obicei, în milisecunde. Fiecare dintre aceste trei funcții revine de îndată ce orice din urmatoarele situatii:

Descriptorul(ii) tranziția(i) la starea de semnal, satisfăcând astfel cerințele standard pentru două dintre funcțiile de așteptare.

Perioada de expirare a expirat.

Toate rutinele de finalizare din coada firului de execuție se opresc din executare și bAlertable este setat la TRUE. Procedura de finalizare este pusă în coadă atunci când operația de I/O corespunzătoare se încheie (Figura 14.2).

Rețineți că nu există evenimente asociate cu structurile OVERLAPPED în funcțiile ReadFileEx și WriteFileEx, astfel încât niciunul dintre mânerele furnizate la apelarea funcției de așteptare nu este asociat direct cu vreo operațiune I/O specifică. În același timp, funcția SleepEx nu este asociată cu obiectele de sincronizare și, prin urmare, este cea mai ușor de utilizat. În cazul funcției SleepEx, intervalul de timeout este de obicei setat la INFINIT, astfel încât funcția va reveni numai după ce unul sau mai multe finalizatoare aflate în coadă au terminat de executat.

Executarea procedurii de oprire și revenirea din funcția de așteptare

Când o operație I/O extinsă se termină de execuție, rutina de completare asociată, cu argumentele sale care specifică structura OVERLAPPED, numărul de octeți și codul de eroare, este pusă în coadă pentru execuție.

Toate rutinele de finalizare din coada firului de execuție încep să se execute când firul de execuție intră în starea de așteptare. Ele sunt executate unul după altul, dar nu neapărat în aceeași ordine în care s-au finalizat operațiunile I/O. Revenirea din funcția de așteptare are loc numai după ce procedura de finalizare a revenit. Această caracteristică este importantă pentru a asigura funcționarea corectă a majorității programelor deoarece presupune că rutinele de terminare au șansa să se pregătească pentru următoarea utilizare a structurii OVERLAPPED și să efectueze alte acțiuni necesare pentru a pune programul într-o stare cunoscută înainte de a reveni din standby. stat.

Dacă revenirea de la funcția SleepEx se datorează execuției uneia sau mai multor rutine de finalizare în coadă, valoarea returnată a funcției va fi WAIT_TO_COMPLETION și aceeași valoare va fi returnată de funcția GetLastError apelată după ce una dintre funcțiile de așteptare a revenit. .

În concluzie, notăm două puncte:

1. Când apelați oricare dintre funcțiile de somn, utilizați INFINIT ca valoare a parametrului intervalului de somn. În absența unei opțiuni de timeout, funcțiile vor reveni numai după ce toate rutinele de terminare au încheiat execuția sau descriptorii au intrat într-o stare de semnal.

2. Pentru a transmite informații către procedura de completare, este obișnuit să se utilizeze elementul de date hEvent al structurii OVERLAPPED, deoarece acest câmp este ignorat de sistemul de operare.

Interacțiunea dintre firul principal, rutinele de finalizare și funcțiile de așteptare este ilustrată în Fig. 14.2. Acest exemplu începe trei citiri concurente, dintre care două sunt finalizate până când începe să se execute standby.

Orez. 14.2. I/O asincron folosind rutine de finalizare

Exemplu: conversia unui fișier utilizând I/O avansat

Programul 14.3 (atouEX) este o versiune reproiectată a programului 14.1. Aceste programe ilustrează diferența dintre cele două metode de I/O asincron. Programul atouEx este similar cu Programul 14.1, dar mută cea mai mare parte a codului de organizare a resurselor într-o rutină de finalizare și face multe variabile globale, astfel încât finalizatorul să le poată accesa. Cu toate acestea, Anexa B arată că în ceea ce privește performanța, atouEx poate concura bine cu alte metode care nu folosesc maparea fișierelor, în timp ce atouOV este mai lent.

Programul 14.2. atouEx: Conversie de fișiere folosind I/O extins
Convertiți un fișier din ASCII în Unicode folosind ADVANCED I/O. */
/* atouEX fișier1 fișier2 */

#define REC_SIZE 8096 /* Dimensiunea blocului nu este la fel de critică pentru performanță precum este cu atouOV. */
#define UREC_SIZE 2 * REC_SIZE

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

/* Prima structură SUPRAPUȚĂ este pentru citire, iar a doua este pentru scriere. Structurile și bufferele sunt alocate pentru fiecare operațiune viitoare. */
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++;
pentru (ic = 0; ic< MAX_OVRLP; ic++) {
OverLapIn.hEvent = (HANDLE)ic; /* Supraîncărcați evenimentul. */
OverLapOut.hEvent = (HANDLE)ic; /* Câmpuri. */
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;
dacă (CurPosIn.QuadPart< FileSize.QuadPart) ReadFileEx(hInputFile, AsRec, REC_SIZE, &OverLapIn , ReadDone);
CurPosIn.QuadPart += (LONGLONG)REC_SIZE;
/* Toate operațiunile de citire sunt efectuate. Intrați în starea de așteptare și rămâneți în ea până când toate înregistrările au fost procesate.*/
în timp ce(nTerminat< 2 * nRecord) SleepEx(INFINITE, TRUE);
_tprintf(_T("Conversia ASCII în Unicode finalizată.\n"));

static VOID WINAPI ReadDone (Cod DWORD, DWORD nBytes, LPOVERLAPPED pOv) (
/* Citirea finalizată. Convertiți datele și începeți înregistrarea. */
LARGE_INTEGER CurPosIn, CurPosOut;
/* Procesează scrierea și inițiază operația de scriere. */
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
CurPosOut.QuadPart = (CurPosIn.QuadPart / REC_SIZE) * UREC_SIZE;
OverLapOut.Offset = CurPosOut.LowPart;
OverLapOut.OffsetHigh = CurPosOut.HighPart;
/* Convertiți o intrare din ASCII în Unicode. */
pentru (i = 0; i< nBytes; i++) UnRec[i] = AsRec[i];
WriteFileEx(hOutputFile, UnRec, nBytes*2, &OverLapOut, WriteDone);
/* Pregătește structura OVERLAPPED pentru următoarea citire. */
CurPosIn.QuadPart += REC_SIZE * (LONGLONG)(MAX_OVRLP);
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;

static VOID WINAPI WriteDone (Cod DWORD, DWORD nBytes, LPOVERLAPPED pOv) (
/* Înregistrare finalizată. Inițiază următoarea operație de citire. */
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
dacă (CurPosIn.QuadPart< FileSize.QuadPart) {
ReadFileEx(hInputFile, AsRec, REC_SIZE, &OverLapIn, ReadDone);

I/O asincronă folosind mai multe fire

I/O suprapuse și extinse permit ca I/O să fie efectuate asincron într-un singur fir, deși sistemul de operare își creează propriile fire pentru a susține această funcționalitate. Într-o formă sau alta, metode de acest tip sunt adesea folosite în multe sisteme de operare timpurii pentru a suporta forme limitate de efectuare a operațiunilor asincrone pe sisteme cu un singur thread.

Cu toate acestea, Windows oferă suport multi-threading, astfel încât este posibil să se obțină același efect prin efectuarea de operațiuni I/O sincrone pe mai multe fire de execuție, care rulează independent. Aceste capabilități au fost demonstrate anterior utilizând servere multithreaded și programul grepMT (Capitolul 7). În plus, firele de execuție oferă o modalitate consistentă din punct de vedere conceptual și probabil mult mai simplă de a efectua operațiuni I/O asincrone. O alternativă la metodele utilizate în Programele 14.1 și 14.2 ar fi să se acorde fiecărui fir propriul său descriptor de fișier, astfel încât fiecare fir să poată procesa fiecare a patra înregistrare sincron.

Acest mod de utilizare a firelor este demonstrat în programul atouMT, care nu este dat în carte, dar este inclus în materialul postat pe site-ul Web. nu numai că atouMT poate rula pe orice versiune de Windows, dar este și mai simplu decât oricare dintre cele două programe I/O asincrone, deoarece contabilizarea utilizării resurselor este mai puțin complexă. Fiecare fir de execuție își menține pur și simplu propriile buffer-uri pe propria sa stivă și trece printr-o secvență de operații sincrone de citire, conversie și scriere. În același timp, performanța programului rămâne la un nivel destul de ridicat.

Notă

Programul atouMT.c, care se află pe site-ul Web, conține comentarii despre mai multe posibile capcane care vă pot aștepta atunci când permiteți mai multor fire să acceseze același fișier simultan. În special, toate identificatoarele de fișiere individuale trebuie create folosind funcția CreateHandle, mai degrabă decât funcția DuplicateHandle.

Personal, prefer să folosesc procesarea fișierelor cu mai multe fire decât I/O asincron. Firele sunt mai ușor de programat și oferă performanțe mai bune în majoritatea cazurilor.

Există două excepții de la această regulă generală. Prima dintre acestea, așa cum a fost arătat mai devreme în acest capitol, se referă la situații în care poate exista o singură operație nerezolvată, iar un descriptor de fișier poate fi utilizat în scopuri de sincronizare. O a doua excepție, mai importantă, apare în cazul porturilor de completare I/O asincrone, care va fi discutată la sfârșitul acestui capitol.

Temporizatoare de așteptare

Windows NT acceptă temporizatoarele așteptabile, care sunt un tip de obiect kernel care așteaptă.

Vă puteți crea oricând propriul semnal de sincronizare creând un fir de sincronizare care setează un eveniment ca urmare a trezirii după apelarea funcției Sleep. În programul serverNP (Programul 11.3), serverul folosește și un fir de sincronizare pentru a-și difuza periodic numele canalului. Prin urmare, cronometrele de așteptare oferă, deși oarecum redundante, o modalitate convenabilă de a organiza execuția sarcinilor în mod periodic sau conform unui program specific. În special, temporizatorul de așteptare poate fi configurat astfel încât semnalul să fie generat la un moment strict definit.

Temporizatorul de așteptare poate fi fie un temporizator de sincronizare, fie un temporizator de notificare cu resetare manuală. Cronometrul de sincronizare este asociat cu o funcție de apel indirect similară procedurii extinse de finalizare a I/O, în timp ce funcția de așteptare este utilizată pentru sincronizarea cu un temporizator de notificare resetat manual.

Mai întâi, trebuie să creați un indicator de temporizator folosind funcția CreateWaitableTimer.

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

Al doilea parametru, bManualReset, determină ce tip de cronometru ar trebui creat - sincronizare sau notificare. Programul 14.3 folosește un cronometru de sincronizare, dar schimbând comentariile și setarea parametrilor îl puteți transforma cu ușurință într-un cronometru de notificare. Rețineți că există și o funcție OpenWaitableTimer care poate folosi numele opțional furnizat de al treilea argument.

Cronometrul este creat inițial într-o stare inactivă, dar folosind funcția SetWaitableTimer îl puteți activa și specifica întârzierea inițială, precum și intervalul de timp dintre semnalele generate periodic.

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

hTimer este un handle valid pentru un cronometru creat folosind funcția CreateWaitableTimer.

Al doilea parametru, indicat de pointerul pDueTime, poate lua fie valori pozitive corespunzătoare timpului absolut, fie valori negative corespunzătoare timpului relativ, valorile reale fiind exprimate în unități de timp de 100 de nanosecunde și formatul lor descris. de structura FILETIME. Variabilele de tip FILETIME au fost introduse în Capitolul 3 și au fost deja folosite de noi în Capitolul 6 în programul timep (Programul 6.2).

Intervalul dintre semnale, specificat în al treilea parametru, este exprimat în milisecunde. Dacă această valoare este setată la 0, temporizatorul este semnalat o singură dată. Dacă acest parametru este pozitiv, cronometrul este periodic și se declanșează periodic până când este terminat prin apelarea funcției CancelWaitableTimer. Valorile negative ale intervalului specificat nu sunt permise.

Al patrulea parametru, pfnCompletionRoutine, este utilizat în cazul unui temporizator de sincronizare și specifică adresa rutinei de completare care este apelată când temporizatorul intră în starea semnalată. și furnizate că firul intră în starea de așteptare. Când această procedură este apelată, unul dintre argumente este pointerul specificat de al cincilea parametru, plArgToComplretionRoutine.

După setarea temporizatorului de sincronizare, puteți pune firul de execuție într-o stare de așteptare apelând funcția SleepEx pentru a permite apelarea rutinei de terminare. În cazul unui temporizator de notificare resetat manual, ar trebui să așteptați ca mânerul temporizatorului să intre în starea semnalului. Mânerul va rămâne în starea semnalată până la următorul apel la funcția SetWaitableTimer. Versiunea completă a programului 14.3, disponibilă pe site-ul Web, vă oferă posibilitatea de a efectua propriile experimente folosind un temporizator la alegere în combinație cu o procedură de terminare sau așteptând ca mânerul temporizatorului să intre în starea de semnal, rezultând în patru combinații diferite.

Ultimul parametru, fResume, este asociat modurilor de economisire a energiei. Pentru mai multe informații despre această problemă, consultați documentația de ajutor.

Funcția CancelWaitableTimer este utilizată pentru a anula funcția numită anterior SetWaitableTimer, dar nu modifică starea semnalului cronometrului. Pentru a face acest lucru, trebuie să apelați din nou funcția SetWaitableTimer.

Exemplu: Utilizarea unui temporizator de așteptare

Programul 14.3 demonstrează utilizarea unui temporizator de așteptare pentru a genera semnale periodice.

Programul 14.3. TimeBeep: generarea de semnale periodice
/* Capitolul 14. TimeBeep.s. Notificare sonoră periodică. */
/* Utilizare: perioada TimeBeep (în milisecunde). */

static BOOL WINAPI Handler (DWORD CntrlEvent);
Beeper static VOID APIENTRY (LPVOID, DWORD, DWORD);
volatil static BOOL Ieșire = FALSE;

int _tmain(int argc, LPTSTR argv) (
/* Interceptarea unei combinații de taste pentru a opri operațiunea. Vezi capitolul 4. */
SetConsoleCtrlHandler(Handler, TRUE);
DueTime.QuadPart = –(LONGLONG)Period * 10000;
/* DueTime este negativ pentru prima perioadă de timeout și este relativ la ora curentă. Perioada de așteptare este măsurată în ms (10 -3 s), iar DueTime este măsurată în unități de 100 ns (10 -7 s) pentru a se potrivi tipului FILETIME. */
hTimer = CreateWaitableTimer(NULL, FALSE /* „Temporizator de sincronizare” */, NULL);
SetWaitableTimer(hTimer, &DueTime, Period, Beeper, &Count, TRUE);
_tprintf(_T("Număr = %d\n"), Număr);
/* Valoarea contorului este incrementată în procedura temporizatorului. */
/* Intrați în starea de așteptare. */
_tprintf(_T("Complet. Contor = %d"), Count);

Beeper static VOID APIENTRY (LPVOID lpCount, DWORD dwTimerLowValue, DWORD dwTimerHighValue) (
*(LPDWORD)lpCount = *(LPDWORD)lpCount + 1;
_tprintf(_T("Se generează numărul semnalului: %d\n"), *(LPDWORD) lpCount);
Ventilator(1000 /* Frecvență. */, 250 /* Durată (ms). */);

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

După cum știți, există două moduri principale de intrare/ieșire: modul de schimb cu interogare a pregătirii dispozitivului de intrare/ieșire și modul de schimb cu întreruperi.

În modul de schimb cu un sondaj de pregătire, controlul de intrare/ieșire este efectuat de procesorul central. Procesorul central trimite o comandă către dispozitivul de control pentru a efectua o acțiune asupra dispozitivului de intrare/ieșire. Acesta din urmă execută comanda, traducând semnale înțelese de dispozitivul central și dispozitivul de control în semnale de înțeles pentru dispozitivul de intrare/ieșire. Dar viteza dispozitivului I/O este mult mai mică decât viteza procesorului central. Prin urmare, trebuie să așteptați un semnal gata foarte mult timp, interogând constant linia de interfață corespunzătoare pentru prezența sau absența semnalului dorit. Nu are sens să trimiți o nouă comandă fără a aștepta semnalul de gata care indică execuția comenzii anterioare. În modul sondaj de pregătire, driverul care controlează procesul de schimb de date cu un dispozitiv extern execută comanda „verificare semnal de pregătire” într-o buclă. Până când apare semnalul de gata, șoferul nu face altceva. În acest caz, desigur, timpul CPU este folosit irațional. Este mult mai profitabil să lansați o comandă I/O, să uitați pentru un timp de dispozitivul I/O și să treceți la executarea unui alt program. Și apariția unui semnal de pregătire este interpretată ca o solicitare de întrerupere de la un dispozitiv I/O. Aceste semnale de pregătire sunt semnalele de solicitare de întrerupere.

Modul de schimb de întreruperi este în esență un mod de control asincron. Pentru a nu pierde conexiunea cu dispozitivul, se poate porni o numărătoare inversă de timp, timp în care dispozitivul trebuie să execute comanda și să emită un semnal de cerere de întrerupere. Perioada maximă de timp în care un dispozitiv I/O sau controlerul său trebuie să emită un semnal de cerere de întrerupere este adesea numită timeout configurat. Dacă acest timp expiră după lansarea următoarei comenzi către dispozitiv, iar dispozitivul tot nu răspunde, atunci se ajunge la concluzia că comunicarea cu dispozitivul este pierdută și nu mai este posibilă controlul acestuia. Utilizatorul și/sau sarcina primesc mesajul de diagnosticare corespunzător.

Orez. 4.1. Control I/O

Șoferii. funcționând în modul întrerupere, sunt un set complex de module de program și pot avea mai multe secțiuni: o secțiune de pornire, una sau mai multe secțiuni de continuare și o secțiune de terminare.

Secțiunea de pornire inițiază operația I/O. Această secțiune este rulată pentru a porni un dispozitiv I/O sau pur și simplu pentru a iniția o altă operație I/O.

Secțiunea de continuare (pot fi mai multe dintre ele dacă algoritmul de control al schimbului de date este complex și sunt necesare mai multe întreruperi pentru a efectua o operațiune logică) efectuează activitatea principală de transfer de date. Secțiunea de continuare, de fapt, este principalul handler de întreruperi. Interfața utilizată poate necesita mai multe secvențe de comenzi de control pentru a controla I/O, iar dispozitivul are de obicei un singur semnal de întrerupere. Prin urmare, după executarea următoarei secțiuni de întrerupere, supervizorul de întrerupere trebuie să transfere controlul către o altă secțiune la următorul semnal gata. Acest lucru se face prin schimbarea adresei de procesare a întreruperii după executarea secțiunii următoare, dacă există o singură secțiune de întrerupere, atunci ea însăși transferă controlul unuia sau altui modul de procesare;

Secțiunea de terminare oprește de obicei dispozitivul I/O sau pur și simplu încheie operațiunea.

O operație I/O poate fi efectuată pe modulul de program care a solicitat operația în moduri sincrone sau asincrone. Semnificația acestor moduri este aceeași ca și pentru apelurile de sistem discutate mai sus - modul sincron înseamnă că modulul de program își suspendă activitatea până la finalizarea operațiunii I/O, iar cu modul asincron modulul de program continuă să se execute în modul multiprogram simultan cu Operare I/O. Diferența este că o operație I/O poate fi inițiată nu numai de un proces utilizator - în acest caz, operația este efectuată ca parte a unui apel de sistem - ci și de codul kernelului, de exemplu, codul de la subsistemul memoriei virtuale la citește o pagină care lipsește din memorie.

Orez. 7.1. Două moduri de efectuare a operațiunilor I/O

Subsistemul I/O trebuie să ofere clienților săi (procesele utilizatorului și codul kernelului) capacitatea de a efectua atât operațiuni I/O sincrone, cât și asincrone, în funcție de nevoile apelantului. Apelurile de sistem I/O sunt adesea încadrate ca proceduri sincrone datorită faptului că astfel de operațiuni durează mult timp și procesul utilizatorului sau firul de execuție va trebui în continuare să aștepte să fie primite rezultatele operației pentru a-și continua activitatea. Apelurile interne I/O de la modulele kernel sunt de obicei executate ca proceduri asincrone, deoarece codul nucleului are nevoie de libertate pentru a alege ce să facă în continuare după ce este solicitată o operație I/O. Utilizarea procedurilor asincrone duce la soluții mai flexibile, deoarece pe baza unui apel asincron, puteți construi întotdeauna unul sincron prin crearea unei proceduri intermediare suplimentare care blochează executarea procedurii de apelare până la finalizarea I/O-ului. Uneori, un proces de aplicație trebuie să efectueze și o operație de I/O asincronă, de exemplu, cu o arhitectură microkernel, atunci când o parte a codului rulează în modul utilizator ca proces de aplicație, dar realizează funcții ale sistemului de operare care necesită libertate completă de acțiune chiar și după apelarea operației I/O.