Selecione Mutex ou Dummy Mutex em tempo de execução
-
24-09-2019 - |
Pergunta
Eu tenho uma classe que é compartilhada entre vários projetos, alguns usos são de tiro único e alguns são multi-thread. Os usuários de tiro único não querem a sobrecarga do bloqueio mutex, e os usuários com vários thread não querem fazer seu próprio bloqueio e querem ser capazes de executar opcionalmente no "modo de thread único". Então, eu gostaria de ser capaz de selecionar entre mutexes reais e "dummy" em tempo de execução.
Idealmente, eu teria um shared_ptr<something>
e atribua um objeto mutex real ou falso. Eu "travaria" isso sem levar em consideração o que há nele.
unique_lock<something> guard(*mutex);
... critical section ...
Agora existe um signals2::dummy_mutex
Mas não compartilha uma classe base comum com boost::mutex
.
Então, o que é uma maneira elegante de selecionar entre um mutex real e um mutex dummy (aquele em sinais2 ou algo mais) sem tornar o código de bloqueio/guarda mais complicado do que o exemplo acima?
E, antes de apontar as alternativas:
- Eu poderia selecionar uma implementação no momento da compilação, mas as macros do pré -processador são feias e a manutenção das configurações de projeto é doloroso para nós.
- Os usuários da classe em um ambiente multithread não desejam assumir a responsabilidade de bloquear o uso da classe, em vez de fazer com que a classe faça seu próprio travamento internamente.
- Existem muitas APIs e usos existentes envolvidos para um "invólucro seguro para threads" ser uma solução prática.
Solução
Que tal algo como isso? É não testado, mas deve estar perto de OK. Você pode considerar fazer com que a classe de modelo mantenha um valor em vez de um ponteiro se seus mutexes suportarem os tipos certos de construções. Caso contrário, você poderá especializar a classe MyMutex para obter comportamento de valor.
Também não está tomando cuidado com a cópia ou a destruição. Deixo isso como um exercício para o leitor;) (Shared_ptr ou armazenar um valor em vez de um ponteiro deve consertar isso)
Ah, e o código seria melhor usando RAII em vez de bloqueio/desbloqueio explícito ... mas essa é uma pergunta diferente. Eu assumo que é isso que o único_lock do seu código faz?
struct IMutex
{
virtual ~IMutex(){}
virtual void lock()=0;
virtual bool try_lock()=0;
virtual void unlock()=0;
};
template<typename T>
class MyMutex : public IMutex
{
public:
MyMutex(T t) : t_(t) {}
void lock() { t_->lock(); }
bool try_lock() { return t_->try_lock(); }
void unlock() { t_->unlock(); }
protected:
T* t_;
};
IMutex * createMutex()
{
if( isMultithreaded() )
{
return new MyMutex<boost::mutex>( new boost::mutex );
}
else
{
return new MyMutex<signal2::dummy_mutex>( new signal2::dummy_mutex );
}
}
int main()
{
IMutex * mutex = createMutex();
...
{
unique_lock<IMutex> guard( *mutex );
...
}
}
Outras dicas
Desde as duas classes mutex signals2::dummy_mutex
e boost::mutex
Não compartilhe uma classe base comum, você pode usar algo como "Polimorfismo externo"Para permitir que eles sejam tratados polimorficamente. Você os usaria como travando estratégias Para uma interface mutex/bloqueio comum. Isso permite que você evite usar "if
"Declarações na implementação do bloqueio.
NOTA: Isso é basicamente o que a solução proposta de Michael implementa. Eu sugiro ir com a resposta dele.
Você já ouviu falar sobre Policy-based Design
?
Você pode definir um Lock Policy
interface e o usuário pode escolher qual política deseja. Para facilitar o uso, a política "padrão" é precisas usando uma variável de tempo de compilação.
#ifndef PROJECT_DEFAULT_LOCK_POLICY
#define PROJECT_DEFAULT_LOCK_POLICY TrueLock
#endif
template <class LP = PROJECT_DEFAULT_LOCK_POLICY>
class MyClass {};
Dessa forma, seus usuários podem escolher suas políticas com um switch simples de tempo de compilação e podem substituí-lo uma instância por vez;)
Isso não é suficiente?
class SomeClass
{
public:
SomeClass(void);
~SomeClass(void);
void Work(bool isMultiThreaded = false)
{
if(isMultiThreaded)
{
lock // mutex lock ...
{
DoSomething
}
}
else
{
DoSomething();
}
}
};
Em geral, um mutex só é necessário se o recurso for compartilhado entre vários processos. Se uma instância do objeto for única para um processo (possivelmente multithread), uma seção crítica geralmente será mais apropriada.
No Windows, a implementação de um único thread de uma seção crítica é um fictício. Não tenho certeza de qual plataforma você está usando.
Apenas para sua informação, aqui está a implementação com quem acabei.
Eu acabei com a classe base abstrata, mesclando-a com a implementação de "fictício". Observe também o shared_ptr
-classe derivada com um operador de conversão implícito. Um pouco complicado demais, eu acho, mas isso me permite usar shared_ptr<IMutex>
objetos onde eu usei anteriormente boost::mutex
objetos com zero alterações.
arquivo de cabeçalho:
class Foo {
...
private:
struct IMutex {
virtual ~IMutex() { }
virtual void lock() { }
virtual bool try_lock() { return true; }
virtual void unlock() { }
};
template <typename T> struct MutexProxy;
struct MutexPtr : public boost::shared_ptr<IMutex> {
operator IMutex&() { return **this; }
};
typedef boost::unique_lock<IMutex> MutexGuard;
mutable MutexPtr mutex;
};
Arquivo de implementação:
template <typename T>
struct Foo::MutexProxy : public IMutex {
virtual void lock() { mutex.lock(); }
virtual bool try_lock() { return mutex.try_lock(); }
virtual void unlock() { mutex.unlock(); }
private:
T mutex;
};
Foo::Foo(...) {
mutex.reset(single_thread ? new IMutex : new MutexProxy<boost::mutex>);
}
Foo::Method() {
MutexGuard guard(mutex);
}
Esta é a minha solução:
std::unique_lock<std::mutex> lock = dummy ?
std::unique_lock<std::mutex>(mutex, std::defer_lock) :
std::unique_lock<std::mutex>(mutex);