[Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

13. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

Uilian Ries
uilianries

(usa Linux Mint)

Enviado em 04/12/2015 - 22:56h

XProtoman escreveu:

paulo1205 escreveu:

Eu não tenho como testar agora, mas eu lembro de uma mensagem neste fórum dizendo que o valgrind tinha problemas com threads.

EDIT: Pesquisando, o problema não era do valgrind com threads, mas alguma coisa com o valgrind de modo geral, que eu não consegui reproduzir na época em nenhuma circunstância. Por sinal, tinha sido um tópico aberto por você.

Boa noite Paulo, tudo bom? Lembro de você naquele post. :D

O código não tem threads por enquanto e ele rodou fora do valgrind(aquelas linhas eram fora dele), o mutex é alocado dinamicamente, decidi deixar o código mais enxuto e usar std::string em vez de std::mutex(para descartar ser um problema do mutex), removi as operações e ele apresenta o mesmo erro a não ser que você faça sobrecarga do operador de atribuição, segue:


#include <iostream>
#include <string>

//#define RESOLVER_PROBLEMA 1

class Teste
{
private:
std::string *string;
public:
Teste();
Teste(const Teste &teste);
~Teste();
#ifdef RESOLVER_PROBLEMA
void operator = (const Teste &teste);
#endif
};

Teste::Teste()
{
this->string = new std::string();
}

Teste::Teste(const Teste &teste): Teste()
{
}

Teste::~Teste()
{
delete this->string;
this->string = NULL;
}

#ifdef RESOLVER_PROBLEMA
void Teste::operator = (const Teste &teste)
{
}
#endif

int main()
{
Teste teste;
for ( size_t posicao = 0, posicao_maximo = 1000000;
posicao_maximo > posicao;
posicao++ )
{
teste = Teste();
}
return 0;
}


Parece ser uma característica da linguagem(C++), estou consertando minhas classes para escapar desse problema. Para solucionar é só descomentar a linha 4.


O Paulo já matou a charada, olhe novamente.

C++ já prevê um operador de atribuição, para uma classe definida. O operador=(cost Test&) provido pelo compilador, copia cada membro do objeto passado por referencia, para a instância this. No sua classe Test, é copiado um ponteiro, logo, string de this recebe string da referência passada por parâmetro, mas não desaloca ela, antes de atribuir.

Sabendo isso, no seu loop é instanciado um objeto temporário a cada o iteração, que passa o ponteiro de string para o objeto Test declarado no incio de main. Após a atribuição, a string original irá apontar para o mesmo local que a string do objeto temporário, e ainda, o ponteiro original está perdido. O segundo problema está nos objetos temporários, cada um chama seu destrutor no final de cada iteração, o destrutor chama delete sobre a string, porém essa mesma string foi atribuída ao objeto anterior, logo, seu string do Test incial, aponta para um ponteiro inválido. No final da main(), o destrutor do Test será invocado, porém, por ter atribuído string de um objeto temporário, que já foi deletado, o destrutor acaba chamado novamente delete sobre mesmo ponteiro. Em ambos os casos, acessar ponteiro inválido e double free, possuem comportamento indefinido, segundo o padrão C++, no teu caso segment fault.

Outra coisa que notei, estas sobrecarregado o construtor por cópia, mas está utilizando o artifício de delegação de construtor, nesse caso, chama o construtor padrão, antes de chamar o construtor de cópia.





  


14. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 04/12/2015 - 23:26h

uilianries escreveu:
...
O Paulo já matou a charada, olhe novamente.

C++ já prevê um operador de atribuição, para uma classe definida. O operador=(cost Test&) provido pelo compilador, copia cada membro do objeto passado por referencia, para a instância this. No sua classe Test, é copiado um ponteiro, logo, string de this recebe string da referência passada por parâmetro, mas não desaloca ela, antes de atribuir.

Sabendo isso, no seu loop é instanciado um objeto temporário a cada o iteração, que passa o ponteiro de string para o objeto Test declarado no incio de main. Após a atribuição, a string original irá apontar para o mesmo local que a string do objeto temporário, e ainda, o ponteiro original está perdido. O segundo problema está nos objetos temporários, cada um chama seu destrutor no final de cada iteração, o destrutor chama delete sobre a string, porém essa mesma string foi atribuída ao objeto anterior, logo, seu string do Test incial, aponta para um ponteiro inválido. No final da main(), o destrutor do Test será invocado, porém, por ter atribuído string de um objeto temporário, que já foi deletado, o destrutor acaba chamado novamente delete sobre mesmo ponteiro. Em ambos os casos, acessar ponteiro inválido e double free, possuem comportamento indefinido, segundo o padrão C++, no teu caso segment fault.

Outra coisa que notei, estas sobrecarregado o construtor por cópia, mas está utilizando o artifício de delegação de construtor, nesse caso, chama o construtor padrão, antes de chamar o construtor de cópia.



Opa, obrigado uilianries e paulo, agora entendi.

De brincadeira e por estar sendo obrigado a ter operador=(const Teste &) e construtor Teste(const Teste &) me dei conta que agora deixei de fazer uso simples de mutex e já passo a ter risco de deadlocks, um exemplo que eu fiz:


#include <iostream>
#include <thread>
#include <mutex>

class Teste
{
private:
std::mutex *mutex;
int algo;
public:
Teste();
Teste(const Teste &teste);
~Teste();
Teste &operator = (const Teste &teste);
};

Teste::Teste()
{
this->mutex = new std::mutex();
this->algo = 0;
}

Teste::Teste(const Teste &teste): Teste()
{
teste.mutex->lock();
this->algo = teste.algo;
teste.mutex->unlock();
}

Teste::~Teste()
{
delete this->mutex;
this->mutex = NULL;
}

Teste &Teste::operator = (const Teste &teste)
{
// Como estou atribuindo preciso garantir
// que this e teste estejam bloqueados
// para não gerar inconsistências no valor
// de "algo" que é passado.
this->mutex->lock();
teste.mutex->lock();
this->algo = teste.algo;
teste.mutex->unlock();
this->mutex->unlock();
return *this;
}

void operacaoA(Teste *a, Teste *b)
{
*a = *b;
}

void operacaoB(Teste *a, Teste *b)
{
*b = *a;
}

int main()
{
Teste a, b;
size_t posicao_final = 512;
std::thread thread[posicao_final];
for ( size_t posicao = 0;
posicao_final > posicao;
posicao++ )
{
if ( posicao % 2 == 0 )
{
thread[posicao] = std::thread(operacaoA, &a, &b);
}
else
{
thread[posicao] = std::thread(operacaoB, &a, &b);
}
}
for ( size_t posicao = 0;
posicao_final > posicao;
posicao++ )
{
std::cout << "Thread " << posicao << " join..." << std::endl;
thread[posicao].join();
}
return 0;
}

Comando para compilar:
$ g++ -std=c++14 -pthread teste_generico.cpp -o teste_generico.run

Nem sempre vai ocorrer deadlocks(ao você testar), mas é só repetir que ocorrerá em algum momento.

Fiz só esse exemplo porque é a primeira situação que vi que deixei de fazer uso simples do mutex(ocasionado pela sobrecarga do operador = ).


15. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 05/12/2015 - 00:17h

Nessa brincadeira de mutex, deadlock e operador =, fiz uma solução muito crua, mas que funciona para evitar deadlock:

void duploLock(std::mutex *x, std::mutex *y)
{
while ( 1 )
{
if ( x->try_lock() )
{
if ( y->try_lock() )
{
return;
}
x->unlock();
}
}
}


Exemplo de uso no código:

#include <iostream>
#include <thread>
#include <mutex>

void duploLock(std::mutex *x, std::mutex *y)
{
while ( 1 )
{
if ( x->try_lock() )
{
if ( y->try_lock() )
{
return;
}
x->unlock();
}
}
}

class Teste
{
private:
std::mutex *mutex;
int algo;
public:
Teste();
Teste(const Teste &teste);
~Teste();
Teste &operator = (const Teste &teste);
};

Teste::Teste()
{
this->mutex = new std::mutex();
this->algo = 0;
}

Teste::Teste(const Teste &teste): Teste()
{
teste.mutex->lock();
this->algo = teste.algo;
teste.mutex->unlock();
}

Teste::~Teste()
{
delete this->mutex;
this->mutex = NULL;
}

Teste &Teste::operator = (const Teste &teste)
{
//this->mutex->lock();
//teste.mutex->lock();
duploLock(this->mutex, teste.mutex);
this->algo = teste.algo;
teste.mutex->unlock();
this->mutex->unlock();
return *this;
}

void operacaoA(Teste *a, Teste *b)
{
*a = *b;
}

void operacaoB(Teste *a, Teste *b)
{
*b = *a;
}

int main()
{
Teste a, b;
size_t posicao_final = 4096;
std::thread thread[posicao_final];
for ( size_t posicao = 0;
posicao_final > posicao;
posicao++ )
{
if ( posicao % 2 == 0 )
{
thread[posicao] = std::thread(operacaoA, &a, &b);
}
else
{
thread[posicao] = std::thread(operacaoB, &a, &b);
}
}
for ( size_t posicao = 0;
posicao_final > posicao;
posicao++ )
{
std::cout << "Thread " << posicao << " join..." << std::endl;
thread[posicao].join();
}
return 0;
}



Para compilar:
g++ -std=c++14 -pthread teste_generico.cpp -o teste_generico.run


16. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 06/12/2015 - 02:56h

Boa noite a todos,

Lendo um fórum sobre sockets e sincronização alguém falou no quão prejudicial pode ser um close(), na hora me dei conta que protegi vários métodos de uma classe, porém não pensei no destrutor.

Fazer lock no mutex na hora destrutor seria uma boa atitude?

Acho paranoico e talvez sem efeito aparente e sem sentindo, mas sei lá pode ter algum sentindo: tudo está protegido, menos o destrutor.

Obrigado.



17. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

Paulo
paulo1205

(usa Ubuntu)

Enviado em 06/12/2015 - 04:26h

XProtoman escreveu:

Lendo um fórum sobre sockets e sincronização alguém falou no quão prejudicial pode ser um close(), na hora me dei conta que protegi vários métodos de uma classe, porém não pensei no destrutor.

Fazer lock no mutex na hora destrutor seria uma boa atitude?

Acho paranoico e talvez sem efeito aparente e sem sentindo, mas sei lá pode ter algum sentindo: tudo está protegido, menos o destrutor.


É difícil pontificar sobre o que é definitivamente permitido ou proibido fazer, pois quase sempre se consegue pensar num caso em que uma coisa não-usual pode ser necessária.

No entanto, se você considerar que um destrutor serve para devolver recursos adquiridos e que não são mais necessários, que estavam vinculados a um objeto que também está deixando de existir, não me parece fazer muito sentido que você tente obter um recurso novo -- o lock na mutex -- justamente nesse momento.


18. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 06/12/2015 - 13:09h

paulo1205 escreveu:
...

É difícil pontificar sobre o que é definitivamente permitido ou proibido fazer, pois quase sempre se consegue pensar num caso em que uma coisa não-usual pode ser necessária.

No entanto, se você considerar que um destrutor serve para devolver recursos adquiridos e que não são mais necessários, que estavam vinculados a um objeto que também está deixando de existir, não me parece fazer muito sentido que você tente obter um recurso novo -- o lock na mutex -- justamente nesse momento.

Entendi, obrigado mais uma vez Paulo.

Ia perguntar a vocês o que acham de por exemplo classes ligadas a gráficos(animações), audio e tempo(que também é utilizada pelos anteriores) usarem mutex e serem thread safe já que elas são críticas em desempenho. Decidi arriscar e estou mutex para praticamente tudo, inspirado num comentário que alguém deu num fórum sobre o Vulkan(sucessor do OpenGL) que trabalhará bem com threads.

Para quem for mexer com mutex e tiver protegendo suas classes olhe com atenção sobrecarga do operador =(alguém pode fazer um objeto = objeto) e operadores como +(alguém também pode fazer objeto+objeto). Escapando dessas ciladas.


19. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

Paulo
paulo1205

(usa Ubuntu)

Enviado em 06/12/2015 - 16:49h

XProtoman escreveu:

Ia perguntar a vocês o que acham de por exemplo classes ligadas a gráficos(animações), audio e tempo(que também é utilizada pelos anteriores) usarem mutex e serem thread safe já que elas são críticas em desempenho. Decidi arriscar e estou mutex para praticamente tudo, inspirado num comentário que alguém deu num fórum sobre o Vulkan(sucessor do OpenGL) que trabalhará bem com threads.


Eu tenho reservas sobre esses usos de “tudo” ou “quase tudo”, porque fica muito difícil falar sobre o que convém ou não convém fazer.

Tenha em mente que mutexes servem para sincronizar acessos a objetos ou a realização de operações que não possam ocorrer simultaneamente. O termo “sincronizar”, aliás talvez nem seja o mais feliz -- ao menos em Português -- porque o que se quer é justamente garantir que as operações sejam realizadas uma de cada vez, em vez de ao mesmo tempo. Algumas operações dispensam sincronização: por exemplo, várias tarefas distintas que apenas consultem, sem modificar, um mesmo objeto podem perfeitamente ocorrer simultaneamente.

E, claro, existe uma muito simples de obter sequenciamento de tarefas, que é programá-las em sequência dentro da mesma thread de execução, em lugar de separar em múltiplas threads e ficar bloqueando ora uma, ora outras.

Para quem for mexer com mutex e tiver protegendo suas classes olhe com atenção sobrecarga do operador =(alguém pode fazer um objeto = objeto) e operadores como +(alguém também pode fazer objeto+objeto). Escapando dessas ciladas.


No seu caso, o problema não era o fato de ter mutexes, mas de ter ponteiros simples como membros da classe. Quando você coloca um ponteiro como membro de uma classe, o ponteiro é que é a variável que compõe o objeto, não o dado para o qual ele aponta. Por isso, se você deixa que o compilador ofereça um operator=() padrão, ele vai copiar o endereço, em vez de copiar o dado.

E já que falamos em SomeType::operator=(), cuidado com o tipo de retorno. Se você quiser um comportamento semelhante ao operador de atribuição nativo, o tipo de retorno dessa função deveria ser SomeType &, e você provavelmente deveria encerrar a função com o comando return *this;.


20. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 07/12/2015 - 01:59h

Boa noite a todos, obrigado mais uma vez Paulo pelas respostas.

A explicação que você deu sobre thread e sincronização foram boas.

Mutex é como um brinquedo novo para mim, querendo usar e testar muito por isso até o exagero em atitudes, muita vontade de encontrar as fronteiras. Threads comuns(sem concorrência) resolviam alguns problemas bons, porém um dos pratos principais são sockets para servidores e indiretamente para clientes e você viu que naquele post estava andando em círculos e talvez o que preciso seja por enquanto seja mutex por isso estou tentando testar em várias partes, ver a reação, o comportamento, acho que foi uma das primeiras oportunidades reais de fazer um construtor(const construtor &) e sobrecarga de operadores como de atribuição(para atender os problemas dos posts anteriores).

Sobre o SomeType::operator=(), pela inexperiência para mim retornava void, mas pelo o que você falou fiquei atento e realizei testes simples(com um inteiro sendo atribuído e a operação sendo impressa com std::cout) entendi que devem retornar o próprio *this como você disse.

O problema do operador= e operador+(e seus irmãos; combinação deles também), não é aquele problema que tinha falado, é um problema adicional, me dei conta que eu estava fazendo isso(usei os locks para facilitar o entendimento em vez daquela função duploLock):

Teste &Teste::operator = (const Teste &teste)
{
this->mutex->lock();
teste.mutex->lock();
this->algo = teste.algo;
teste.mutex->unlock();
this->mutex->unlock();
return *this;
}


Porém se o cara fizer teste = teste: deadlock claro(já que são a mesma coisa). Aí por isso criar esse if para proteger.

Teste &Teste::operator = (const Teste &teste)
{
if ( this == &teste )
{
return *this;
}
this->mutex->lock();
teste.mutex->lock();
this->algo = teste.algo;
teste.mutex->unlock();
this->mutex->unlock();
return *this;
}


Operador de adição você tem quase a mesma natureza de problema caso ocorra um teste + teste, onde tem que também tomar cuidado na hora dos dois locks(para o this e para teste passado como argumento), para resolver basta criar IF para tratar a situação em que this é igual a teste(argumento) onde só é preciso um lock e no final um unlock; e o ELSE para tratar a situação que a maioria espera(teste1 + teste2 por exemplo), this sendo diferente de teste(argumento), então você precisa dar um lock em this e em teste e unlock em ambos no final. Operador de atribuição você nem precisou dar nenhum lock, porém nesse de adição você precisa se preocupar com lock unico(this == &teste) ou duplo (this != &teste).

Eu escrevi para tomar cuidado com os dois porque não sei se essa explicação que dei ficou boa dos problemas e soluções.

Sobre "Por isso, se você deixa que o compilador ofereça um operator=() padrão, ele vai copiar o endereço, em vez de copiar o dado.", foi inexperiência também. Maior parte das vezes os atributos dentro das minhas classes eram estáticos(acho que é esse o termo), existiam poucos ponteiros em outras classes e para esconder ainda mais o erro de mim nunca houve necessidade de fazer atribuição entre objetos de mesma classe, então o problema nunca chegou direito até a minha tela, até que surgir o ponteiro de std::mutex em uma classe que fazia uso de sobrecarga de operadores(adição, subtração) e como você mesmo falou eu sem saber estava deixando a cargo do "operator=() padrão" para resolver meus problemas.


21. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 18/12/2015 - 06:06h

Boa noite a todos,

Me deparei com mais um cenário de deadlock(maior ainda) e uma solução que parece boa:

Existe uma classe gráfica chamada Vetores(responsável por armazenar vetores, ela armazena tudo em double por exemplo) e outra chamada Vetor(responsável por armazenar os valores de um vetor). Ambas estão trabalhando com mutex.

Se eu fosse adicionar um Vetor em Vetores deveria dar lock em ambos, até ai tudo bem porque tem aquele lock_duplo, porém chegou um método novo em Vetores que recebia 8 objetos da classe Vetor, logo eu teria que dar 1 lock em Vetores e lock nos 8 Vetor, totalizando: 9 locks de uma vez.

Da para imaginar que dar lock em 9 objetos de uma vez é muito diferente de dar em apenas 2, tem muito mais areia e a chance de deadlock é enorme. Se você passar os valores por referência você ainda teria que verificar se existem objetos iguais(nesse caso apenas um lock), foi ai que depois de imaginar vários cenários e várias decisões encontrei a forma mais fácil:

Passar os 8 Vetor como argumento por valor em vez de passar por referência, assim eu evitaria 8 locks, evitaria outras coisas que comentei, enfim é a melhor solução, precisando apenas dar um lock em Vetores.

Para esse caso especial envolvendo Vetores não estou usando o lock duplo(até porque não é mais necessário).

Depois vou estudar se em outras classes é melhor passar por valor também e talvez aposentar o lock duplo, mas ele em objetos grandes, com grande quantidade de dados, um lock duplo é mais rápido e menos custoso.


22. Re: [Threads Concorrentes] Uso simples de MUTEX é seguro e não gera DEADLOCKs? [RESOLVIDO]

M.
XProtoman

(usa Fedora)

Enviado em 18/12/2015 - 06:42h

Continuação do post anterior.

Surgiu agora um outro método que pega(GET) 8 Vetor de Vetores e sou obrigado a passa-los por referência, nesse caso extraordinário vou ter que ir dando lock em cada Vetor, fazendo a operação para gravar o dado nele e depois um unlock e seguindo de um em um, dessa forma não tem deadlock e resolve o problema de maneira simples também.



01 02



Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts