web-dev-qa-db-fra.com

Pourquoi ai-je besoin d'un brin par connexion lors de l'utilisation de boost :: asio?

J'examine HTTP Server exemple sur le site Web de Boost.

Pourriez-vous expliquer pourquoi j'ai besoin de strand par connexion? Comme je peux le voir, nous appelons read_some uniquement dans le gestionnaire de lecture-événement. Donc en gros read_some les appels sont séquentiels donc il n'y a pas besoin de strand (et point 2 du 3ème paragraphe dit la même chose). Où est le risque dans un environnement multi-thread?

45
expert

La documentation est correcte. Avec une implémentation de protocole semi-duplex, telle que HTTP Server , le strand n'est pas nécessaire. Les chaînes d'appel peuvent être illustrées comme suit:

void connection::start()
{
  socket.async_receive_from(..., &handle_read);  ----.
}                                                    |
    .------------------------------------------------'
    |      .-----------------------------------------.
    V      V                                         |
void connection::handle_read(...)                    |
{                                                    |
  if (result)                                        |
    boost::asio::async_write(..., &handle_write); ---|--.
  else if (!result)                                  |  |
    boost::asio::async_write(..., &handle_write);  --|--|
  else                                               |  |
    socket_.async_read_some(..., &handle_read);  ----'  |
}                                                       |
    .---------------------------------------------------'
    |
    V
void handle_write(...)

Comme le montre l'illustration, un seul événement asynchrone est démarré par chemin. Sans possibilité d'exécution simultanée des gestionnaires ou des opérations sur socket_, Il est dit qu'il s'exécute dans un brin implicite.


Sécurité des fils

Bien qu'il ne se présente pas comme un problème dans l'exemple, je voudrais souligner un détail important des brins et des opérations composées, tels que boost::asio::async_write . Avant d'expliquer les détails, couvrons d'abord le modèle de sécurité du fil avec Boost.Asio. Pour la plupart des objets Boost.Asio, il est sûr d'avoir plusieurs opérations asynchrones en attente sur un objet; il est simplement spécifié que les appels simultanés sur l'objet ne sont pas sûrs. Dans les diagrammes ci-dessous, chaque colonne représente un thread et chaque ligne représente ce qu'un thread fait à un moment donné.

Il est sûr qu'un seul thread effectue des appels séquentiels tandis que d'autres threads n'en font aucun:

 thread_1 | thread_2 
 -------------------------------------- + ----- ---------------------------------- 
 socket.async_receive (...); | ... 
 socket.async_write_some (...); | ...

Il est sûr que plusieurs threads effectuent des appels, mais pas simultanément:

 thread_1 | thread_2 
 -------------------------------------- + ----- ---------------------------------- 
 socket.async_receive (...); | ... 
 ... | socket.async_write_some (...);

Cependant, il n'est pas sûr que plusieurs threads effectuent des appels simultanément1:

 thread_1 | thread_2 
 -------------------------------------- + ----- ---------------------------------- 
 socket.async_receive (...); | socket.async_write_some (...); 
 ... | ...

Brins

Pour éviter des appels simultanés, les gestionnaires sont souvent appelés à partir de brins. Cela se fait soit par:

  • Envelopper le gestionnaire avec strand.wrap . Cela renverra un nouveau gestionnaire, qui sera distribué via le brin.
  • Publication ou répartition directement via le volet.

Les opérations composées sont uniques en ce sens que les appels intermédiaires au flux sont invoqués dans le gestionnaire le brin, s'il y en a un, au lieu du brin dans lequel l'opération composée est initiée. Par rapport à d'autres opérations, cela présente une inversion de l'endroit où le brin est spécifié. Voici un exemple de code se concentrant sur l'utilisation de brins, qui montrera un socket lu via une opération non composée et écrit simultanément avec une opération composée.

void start()
{
  // Start read and write chains.  If multiple threads have called run on
  // the service, then they may be running concurrently.  To protect the
  // socket, use the strand.
  strand_.post(&read);
  strand_.post(&write);
}

// read always needs to be posted through the strand because it invokes a
// non-composed operation on the socket.
void read()
{
  // async_receive is initiated from within the strand.  The handler does
  // not affect the strand in which async_receive is executed.
  socket_.async_receive(read_buffer_, &handle_read);
}

// This is not running within a strand, as read did not wrap it.
void handle_read()
{
  // Need to post read into the strand, otherwise the async_receive would
  // not be safe.
  strand_.post(&read);
}

// The entry into the write loop needs to be posted through a strand.
// All intermediate handlers and the next iteration of the asynchronous write
// loop will be running in a strand due to the handler being wrapped.
void write()
{
  // async_write will make one or more calls to socket_.async_write_some.
  // All intermediate handlers (calls after the first), are executed
  // within the handler's context (strand_).
  boost::asio::async_write(socket_, write_buffer_,
                           strand_.wrap(&handle_write));
}

// This will be invoked from within the strand, as it was a wrapped
// handler in write().
void handle_write()
{
  // handler_write() is invoked within a strand, so write() does not
  // have to dispatched through the strand.
  write();
}

Importance des types de gestionnaires

De plus, dans les opérations composées, Boost.Asio utilise recherche dépendante de l'argument (ADL) pour appeler des gestionnaires intermédiaires via le brin du gestionnaire d'achèvement. En tant que tel, il est important que le type du gestionnaire de complétion ait les hooks asio_handler_invoke() appropriés. Si l'effacement de type se produit sur un type qui n'a pas les crochets asio_handler_invoke() appropriés, comme un cas où un boost::function Est construit à partir du type de retour de strand.wrap, Alors intermédiaire les gestionnaires s'exécuteront en dehors du brin, et seul le gestionnaire d'achèvement s'exécutera dans le brin. Voir cette réponse pour plus de détails.

Dans le code suivant, tous les gestionnaires intermédiaires et le gestionnaire d'achèvement s'exécuteront dans le brin:

boost::asio::async_write(stream, buffer, strand.wrap(&handle_write));

Dans le code suivant, seul le gestionnaire d'achèvement s'exécutera dans le brin. Aucun des gestionnaires intermédiaires ne s'exécutera dans le volet:

boost::function<void()> handler(strand.wrap(&handle_write));
boost::asio::async_write(stream, buffer, handler);

1. Le historique des révisions documente une anomalie à cette règle. Si pris en charge par le système d'exploitation, synchrone les opérations de lecture, d'écriture, d'acceptation et de connexion sont thread-safe. Je l'inclus ici pour être complet, mais je suggère de l'utiliser avec prudence.

91
Tanner Sansbury

Je crois que c'est parce que l'opération composée async_write . async_write est composé de plusieurs socket :: async_write_some de manière asynchrone. Strand est utile pour sérialiser ces opérations. Chris Kohlhoff, l'auteur de asio, en parle brièvement dans son boostcon talk vers 1:17.

8
Vikas