Exemplo para boost shared_mutex (múltiplas leituras / uma gravação)?

116

Tenho um aplicativo multithread que precisa ler alguns dados com frequência e, ocasionalmente, esses dados são atualizados. No momento, um mutex mantém o acesso a esses dados seguro, mas é caro porque eu gostaria que vários threads pudessem ler simultaneamente, e apenas bloqueá-los quando uma atualização for necessária (o thread de atualização pode esperar que os outros threads terminem) .

Acho que é isso que boost::shared_mutexse deve fazer, mas não tenho certeza de como usá-lo e não encontrei um exemplo claro.

Alguém tem um exemplo simples que eu possa usar para começar?

kevin42
fonte
O exemplo de 1800 INFORMAÇÕES está correto. Consulte também este artigo: O que há de novo em Boost Threads .
Assaf Lavie
possível duplicata de Locks Reader / Writer em C ++
cHao

Respostas:

102

Parece que você faria algo assim:

boost::shared_mutex _access;
void reader()
{
  // get shared access
  boost::shared_lock<boost::shared_mutex> lock(_access);

  // now we have shared access
}

void writer()
{
  // get upgradable access
  boost::upgrade_lock<boost::shared_mutex> lock(_access);

  // get exclusive access
  boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
  // now we have exclusive access
}
1800 INFORMAÇÕES
fonte
7
Esta é a primeira vez que uso boost e sou um novato em C ++, então talvez haja algo que esteja faltando - mas em meu próprio código, tive que especificar o tipo, assim: boost :: shared_lock <shared_mutex> lock (_Acesso);
Ken Smith
2
Estou tentando usar isso sozinho, mas estou recebendo um erro. argumentos de template ausentes antes de 'lock'. Alguma ideia?
Matt
2
@shaz Esses têm escopo, mas você pode liberá-los antecipadamente com .unlock () se precisar.
mmocny
4
Eu adicionei os argumentos de modelo ausentes.
1
@raaj você pode obter o upgrade_lock, mas atualizar para um bloqueio exclusivo será bloqueado até que o shared_lock seja lançado
1800 INFORMAÇÕES
166

1800 INFORMAÇÕES está mais ou menos correto, mas há alguns problemas que eu gostaria de corrigir.

boost::shared_mutex _access;
void reader()
{
  boost::shared_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access
}

void conditional_writer()
{
  boost::upgrade_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access

  if (something) {
    boost::upgrade_to_unique_lock< boost::shared_mutex > uniqueLock(lock);
    // do work here, but now you have exclusive access
  }

  // do more work here, without anyone having exclusive access
}

void unconditional_writer()
{
  boost::unique_lock< boost::shared_mutex > lock(_access);
  // do work here, with exclusive access
}

Observe também que, ao contrário de um shared_lock, apenas uma única thread pode adquirir um upgrade_lock por vez, mesmo quando não está atualizado (o que achei estranho quando o encontrei). Portanto, se todos os seus leitores são escritores condicionais, você precisa encontrar outra solução.

mmocny
fonte
1
Apenas para comentar "outra solução". Quando todos os meus leitores eram gravadores condicionais, o que eu fiz foi fazer com que eles sempre adquirissem um shared_lock, e quando eu precisasse atualizar para privilégios de gravação, eu iria .unlock () o bloqueio do leitor e adquirir um novo unique_lock. Isso complicará a lógica do seu aplicativo e agora existe uma janela de oportunidade para outros escritores alterarem o estado de quando você leu pela primeira vez.
mmocny
8
A linha não deve boost::unique_lock< boost::shared_mutex > lock(lock);ler boost::unique_lock< boost::shared_mutex > lock( _access ); ?
SteveWilkinson,
4
Essa última advertência é muito estranha. Se apenas um thread pode conter um upgrade_lock por vez, qual é a diferença entre um upgrade_lock e um unique_lock?
Ken Smith
2
@Ken Eu não fui muito claro, mas o benefício do upgrade_lock é que ele não bloqueia se houver alguns shared_locks adquiridos (pelo menos não até que você atualize para exclusivo). No entanto, o segundo thread para tentar adquirir um upgrade_lock será bloqueado, mesmo que o primeiro não tenha sido atualizado para exclusivo, o que eu não esperava.
mmocny
6
Este é um problema conhecido de aumento. Parece estar resolvido no boost 1.50 beta: svn.boost.org/trac/boost/ticket/5516
Ofek Shilon
47

Desde C ++ 17 (VS2015), você pode usar o padrão para bloqueios de leitura e gravação:

#include <shared_mutex>

typedef std::shared_mutex Lock;
typedef std::unique_lock< Lock > WriteLock;
typedef std::shared_lock< Lock > ReadLock;

Lock myLock;


void ReadFunction()
{
    ReadLock r_lock(myLock);
    //Do reader stuff
}

void WriteFunction()
{
     WriteLock w_lock(myLock);
     //Do writer stuff
}

Para versões anteriores, você pode usar boost com a mesma sintaxe:

