Este artigo foi traduzido por máquina.

Windows com C++

A evolução da sincronização no Windows e no C++

Kenny Kerr

 

Kenny KerrQuando eu comecei a escrever software simultânea, C++ não tinha nenhum suporte para sincronização. Próprio Windows teve somente um punhado dos primitivos de sincronização, todos os quais foram implementados no kernel. Eu tendia a usar uma seção crítica, a menos que eu precisava para sincronizar entre processos, caso em que usei um mutex. Em termos gerais, esses dois são bloqueios ou bloquear objetos.

O mutex leva o nome do conceito de "exclusão mútua", outro nome para sincronização. Refere-se à garantia de que apenas um thread pode acessar algum recurso no momento. A seção crítica leva o nome da seção de código que pode estar acessando um recurso tão real. Para garantir a exactidão, apenas um thread pode executar esta seção crítica de código em um momento. Esses dois objetos de bloqueio têm recursos diferentes, mas é útil lembrar que eles são apenas bloqueios, oferecem garantias de exclusão mútua e ambos podem ser usados para demarcar as seções críticas do código.

Hoje, o cenário de sincronização mudou dramaticamente. Há uma infinidade de opções para o programador de C++. Windows agora oferece suporte a muitas mais funções de sincronização e C++-se finalmente fornece uma interessante colecção de recursos de sincronização e simultaneidade para aqueles usando um compilador que suporte o C + + 11 padrão.

Na coluna deste mês, vou explorar o estado de sincronização no Windows e C++. Vou começar com uma revisão dos primitivos de sincronização fornecido pelo próprio Windows e, em seguida, considerar as alternativas fornecidas pela biblioteca C++ padrão. Se portabilidade é a sua principal preocupação, as novas adições de biblioteca C++ será muito atraentes. Se, no entanto, a portabilidade é menos uma preocupação e desempenho é fundamental, em seguida, familiarizando-se com o Windows agora oferece será importante. Vamos mergulhar logo em.

Seção crítica

Primeiro-se é o objeto de seção crítica. Esse bloqueio é muito usado por inúmeros aplicativos, mas tem uma história sórdida. Quando eu comecei a usar seções críticas, eles eram realmente simples. Para criar um tal bloqueio, tudo que você precisava era para alocar uma estrutura CRITICAL_SECTION e chamar a função InitializeCriticalSection para prepará-lo para uso. Esta função não retorna um valor, o que implica que não pode falhar. Naqueles dias, no entanto, foi necessário para esta função criar vários recursos do sistema, nomeadamente um objeto de evento de kernel, e era possível que em situações extremamente baixa memória falhará, resultando em uma exceção estruturada que está sendo gerada. Ainda assim, isso foi bastante raro, então a maioria dos desenvolvedores ignorou essa possibilidade.

Com a popularidade de COM, o uso de seções críticas disparou porque muitas classes COM usado seções críticas para a sincronização, mas em muitos casos, houve pouca ou nenhuma contenção real para falar. Quando computadores multiprocessador tornou-se mais difundidas, evento interno na seção crítica vi ainda menos uso porque a seção crítica giraria rapidamente no modo de usuário enquanto espera para adquirir o bloqueio. Uma contagem de rotação pequena significou que muitos períodos de curta duração da disputa poderiam evitar uma transição de kernel, melhorando o desempenho.

Em torno deste tempo, alguns desenvolvedores do kernel perceberam que eles poderiam melhorar dramaticamente a escalabilidade do Windows se eles adiada a criação de objetos de evento de seção crítica até que houvesse suficiente contenção para exigir a sua presença. Isso parecia ser uma boa idéia até que os desenvolvedores perceberam que isso significava que embora InitializeCriticalSection agora possivelmente não poderia falhar, a função EnterCriticalSection (usada para aguardar a propriedade de bloquear) não era confiável. Isso poderia não tão facilmente ser negligenciado pelos desenvolvedores, porque introduziu uma variedade de condições de falha que teria fiz seções críticas todos, mas impossível de usar corretamente e quebrar inúmeras aplicações. Ainda assim, as vitórias de escalabilidade não poderiam ser negligenciadas.

Um desenvolvedor de kernel finalmente chegou a uma solução na forma de um objeto de evento de kernel de novo e não documentado, chamado de um evento com chave. Você pode ler um pouco sobre isso no livro, "Windows Internals," por Mark E. Russinovich, David A. Salomão e Alex Ionescu (Microsoft Press, 2012), mas basicamente, em vez de exigir que um objeto de evento para cada seção crítica, um único evento chave poderia ser usado para todas as seções críticas do sistema. Isso funciona porque um objeto de evento chave é apenas isso: Ele se baseia em uma chave, que é apenas um tamanho de ponteiro identificador que é naturalmente o espaço de endereço local.

Havia certamente uma tentação para atualizar seções críticas para usar com chave eventos exclusivamente, mas porque muitos depuradores e outras ferramentas dependem internos de seções críticas, o evento com chave foi usado apenas como um último recurso se o kernel não conseguiu alocar um objeto de evento regular.

Isto pode soar como um monte de história irrelevante mas para o fato de que o desempenho dos eventos de chave foi significativamente melhorado durante o ciclo de desenvolvimento do Windows Vista, e isso levou à introdução de um bloqueio completamente novo objeto que foi mais simples e rápido, mas mais sobre isso em um minuto.

Como o objeto de seção crítica agora é isento de falhas devido a condições de pouca memória, é realmente muito simples de usar. Figura 1 fornece um wrapper simples.

Figura 1: O bloqueio de seção crítica

class lock
{
  CRITICAL_SECTION h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock()
  {
    InitializeCriticalSection(&h);
  }
  ~lock()
  {
    DeleteCriticalSection(&h);
  }
  void enter()
  {
    EnterCriticalSection(&h);
  }
  bool try_enter()
  {
    return 0 != TryEnterCriticalSection(&h);
  }
  void exit()
  {
    LeaveCriticalSection(&h);
  }
  CRITICAL_SECTION * handle()
  {
    return &h;
  }
};

A função EnterCriticalSection que já mencionado é complementada por uma função TryEnterCriticalSection que fornece uma alternativa sem bloqueio. A função LeaveCriticalSection libera o bloqueio e DeleteCriticalSection libera quaisquer recursos do kernel que podem ter sido alocados ao longo do caminho.

Assim, a seção crítica é uma escolha razoável. Ele executa muito bem como ele tenta evitar transições de kernel e alocação de recursos. Ainda assim, ele tem um pouco de bagagem que deve transportar devido a sua história e aplicação compatibilidade.

Mutex

O objeto mutex é um objeto de sincronização de kernel verdadeira. Ao contrário de seções críticas, um bloqueio de mutex sempre consome recursos alocados do kernel. O benefício, é claro, é que o kernel é então capaz de fornecer sincronização entre processos, devido a sua consciência do bloqueio. Como um objeto de kernel, fornece os atributos usuais — tais como um nome — que pode ser usado para abrir o objeto de outros processos ou apenas para identificar o bloqueio em um depurador. Você também pode especificar uma máscara de acesso para restringir o acesso ao objeto. Como um bloqueio intraprocess, é a um exagero, um pouco mais complicado de usar e muito mais lento. Figura 2 fornece um wrapper simples para um mutex sem nome que é efetivamente local de processo.

Figura 2 o bloqueio Mutex

#ifdef _DEBUG
  #include <crtdbg.h>
  #define ASSERT(expression) _ASSERTE(expression)
  #define VERIFY(expression) ASSERT(expression)
  #define VERIFY_(expected, expression) ASSERT(expected == expression)
#else
  #define ASSERT(expression) ((void)0)
  #define VERIFY(expression) (expression)
  #define VERIFY_(expected, expression) (expression)
#endif
class lock
{
  HANDLE h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock() :
    h(CreateMutex(nullptr, false, nullptr))
  {
    ASSERT(h);
  }
  ~lock()
  {
    VERIFY(CloseHandle(h));
  }
  void enter()
  {
    VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  }
  bool try_enter()
  {
    return WAIT_OBJECT_0 == WaitForSingleObject(h, 0);
   }
  void exit()
  {
    VERIFY(ReleaseMutex(h));
  }
  HANDLE handle()
  {
    return h;
  }
};

