Partager via


Cet article a fait l'objet d'une traduction automatique.

Windows avec C++

Minuteries et E/S de pools de threads

Kenny Kerr

Kenny KerrEn cela, mon versement final sur le pool de threads de Windows 7, je vais pour couvrir les deux autres génératrices de rappel des objets fournis par l'API. Il n'y a même plus je pourrais écrire sur le pool de threads, mais après cinq articles qui couvrent pratiquement toutes ses fonctionnalités, vous devriez être confortable de l'utiliser pour vos applications de puissance efficace et efficiente.

Dans mon août (msdn.microsoft.com/magazine/hh335066) et en novembre (msdn.microsoft.com/magazine/hh547107) colonnes, j'ai décrit travailler et d'attendent les objets respectivement. Un objet de travail vous permet de présenter des travaux, sous la forme d'une fonction, directement au pool de threads pour exécution. La fonction s'exécute à la première occasion. Un objet d'attente raconte le pool de threads d'attendre pour un objet de synchronisation du noyau en votre nom et une fonction de la file d'attente lorsqu'il est signalé. C'est une alternative évolutive de primitives de synchronisation traditionnelle et une alternative efficace aux bureaux de vote. Il y a, cependant, de nombreux cas où les minuteries sont requis d'exécuter un code après un certain intervalle ou à certaines période normale. C'est peut-être en raison d'un manque de soutien de « push » dans certains protocole Web ou peut-être parce que vous implémentez un protocole de communication UDP-style et vous devez gérer les retransmissions. Heureusement, le pool de threads API fournit un objet timer pour gérer l'ensemble de ces scénarios d'une manière efficace et familier.

Objets de minuterie

La fonction CreateThreadpoolTimer crée un objet timer. Si la fonction réussit, elle retourne un pointeur opaque qui représente l'objet timer. Si elle échoue, il renvoie une valeur de pointeur null et fournit plus d'information via la fonction GetLastError. Étant donné un objet timer, la fonction CloseThreadpoolTimer informe le pool de threads que l'objet peut être libéré. Si vous avez suivi le long de la série, cela devrait tous sonore très familier. Voici une classe de caractères qui peut être utilisée avec le modèle de classe unique_handle handy, j'ai présenté ma colonne de juillet 2011 (msdn.microsoft.com/magazine/hh288076) :

struct timer_traits
{
  static PTP_TIMER invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_TIMER value) throw()
  {
    CloseThreadpoolTimer(value);
  }
};
typedef unique_handle<PTP_TIMER, timer_traits> timer;

Je peux maintenant utiliser le typedef et créer un objet timer comme suit :

void * context = ...
timer t(CreateThreadpoolTimer(its_time, context, nullptr));
check_bool(t);

Comme d'habitude, le paramètre final éventuellement accepte un pointeur vers un environnement afin que vous pouvez associer l'objet timer à un environnement, comme dans ma colonne de septembre 2011 (msdn.microsoft.com/­magazine/hh416747). Le premier paramètre est la fonction de rappel qui sera file d'attente au pool de threads chaque fois que la minuterie expire. Le rappel de la minuterie est déclaré comme suit :

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void * context, PTP_TIMER);

Pour contrôler quand et combien de fois l'expiration de la minuterie, vous utilisez la fonction SetThreadpoolTimer. Naturellement, son premier paramètre fournit l'objet timer, mais le second paramètre indique le temps due au cours de laquelle la minuterie doit expirer. Il utilise une structure FILETIME pour décrire le temps soit absolu ou relatif. Si vous n'êtes pas très bien comment cela fonctionne, je vous encourage à lire colonne du mois dernier, où j'ai décrit la sémantique autour de la structure FILETIME en détail. Voici un exemple simple, où j'ai défini la minuterie expirer en cinq secondes :

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
FILETIME relative_time(DWORD milliseconds)
{
  FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
  return ft.ft;
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Encore une fois, si vous êtes incertain sur le fonctionne de la fonction relative_time, veuillez lire ma colonne de novembre 2011. Dans cet exemple, la minuterie expirera après cinq secondes, à quel point le pool de threads mettra file d'attente une instance de la fonction de rappel its_time. À moins que l'action est intentée, aucun autres rappels ne seront en attente.

Vous pouvez également utiliser SetThreadpoolTimer pour créer une minuterie périodique qui est en file d'attente un rappel sur certains intervalles réguliers. Voici un exemple :

auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 500, 0);