#include <boost/thread/locks.hpp>
#include <boost/thread/shared_mutex.hpp>

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock >  WriteLock;
typedef boost::shared_lock< Lock >  ReadLock;
Yochai Timmer
fonte
5
Eu também diria typedef boost::unique_lock< Lock > WriteLock; typedef boost::shared_lock< Lock > ReadLock;.
vinhas
6
Não é necessário incluir o thread.hpp inteiro. Se você só precisa dos bloqueios, inclua os bloqueios. Não é uma implementação interna. Mantenha as inclusões ao mínimo.
Yochai Timmer de
5
Definitivamente a implementação mais simples, mas acho confuso referir-se a mutexes e bloqueios como bloqueios. Um mutex é um mutex, um bloqueio é algo que o mantém em um estado bloqueado.
Tim MB
17

Apenas para adicionar mais algumas informações empíricas, estive investigando toda a questão de bloqueios atualizáveis ​​e Exemplo para boost shared_mutex (várias leituras / uma gravação)? é uma boa resposta adicionando a informação importante de que apenas um thread pode ter um upgrade_lock mesmo se não for atualizado, o que é importante porque significa que você não pode atualizar de um bloqueio compartilhado para um bloqueio exclusivo sem liberar o bloqueio compartilhado primeiro. (Isso foi discutido em outro lugar, mas o tópico mais interessante está aqui http://thread.gmane.org/gmane.comp.lib.boost.devel/214394 )

No entanto, encontrei uma diferença importante (não documentada) entre um thread aguardando uma atualização para um bloqueio (ou seja, precisa esperar que todos os leitores liberem o bloqueio compartilhado) e um bloqueio de gravador aguardando a mesma coisa (ou seja, um unique_lock).

  1. O thread que está esperando por um unique_lock no shared_mutex bloqueia quaisquer novos leitores que entrem, eles têm que esperar pela solicitação dos escritores. Isso garante que os leitores não deixem os escritores famintos (no entanto, acredito que os escritores podem matar os leitores de fome).

  2. O encadeamento que está aguardando a atualização de um upgradeable_lock permite que outros encadeamentos obtenham um bloqueio compartilhado, portanto, este encadeamento pode ser interrompido se os leitores forem muito frequentes.

Esta é uma questão importante a se considerar e provavelmente deve ser documentada.

Jim Morris
fonte
3
Os Terekhov algorithmgarante que em 1., o escritor não pode morrer de fome os leitores. Veja isso . Mas 2.é verdade. Um upgrade_lock não garante justiça. Veja isso .
JonasVautherin de
2

Use um semáforo com uma contagem igual ao número de leitores. Deixe cada leitor contar uma vez no semáforo para ler, de forma que todos possam ler ao mesmo tempo. Em seguida, deixe o escritor fazer TODAS as contagens do semáforo antes de escrever. Isso faz com que o gravador espere que todas as leituras terminem e bloqueie as leituras durante a gravação.

R Virzi
fonte
(1) Como você faz um escritor diminuir a contagem em uma quantidade arbitrária atomicamente ? (2) Se o escritor de alguma forma diminui a contagem para zero, como ele espera que os leitores já em execução terminem antes de escrever?
Ofek Shilon
Má ideia: se dois gravadores tentarem acessar simultaneamente, você pode ter um deadlock.
Caduchon
2

Ótima resposta de Jim Morris, descobri isso e demorei um pouco para descobrir. Aqui está um código simples que mostra que, após enviar uma "solicitação" para um impulso unique_lock (versão 1.54), bloqueia todas as solicitações shared_lock. Isso é muito interessante, pois me parece que escolher entre unique_lock e upgradeeable_lock permite se queremos prioridade de gravação ou nenhuma prioridade.

Também (1) na postagem de Jim Morris parece contradizer isso: Boost shared_lock. Leia preferido?

#include <iostream>
#include <boost/thread.hpp>

using namespace std;

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock > UniqueLock;
typedef boost::shared_lock< Lock > SharedLock;

Lock tempLock;

void main2() {
    cout << "10" << endl;
    UniqueLock lock2(tempLock); // (2) queue for a unique lock
    cout << "11" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(1));
    lock2.unlock();
}

void main() {
    cout << "1" << endl;
    SharedLock lock1(tempLock); // (1) aquire a shared lock
    cout << "2" << endl;
    boost::thread tempThread(main2);
    cout << "3" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(3));
    cout << "4" << endl;
    SharedLock lock3(tempLock); // (3) try getting antoher shared lock, deadlock here
    cout << "5" << endl;
    lock1.unlock();
    lock3.unlock();
}
dale1209
fonte
Na verdade, estou tendo problemas para descobrir por que o código acima bloqueia enquanto o código em [ stackoverflow.com/questions/12082405/… funciona.
dale1209
1
Na verdade, ele bloqueia em (2), não em (3), porque (2) está esperando que (1) libere seu bloqueio. Lembre-se: para obter um bloqueio exclusivo, você precisa esperar que todos os bloqueios compartilhados existentes terminem.
JonasVautherin de
@JonesV, mesmo que (2) espere que todos os bloqueios compartilhados terminem, não seria um deadlock porque é um thread diferente daquele que adquiriu (1), se a linha (3) não existisse, o programa termine sem bloqueios.
SagiLow