A função CreateMutex cria o fechamento e a função CloseHandle comum fecha o processo de manipular, que efetivamente diminui a contagem de referência do lock no kernel. Espera para a posse de bloqueio é realizada com a função WaitForSingleObject polivalente que verifica e, opcionalmente, aguarda o sinalizado estado de uma variedade de objetos do kernel. Seu segundo parâmetro indica quanto tempo o thread de chamada deve bloquear enquanto aguarda para adquirir o bloqueio. É a constante infinita — não surpreendentemente — uma espera por tempo indeterminado, enquanto um valor de zero impede que o thread espera a todos e só vai adquirir o bloqueio se é gratuito. Finalmente, a função ReleaseMutex libera o bloqueio.

O bloqueio mutex é um grande martelo com muita energia, mas ele tem um custo de desempenho e complexidade. O wrapper em Figura 2 está repleta de declarações para indicar as formas possíveis que pode falhar, mas é as implicações de desempenho que desqualificam o bloqueio mutex na maioria dos casos.

Evento

Antes de eu falar sobre um bloqueio de alto desempenho, preciso introduzir um objeto de sincronização de kernel mais, um a que já aludi. Embora não realmente um bloqueio, em que ele não fornece um mecanismo para implementar diretamente a exclusão mútua, o objeto de evento é de vital importância para a coordenação do trabalho entre threads. Na verdade, é o mesmo objeto usado internamente pelo bloqueio de seção crítica — e, além disso, ele vem a calhar quando implementar todos os tipos de padrões de concorrência de forma eficiente e escalável.

A função CreateEvent cria o evento e — como o mutex — a função CloseHandle fecha o identificador, liberando o objeto no kernel. Porque não é realmente um bloqueio, ele tem nenhuma semântica de aquisição/liberação. Em vez disso, é a personificação muito da sinalização funcionalidade fornecida por muitos dos objetos de kernel. Para entender como as obras de sinalização, você precisa de apreciar que um objeto de evento pode ser criado em um dos dois Estados. Se você passar true para o segundo parâmetro de na CreateEvent, o objeto de evento resultante é dito ser um evento de reset manual; caso contrário, é criado um evento auto-reset. Um manual redefinir evento requer que você defina manualmente e restaurar o estado do objeto sinalizado. As funções SetEvent e ResetEvent são fornecidas para essa finalidade. Um auto auto-redefinir evento­redefine automaticamente (alterações de sinalizado para não sinalizado) quando um segmento em espera é liberado. Assim, um evento de auto-reset é útil quando um thread precisa coordenar com um outro segmento, considerando um manual-redefinir evento é útil quando um thread precisa coordenar com qualquer número de segmentos. Chamando o SetEvent em um evento de auto-reset vai lançar no máximo de um segmento, Considerando que, com um evento de rearme manual, que chamar vai liberar todos os segmentos de espera. Como o mutex, à espera de um evento para tornar-se sinalizado é conseguida com a função WaitForSingleObject. Figura 3 fornece um wrapper simples para um evento sem nome que pode ser construído em qualquer modo.

Figura 3 o sinal de evento

class event
{
  HANDLE h;
  event(event const &);
  event const & operator=(event const &);
public:
  explicit event(bool manual = false) :
    h(CreateEvent(nullptr, manual, false, nullptr))
  {
    ASSERT(h);
  }
  ~event()
  {
    VERIFY(CloseHandle(h));
  }
  void set()
  {
    VERIFY(SetEvent(h));
  }
  void clear()
  {
    VERIFY(ResetEvent(h));
  }
  void wait()
  {
    VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  }
};

Bloqueio de leitor/gravador reduzido

O nome de bloqueio de leitor/gravador Slim (SRW) pode ser um bocado, mas a palavra operativa é "magro". Programadores podem ignorar esse bloqueio devido à sua capacidade para distinguir entre compartilhado de leitores e escritores exclusivas, talvez pensando que se trata de um exagero, quando tudo o que precisamos é de uma seção crítica. Como se vê, este é o bloqueio mais simples para lidar com e também de longe o mais rápido e você certamente não precisa ter compartilhado leitores para usá-lo. Tem esta fama rápida não só porque ele depende do objeto de evento chave eficiente, mas também porque ele é implementado principalmente no modo de usuário e só cai de volta para o kernel se a contenção é tal que o segmento seria melhor dormir. Novamente, o crítico seção e mutex objetos fornecem recursos adicionais que você pode exigir, como recursiva ou bloqueios entre processos, mas mais frequentemente do que não, tudo que você precisa é de um fechamento rápido e leve para uso interno.