Dans cet exemple, rappel du minuteur est d'abord queued après cinq secondes et ensuite chaque seconde moitié après que jusqu'à ce que l'objet timer est réinitialisé ou fermé. Contrairement à la fois due, la période est simplement spécifiée en millisecondes. Gardez à l'esprit qu'une minuterie périodique mettra file d'attente un rappel après expiration de la période donnée, peu importe de combien de temps il faut le rappel à exécuter. Cela signifie il est possible pour les rappels multiples exécuter simultanément, ou les rappels ou chevauchement, si l'intervalle est assez petit pour prennent un assez long moment d'exécuter.

Si vous devez vous assurer de rappels ne se chevauchent et l'heure de départ précis pour chaque période n'est pas qu'important, puis une approche différente pour la création d'une minuterie périodique pourrait être appropriée. Au lieu de spécifier une période dans l'appel à SetThreadpoolTimer, simplement réinitialiser la minuterie dans le rappel elle-même. De cette façon, vous pouvez vous assurer que les rappels seront superposeront jamais. Si rien d'autre, cela simplifie le débogage. Imaginez parcourant un rappel de minuterie dans le débogueur pour constater que le pool de threads a déjà queued quelques cas plus alors que vous étaient analyse votre code (ou recharger votre café). Avec cette approche, qui n'arrivera jamais. Voici à quoi il ressemble :

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void *, PTP_TIMER timer)
{
  // Your code goes here
  auto due_time = relative_time(500);
  SetThreadpoolTimer(timer, &due_time, 0, 0);
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Comme vous le voyez, l'échéance initiale temps est de cinq secondes, et puis je redéfinir le temps due à 500 ms, à la fin du rappel. J'ai pris parti du fait que la signature de rappel fournit un pointeur vers l'objet timer introductive, rendant le travail de réinitialisation de la minuterie très simple. Vous pouvez également utiliser RAII pour s'assurer que l'appel à SetThreadpoolTimer est appelée fiable avant le rappel retourne.

Vous pouvez appeler SetThreadpoolTimer avec une valeur de pointeur null pour le temps d'arrêter toute expirations de futurs minuterie qui peuvent entraîner des rappels supplémentaires. Vous aurez également besoin d'appeler WaitForThreadpool­TimerCallbacks afin d'éviter toute la race des conditions. Bien entendu, les objets de minuterie travaillent tout aussi bien avec des groupes de nettoyage, comme décrit dans ma colonne octobre 2011.

Paramètre final du SetThreadpoolTimer peut être un peu déroutant car la documentation fait référence à une « longueur de la fenêtre » comme un retard. Ce qui est tout à propos ? C'est en réalité une caractéristique qui affecte l'efficacité énergétique et contribue à réduire la puissance globale consommation. Il repose sur une technique appelée minuterie coalescence. De toute évidence, la meilleure solution consiste à éviter complètement les minuteries et utilisez plutôt des événements. Cela permet aux processeurs du système, la plus grande quantité de temps d'inactivité, encourageant ainsi d'entrer leurs États de ralenti de faible puissance autant que possibles. Encore, si les minuteries sont nécessaires, minuterie coalescence peut réduire l'ensemble la consommation d'énergie en réduisant le nombre d'interruptions de minuterie qui sont requis. Minuterie coalescence est basée sur l'idée d'un « délai tolérable » pour l'expiration de la minuterie. Compte tenu de certains retards tolérable, le noyau de Windows peut ajuster le délai d'expiration réelles pour coïncider avec les minuteries existants. Une bonne règle est de mettre le retard à un dixième de la période d'utilisation. Par exemple, si la minuterie doit expirer en 10 secondes, utilisez un délai d'une seconde, selon ce qui est approprié pour votre application. Plus le retard, l'occasion plus que le noyau a afin d'optimiser son timer interrompt. En revanche, rien de moins de 50 ms sera pas de grande utilisation car il commence à empiéter sur l'intervalle de horloge par défaut du noyau.

Objets d'achèvement d'e/S

Il est maintenant temps pour moi d'introduire le joyau du thread pool API: l'objet de fin d'entrée/sortie (e/S), ou tout simplement l'objet I/O. Quand j'ai introduit d'abord le pool de threads API, j'ai mentionné que le pool de threads est construit au dessus de l'API I/O de port achèvement. Traditionnellement, la mise en oeuvre de la plus évolutive e/S sur Windows était possible uniquement à l'aide de l'API I/O de port achèvement. J'ai écrit sur cette API dans le passé. Bien que pas particulièrement difficile à utiliser, il n'était pas toujours que facile à intégrer avec une application autres threading besoin. Grâce à l'API de pool de threads, cependant, vous avez le meilleur des deux mondes avec une API unique pour le travail, synchronisation, minuteurs et maintenant les e/S, trop. L'autre avantage est que l'exécution d'achèvement de I/O avec chevauchement avec le pool de threads est effectivement plus intuitive qu'à l'aide de l'API I/O de port achèvement, surtout lorsqu'il s'agit de la manipulation des handles de fichiers multiples et de multiples opérations superposées simultanément.

Comme vous l'avez peut-être deviné, la fonction CreateThreadpoolIo crée un objet I/O et la fonction CloseThreadpoolIo informe le pool de threads que l'objet peut être libéré. Voici une classe de caractères pour le modèle de la classe unique_handle :

struct io_traits
{
  static PTP_IO invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_IO value) throw()
  {
    CloseThreadpoolIo(value);
  }
};
typedef unique_handle<PTP_IO, io_traits> io;

La fonction CreateThreadpoolIo accepte un handle de fichier, ce qui implique qu'un objet I/O est capable de contrôler les e/S pour un seul objet. Naturellement, qu'objet doit soutenir avec chevauchement I/O, mais ceci inclut des types de ressources populaires tels que les fichiers de système de fichier, canaux nommés, sockets et ainsi de suite. Permettez-moi de vous démontrer avec un exemple simple d'attendre pour recevoir un paquet UDP à l'aide d'un socket. Pour gérer le socket, j'utilise unique_handle avec la classe de caractères suivants :

struct socket_traits
{
  static SOCKET invalid() throw()
  {
    return INVALID_SOCKET;
  }
  static void close(SOCKET value) throw()
  {
    closesocket(value);
  }
};
typedef unique_handle<SOCKET, socket_traits> socket;

Contrairement à des classes de caractères que j'ai montré jusqu'ici, dans ce cas la fonction invalide ne retourne une valeur de pointeur null. C'est parce que la fonction WSASocket, comme la fonction CreateFile, utilise une valeur inhabituelle pour indiquer un handle non valide. Compte tenu de cette classe de caractères et le typedef, je peux créer une socket et l'objet de I/O tout simplement :

socket s(WSASocket( ...
, WSA_FLAG_OVERLAPPED));
check_bool(s);
void * context = ...
io i(CreateThreadpoolIo(reinterpret_cast<HANDLE>(s.get()), io_completion, context, nullptr));
check_bool(i);

La fonction de rappel que les signaux de l'achèvement de toute opération d'e/S est déclarée comme suit :

void CALLBACK io_completion(PTP_CALLBACK_INSTANCE, void * context, void * overlapped,
  ULONG result, ULONG_PTR bytes_copied, PTP_IO)

Les paramètres uniques pour ce rappel doivent être familiers si vous avez utilisé avec chevauchement I/O avant. Parce que les e/S avec chevauchement est par nature asynchrone et permet de chevauchement des opérations e/S — d'où le nom se chevauchent I / O — il doit y avoir une façon d'identifier l'opération d'e/S particulière qui a terminé. C'est le but du paramètre superposé. Ce paramètre fournit un pointeur vers la structure OVERLAPPED ou WSAOVERLAPPED qui a été spécifié lorsqu'une opération d'e/S particulière a été initiée. L'approche traditionnelle d'emballage d'une structure OVERLAPPED dans une structure plus vaste d'accrocher les données plus au large de ce paramètre peut toujours être utilisé. Le paramètre superposé fournit un moyen d'identifier l'opération d'e/S particulière qui a terminé, tandis que le paramètre de contexte — comme d'habitude — fournit un contexte pour le point de terminaison I/O, indépendamment de toute opération particulière. Compte tenu de ces deux paramètres, vous devriez avoir aucune difficulté à coordonner le flux de données par le biais de votre application. Le paramètre résultat vous indique si l'opération avec chevauchement a réussi avec la ERROR_SUCCESS habituelle ou zéro, indiquant le succès. Enfin, le paramètre bytes_copied évidemment vous indique le nombre d'octets était effectivement lues ou écrit. Une erreur courante consiste à supposer que le nombre d'octets demandé était effectivement copié. Ne trompez : c'est la raison même de l'existence de ce paramètre.