Esse bloqueio depende exclusivamente os eventos chave que mencionei antes, e como tal, é extremamente leve, apesar de uma grande quantidade de funcionalidade. O bloqueio de SRW requer apenas um ponteiro -­tamanho a quantidade de armazenamento, que é alocado pelo processo de chamada, em vez de kernel. Por esta razão, a função de inicialização, InitializeSRWLock, não pode falhar e apenas assegura que o bloqueio contém o padrão de bit apropriado antes de serem utilizados.

Aguardando a propriedade de fechamento é conseguida usando a adquirir ou­SRWLockExclusive função para um bloqueio de escritor chamado ou usando a função AcquireSRWLockShared para bloqueios de leitor. No entanto, a terminologia exclusiva e compartilhada é mais apropriada. Há liberação correspondente e tente adquirir funções como um esperaria para ambos exclusivo e compartilhado de modos. Figura 4 fornece um wrapper simples para um bloqueio de SRW de modo exclusivo. Não seria difícil para você adicionar as funções de modo compartilhado, se necessário. No entanto, observe que não há nenhum destruidor porque não existem recursos para livre.

Figura 4 o bloqueio de SRW

class lock
{
  SRWLOCK h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock()
  {
    InitializeSRWLock(&h);
  }
  void enter()
  {
    AcquireSRWLockExclusive(&h);
  }
  bool try_enter()
  {
    return 0 != TryAcquireSRWLockExclusive(&h);
  }
  void exit()
  {
    ReleaseSRWLockExclusive(&h);
  }
  SRWLOCK * handle()
  {
    return &h;
  }
};

Variável de condição

O objeto de sincronização final que preciso introduzir é a variável de condição. Este é, talvez, aquele com o qual a maioria dos programadores será desconhecidos. Tem, no entanto, notei um interesse renovado em variáveis de condição nos últimos meses. Isso pode ter algo a ver com C + + 11, mas a idéia não é nova e o suporte para este conceito tem sido ao redor por algum tempo no Windows. Na verdade, o Microsoft .NET Framework apoiou o padrão variável de condição desde a primeira versão, embora fundiu-se para a classe Monitor, limitando sua utilidade em alguns aspectos. Mas este interesse renovado é também graças a incríveis eventos chave que permitiu variáveis de condição a ser introduzido pelo Windows Vista, e eles só melhoraram desde. Embora uma variável de condição é apenas um padrão de concorrência e, assim, pode ser implementada com outros primitivos, sua inclusão no sistema operacional significa que ele pode obter um desempenho surpreendente e libera os programadores de ter de garantir a exactidão de tal código. Na verdade, se você está empregando primitivos de sincronização do sistema operacional, é quase impossível assegurar a exatidão de alguns padrões de simultaneidade sem a ajuda do sistema operacional propriamente dito.

O padrão variável de condição é bastante comum, se você pensar sobre isso. Um programa precisa esperar por algumas condições a serem cumpridas antes que ele possa prosseguir. Avaliar esta condição envolve adquirir um bloqueio para avaliar algum estado compartilhado. Se, no entanto, a condição ainda não foi atendida, o bloqueio deve ser lançado para permitir que algum outro segmento cumprir a condição. O thread de avaliação, em seguida, deve esperar até que a condição for atendida antes de adquirir mais uma vez o bloqueio. Uma vez que o bloqueio é readquirido, a condição novamente deve ser avaliada para evitar a condição de corrida óbvio. Implementar isso é mais difícil do que parece porque há, de fato, outras armadilhas se preocupar — e implementá-lo de forma eficiente é ainda mais difícil. O pseudocódigo a seguir ilustra o problema:

lock-enter
while (!condition-eval)
{
  lock-exit
  condition-wait
  lock-enter
}
// Do interesting stuff here
lock-exit

Mas mesmo nessa ilustração aí reside um erro sutil. Para funcionar corretamente, a condição deve ser aguardada com antes que o bloqueio é encerrado, mas fazendo assim não iria funcionar porque o bloqueio, então seria nunca lançado. A capacidade de atomicamente liberar um objeto e esperar em outro é tão crítica que o Windows fornece a função de SignalObjectAndWait para fazer assim para determinados objetos kernel. Mas porque o bloqueio de SRW vive principalmente no modo de usuário, é necessária uma solução diferente. Inserir variáveis de condição.

Como o bloqueio de SRW, a variável de condição ocupa apenas uma única tamanho de ponteiro quantidade de armazenamento e é inicializada com a função de InitializeConditionVariable à prova de falhas. Como com bloqueios de SRW, não existem recursos para livre, que quando a variável de condição não é mais necessária a memória simplesmente pode ser recuperada.

Porque a própria condição é específico do programa, ela é deixada ao chamador para escrever o padrão como um tempo loop com o corpo sendo uma única chamada para a função SleepConditionVariableSRW. Esta função atomicamente libera o bloqueio de SRW enquanto espera para ser acordado, uma vez que a condição for atendida. Há também uma função SleepConditionVariableCS correspondente se você quiser usar variáveis de condição com um bloqueio de seção crítica, em vez disso.

A função WakeConditionVariable é chamada para despertar uma única espera ou dormindo, thread. O thread woken irá readquirir o bloqueio antes de retornar. Alternativamente, a função WakeAllConditionVariable pode ser usada para despertar todos os segmentos de espera. Figura 5fornece um wrapper simples com o necessário ao loop. Note que é possível para o segmento a ser despertado é imprevisível, e o loop while garante que a condição é sempre checada depois que o thread reacquires o Bloquear. Também é importante observar que o predicado é sempre avaliado, mantendo o bloqueio.

Figura 5 a variável de condição

class condition_variable
{
  CONDITION_VARIABLE h;
  condition_variable(condition_variable const &);
  condition_variable const & operator=(condition_variable const &);
public:
  condition_variable()
  {
    InitializeConditionVariable(&h);
  }
  template <typename T>
  void wait_while(lock & x, T predicate)
  {
    while (predicate())
    {
      VERIFY(SleepConditionVariableSRW(&h, x.handle(), INFINITE, 0));
    }
  }
  void wake_one()
  {
    WakeConditionVariable(&h);
  }
  void wake_all()
  {
    WakeAllConditionVariable(&h);
  }
};

Fila de bloqueio

Para dar este de alguma forma, vou usar uma fila de bloqueio como um exemplo. Gostaria de salientar que não é recomendável bloquear filas em geral. Você pode ser melhor servido usando uma porta de conclusão de e/s ou o pool de threads do Windows, é apenas uma abstração mais antigo, ou concurrent_queueclasse mesmo do Runtime de simultaneidade. Qualquer coisa sem bloqueio é geralmente preferida. Ainda assim, uma fila de bloqueio é um conceito simples de entender e algo que muitos desenvolvedores parecem achar útil. É certo que nem todo programa precisa escalar, mas cada programa precisa ser correta. Uma fila de bloqueio também fornece ampla oportunidade para empregar a sincronização para correção — e, claro, ampla oportunidade de obtê-lo errado.

Considere a implementação de uma fila de bloqueio com apenas um bloqueio e um evento. O bloqueio protege a fila compartilhada e o evento sinaliza a um consumidor que um produtor tem empurrado algo para a fila. Figura 6 fornece um exemplo simples usando um evento auto-reset. Eu usei esse modo evento porque o método push filas apenas um único elemento e, assim, eu só quero um consumidor a ser despertado para colocá-la fora da fila. O método push adquire o bloqueio, o elemento de filas e, em seguida, sinaliza o evento para despertar qualquer consumidor espera. O método pop adquire o bloqueio e, em seguida, aguarda até que a fila não está vazia antes de dequeuing um elemento e retorná-lo. Ambos os métodos usam uma classe lock_block. Para resumir, não tem sido incluído, mas simplesmente chama o bloqueio entrar método no seu construtor e o método de saída em seu destruidor.

Figura 6 Auto-Reset bloqueio fila

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  event e;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    e.set();
  }
  T pop()
  {
    lock_block block(x);
    while (q.empty())
    {
      x.exit(); e.wait(); // Bug!
x.enter();
    }
    T v = q.front();
    q.pop_front();
    return v;
  }
};