La seule partie du soutien de I/O du pool de threads qui est un peu délicat est le traitement de la demande d'e/S elle-même. Il prend soin de ce code correctement. Avant d'appeler une fonction pour lancer une opération d'e/S asynchrone, comme ReadFile ou WSARecvFrom, vous devez appeler la fonction StartThreadpoolIo pour laisser le pool de threads savent qu'une opération d'e/S est sur le point de commencer. Le truc est que si l'opération d'e/S arrive à compléter de façon synchrone, alors vous devez en aviser le pool de threads de cela en appelant la fonction CancelThreadpoolIo. N'oubliez pas que l'achèvement de la I/O n'est pas nécessairement équivaut à la réussite. Une opération d'e/S pourrait réussir ou échouer les deux façon synchrone ou asynchrone. Quoi qu'il en soit, si l'opération d'e/S avisera pas le port de l'achèvement de son achèvement, vous devez informer le pool de threads. Voici ce que cela pourrait ressembler dans le contexte de la réception d'un paquet UDP :

StartThreadpoolIo(i.get());
auto result = WSARecvFrom(s.get(), ...
if (!result)
{
  result = WSA_IO_PENDING;
}
else
{
  result = WSAGetLastError();
}
if (WSA_IO_PENDING != result)
{
  CancelThreadpoolIo(i.get());
}

Comme vous pouvez le voir, je commence le processus en appelant StartThreadpoolIo à raconter le pool de threads qu'une opération d'e/S est sur le point de commencer. J'appelle ensuite WSARecvFrom pour obtenir des choses. Interpréter le résultat, c'est la partie cruciale. La fonction WSARecvFrom retourne zéro si l'opération s'est terminée avec succès, mais le port d'achèvement sera toujours informé, afin de changer le résultat à WSA_IO_PENDING. Tout autre résultat de WSARecvFrom indique l'échec, à l'exception, bien entendu, de WSA_IO_PENDING, qui signifie simplement que l'opération a été lancée avec succès, mais elle sera achevée plus tard. Maintenant, j'ai simplement appeler les CancelThreadpoolIo si le résultat n'est pas en attente de garder le pool de threads up to speed. Différents points de terminaison de I/O peuvent fournir différente sémantique. Par exemple, fichier I/O peut être configuré pour éviter les avisant le port de terminaison sur achèvement synchrone. Vous devrez ensuite appeler CancelThreadpoolIo comme il convient.

Comme les autres génératrices de rappel objets dans le pool de threads API, en attendant de rappels d'e/s objets peuvent être annulés en utilisant la fonction WaitForThreadpoolIoCallbacks. Juste garder à l'esprit que cela va annuler toute rappels en attente, mais pas annuler tout en attente d'opérations d'e/S eux-mêmes. Vous devrez toujours utiliser la fonction appropriée pour annuler l'opération afin d'éviter toute condition de course. Cela vous permet en toute sécurité gratuitement toute structures OVERLAPPED, et ainsi de suite.

Et c'est tout pour le pool de threads API. Comme je l'ai dit, il n'y a plus je pourrais écrire sur cette API puissante, mais étant donné la visite détaillée, j'ai fourni jusqu'à présent, je suis certain que vous êtes bien sur votre façon de l'utiliser pour votre prochaine application de puissance. Joignez-vous à moi le mois prochain je continue d'explorer Windows avec C++.

Kenny Kerr est un artisan de logiciel avec une passion pour le développement de Windows natif. Le joindre à kennykerr.ca.