No entanto, observe o impasse provavelmente porque as chamadas de saída e espera não são atômicas. Se o bloqueio de um mutex, eu poderia usar a função SignalObjectAndWait, mas o desempenho da fila bloqueio iria sofrer.

Outra opção é usar um evento de rearme manual. Em vez de sinalização sempre que um elemento está na fila, basta defina dois Estados. O evento pode ser sinalizado para contanto que há elementos na fila e não sinalizado quando ela está vazia. Isso também irá realizar muito melhor, porque há menos chamadas ao kernel para sinalizar o evento. Figura 7 fornece um exemplo disso. Observe como o método push define o evento se a fila tem um elemento. Isso evita chamadas desnecessárias para a função SetEvent. O método pop obedientemente limpa o evento se encontra a fila vazia. Como há vários elementos na fila, qualquer número de consumidores pode pop elementos fora da fila sem envolver o objeto de evento, melhorando a escalabilidade.

Figura 7 fila de bloqueio de rearme Manual

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  event e;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue() :
    e(true) // manual
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    if (1 == q.size())
    {
      e.set();
    }
  }
  T pop()
  {
    lock_block block(x);
    while (q.empty())
    {
      x.exit();
      e.wait();
      x.enter();
    }
    T v = q.front();
    q.pop_front();
    if (q.empty())
    {
      e.clear();
    }
    return v;
  }
};

Neste caso não há nenhum deadlock potencial na sequência de saída-espera-entrar porque outro consumidor não pode roubar o evento, dado que é um evento de rearme manual. É difícil de bater que em termos de desempenho. No entanto, uma alternativa (e talvez mais natural) solução é usar uma variável de condição em vez de um evento. Isso é facilmente feito com a classe condition_variable no Figura 5 e é semelhante para a fila de bloqueio de rearme manual, embora seja um pouco mais simples. Figura 8 fornece um exemplo. Observe como as intenções de semântica e simultaneidade se tornam mais claras de como os objetos de sincronização de nível mais alto são empregados. Esta clareza ajuda a evitar bugs simultâneos que afligem muitas vezes código mais obscura.

Figura 8 condição variável bloqueio fila

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  condition_variable cv;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    cv.wake_one();
  }
  T pop()
  {
    lock_block block(x);
    cv.wait_while(x, [&]()
    {
      return q.empty();
    });
    T v = q.front();
    q.pop_front();
    return v;
  }
};

Finalmente, devo mencionar que C + + 11 agora fornece um bloqueio, chamado um mutex, bem como um condition_variable. O C + + 11 mutex não tem nada a ver com o mutex de Windows. Da mesma forma, o C + + 11 condition_variable não é baseado na variável de condição de Windows. Esta é uma boa notícia em termos de portabilidade. Ele pode ser usado em qualquer lugar você pode encontrar um compilador de C++ em conformidade. Por outro lado, o C + + 11 implementação na versão 2012 de C++ Visual executa muito mal em relação à variável Windows SRW lock e condição. Figura 9 fornece um exemplo de uma fila de bloqueio, implementado com o padrão C + + 11 tipos de biblioteca.

Figura 9 + + bloqueio 11 fila

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  std::mutex x;
  std::condition_variable cv;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    std::lock_guard<std::mutex> lock(x);
    q.push_back(value);
    cv.
notify_one();
  }
  T pop()
  {
    std::unique_lock<std::mutex> lock(x);
    cv.wait(lock, [&]()
    {
      return !q.empty();
    });
    T v = q.front();
    q.pop_front();
    return v;
  }
};

A implementação da biblioteca C++ padrão, sem dúvida, irá melhorar no tempo, como será o apoio da biblioteca de simultaneidade em geral. O Comitê C++ tomou algumas medidas pequenas e conservadoras para suporte de simultaneidade que deve ser reconhecido, mas o trabalho não é feito ainda. Conforme discutido na Minhas última três colunas, o futuro da simultaneidade de C++ está ainda em questão. Por agora, a combinação de alguns primitivos de sincronização excelente no Windows e o compilador de C++ da arte faz para um toolkit convincente para produzir programas de simultaneidade de leves e escaláveis.

Kenny Kerr é um profissional de fabricação de software apaixonado pelo desenvolvimento nativo para Windows. Entre em contato com Kenny em kennykerr.ca.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Mohamed Amin Ibrahim