Exemplo / tutorial da Mutex? [fechadas]

176

Eu sou novo no multithreading e estava tentando entender como os mutexes funcionam. Pesquisei bastante no Google, mas ainda havia algumas dúvidas de como funciona, porque criei meu próprio programa no qual o bloqueio não funcionava.

Uma sintaxe absolutamente não intuitiva do mutex é pthread_mutex_lock( &mutex1 );, onde parece que o mutex está sendo bloqueado, quando o que realmente quero bloquear é outra variável. Essa sintaxe significa que o bloqueio de um mutex bloqueia uma região de código até que o mutex seja desbloqueado? Então, como os threads sabem que a região está bloqueada? [ UPDATE: Threads sabem que a região está bloqueada pelo Memory Fencing ]. E esse fenômeno não deveria ser chamado de seção crítica? [ ATUALIZAÇÃO: os objetos de seção crítica estão disponíveis apenas no Windows, onde os objetos são mais rápidos que os mutexes e são visíveis apenas ao encadeamento que o implementa. Caso contrário, a seção crítica se refere apenas à área do código protegida por um mutex ]

Em resumo, você poderia ajudar com o programa de exemplo mutex mais simples possível e com a explicação mais simples possível sobre a lógica de como ele funciona? Tenho certeza de que isso ajudará muitos outros novatos.

Nav
fonte
2
Continuando a enfatizar a necessidade de um tutorial simples (seja aumentar threads, tbb ou pthreads): Exemplos da confusão: 1. stackoverflow.com/questions/3528877/… 2. stackoverflow.com/questions/2979525/… 3. stackoverflow.com/questions/2095977/to-mutex-or-not-to-mutex 4. stackoverflow.com/questions/3931026/… 5. stackoverflow.com/questions/1525189/…
Nav
1
Não quero dizer isso ofensivamente, mas o que seu último comentário me sugere é que precisamos de menos analogias e uma melhor explicação técnica de como um mutex funciona e por que precisamos deles.
San Jacinto
@ San: Sem ofensas :) Meus comentários foram feitos apenas para sugerir que um novato poderia obter a explicação mais curta e clara de mutexes. Muitas analogias podem ficar confusas para o novato, portanto, analogias diferentes devem ser mantidas separadamente. Todo o motivo para eu postar as perguntas e respostas é porque, como novato, achei difícil ler longas explicações e exemplos de código. Eu não gostaria que mais ninguém passasse pela dor.
Nav
2
@Cory: Se essa resposta puder ser melhorada, ficarei feliz em aceitar suas sugestões. Estou feliz que muitas outras pessoas tenham achado útil. Se isso não ajudou, também existem respostas de outras pessoas que apontaram para outros tutoriais sobre mutex. Por que ser tão negativo?
Nav

Respostas:

278

Aqui está minha humilde tentativa de explicar o conceito para iniciantes em todo o mundo: (uma versão com código de cores no meu blog também)

Muitas pessoas correm para uma cabine telefônica solitária (não têm celular) para conversar com seus entes queridos. A primeira pessoa a pegar a maçaneta da porta é quem tem permissão para usar o telefone. Ele tem que continuar segurando a maçaneta da porta enquanto usar o telefone, caso contrário, outra pessoa vai agarrar a maçaneta, jogá-lo fora e conversar com a esposa :) Não existe um sistema de filas como tal. Quando a pessoa termina a ligação, sai do estande e sai da maçaneta da porta, a próxima pessoa a segurar a maçaneta da porta poderá usar o telefone.

Um segmento é: Cada pessoa
O mutex é: A maçaneta da porta
A fechadura é: A mão da pessoa
O recurso é: O telefone

Qualquer encadeamento que precise executar algumas linhas de código que não devem ser modificadas por outros encadeamentos ao mesmo tempo (usando o telefone para conversar com a esposa), primeiro precisa adquirir uma trava em um mutex (segurando a maçaneta da porta do estande ) Somente então um thread poderá executar essas linhas de código (fazendo a ligação).

Depois que o segmento executar esse código, ele deverá liberar o bloqueio no mutex para que outro segmento possa adquirir um bloqueio no mutex (outras pessoas poderão acessar a cabine telefônica).

[ O conceito de ter um mutex é um pouco absurdo ao considerar o acesso exclusivo do mundo real, mas no mundo da programação acho que não havia outra maneira de permitir que os outros threads 'vissem' que um segmento já estava executando algumas linhas de código. Existem conceitos de mutexes recursivos, etc., mas este exemplo foi feito apenas para mostrar o conceito básico. Espero que o exemplo lhe dê uma imagem clara do conceito. ]

Com o segmento C ++ 11:

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

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Compile e execute usando g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Em vez de usar explicitamente locke unlock, você pode usar colchetes como mostrado aqui , se estiver usando um bloqueio de escopo para a vantagem que ele oferece . Bloqueios com escopo definido têm uma ligeira sobrecarga de desempenho.

Nav
fonte
2
@ San: eu vou ser honesto; Sim, eu gosto do fato de você ter tentado ao máximo explicar os detalhes (com fluxo) para um novato completo. MAS, (por favor, não me entenda mal) a intenção deste post foi colocar o conceito em uma breve explicação (porque as outras respostas apontaram para tutoriais longos). Espero que você não se importe se eu solicitar que você copie toda a sua resposta e a publique como uma resposta separada. Para que eu possa reverter e editar minha resposta para apontar para sua resposta.
Nav
2
@ Tom Nesse caso, você não deveria estar acessando esse mutex. As operações nele devem ser encapsuladas, para proteger o que quer que esteja protegendo de tais bobagens. Se, ao usar a API exposta da biblioteca, for garantida a segurança da biblioteca, você poderá incluir um mutex distintamente diferente para proteger seus próprios itens compartilhados. Caso contrário, você está realmente adicionando uma nova maçaneta, como sugerido.
San Jacinto
2
Para estender meu argumento, o que você gostaria de fazer é adicionar outra sala maior ao redor do estande. O quarto também pode conter um vaso sanitário e chuveiro. Digamos que apenas 1 pessoa é permitida na sala de uma vez. Você deve projetar a sala para que esta sala tenha uma porta com uma alça que proteja a entrada na sala, como a cabine telefônica. Portanto, agora, mesmo que você tenha mutexes extras, é possível reutilizar a cabine telefônica em qualquer projeto. Outra opção seria expor os mecanismos de bloqueio para cada dispositivo na sala e gerenciar os bloqueios na classe da sala. De qualquer maneira, você não adicionaria novos bloqueios ao mesmo objeto.
San Jacinto
8
Seu exemplo de encadeamento em C ++ 11 está errado . Assim como o TBB, a pista está no nome do bloqueio no escopo .
Jonathan Wakely
3
Estou ciente de ambos, @ Jonathan. Você parecia ter perdido a frase que escrevi (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Quanto ao uso do bloqueio de escopo, cabe ao desenvolvedor, dependendo do tipo de aplicativo que eles estão criando. Esta resposta foi criada para abordar o entendimento básico do conceito de mutex e não entrar em todas as complexidades dele; portanto, seus comentários e links são bem-vindos, mas estão um pouco fora do escopo deste tutorial.
Nav
41

Embora um mutex possa ser usado para resolver outros problemas, a principal razão pela qual eles existem é fornecer exclusão mútua e, assim, resolver o que é conhecido como condição de corrida. Quando dois (ou mais) threads ou processos tentam acessar a mesma variável simultaneamente, temos potencial para uma condição de corrida. Considere o seguinte código

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

O interior desta função parece tão simples. É apenas uma afirmação. No entanto, um equivalente típico da linguagem pseudo-assembly pode ser:

load i from memory into a register
add 1 to i
store i back into memory

Como as instruções equivalentes em linguagem assembly são necessárias para executar a operação de incremento em i, dizemos que incrementar i é uma operação não atômica. Uma operação atômica é aquela que pode ser concluída no hardware com a garantia de não ser interrompida após o início da execução da instrução. Incrementar i consiste em uma cadeia de 3 instruções atômicas. Em um sistema simultâneo em que vários threads estão chamando a função, surgem problemas quando um thread lê ou grava na hora errada. Imagine que temos dois threads em execução simultaneoulsy e um chama a função imediatamente após o outro. Digamos também que eu inicializei com 0. Suponhamos também que temos muitos registros e que os dois threads estão usando registros completamente diferentes, portanto não haverá colisões. O tempo real desses eventos pode ser:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

O que aconteceu é que temos dois threads incrementando i simultaneamente, nossa função é chamada duas vezes, mas o resultado é inconsistente com esse fato. Parece que a função foi chamada apenas uma vez. Isso ocorre porque a atomicidade é "interrompida" no nível da máquina, o que significa que os threads podem se interromper ou trabalhar juntos nos momentos errados.

Precisamos de um mecanismo para resolver isso. Precisamos impor alguns pedidos para as instruções acima. Um mecanismo comum é bloquear todos os threads, exceto um. Pthread mutex usa esse mecanismo.

Qualquer encadeamento que precise executar algumas linhas de código que possam modificar valores compartilhados por outros encadeamentos ao mesmo tempo (usando o telefone para conversar com sua esposa), deve primeiro fazer com que você adquira um bloqueio em um mutex. Dessa maneira, qualquer encadeamento que exija acesso aos dados compartilhados deve passar pelo bloqueio mutex. Somente então um thread poderá executar o código. Esta seção do código é chamada de seção crítica.

Depois que o encadeamento tiver executado a seção crítica, ele deverá liberar o bloqueio no mutex para que outro encadeamento possa adquirir um bloqueio no mutex.

O conceito de ter um mutex parece um pouco estranho quando consideramos seres humanos buscando acesso exclusivo a objetos físicos reais, mas ao programar, devemos ser intencionais. Os threads e processos simultâneos não têm a educação social e cultural que possuímos, portanto, devemos forçá-los a compartilhar dados de maneira adequada.

Então, tecnicamente falando, como um mutex funciona? Não sofre das mesmas condições raciais que mencionamos anteriormente? O pthread_mutex_lock () não é um pouco mais complexo que um simples incremento de uma variável?

Tecnicamente falando, precisamos de suporte de hardware para nos ajudar. Os projetistas de hardware nos dão instruções de máquina que fazem mais de uma coisa, mas são garantidas como atômicas. Um exemplo clássico de tal instrução é o teste e configuração (TAS). Ao tentar adquirir um bloqueio em um recurso, podemos usar o TAS para verificar se um valor na memória é 0. Se for, esse seria o nosso sinal de que o recurso está em uso e não fazemos nada (ou com mais precisão , esperamos por algum mecanismo. Um mutex pthreads nos colocará em uma fila especial no sistema operacional e nos notificará quando o recurso estiver disponível. Os sistemas mais difíceis podem exigir que façamos um loop de rotação apertado, testando a condição repetidamente) . Se o valor na memória não for 0, o TAS definirá o local para algo diferente de 0 sem usar outras instruções. Isto' é como combinar duas instruções de montagem em 1 para nos fornecer atomicidade. Portanto, o teste e a alteração do valor (se a alteração for apropriada) não podem ser interrompidos após o início. Podemos construir mutexes sobre essa instrução.

Nota: algumas seções podem parecer semelhantes a uma resposta anterior. Eu aceitei o convite dele para editar, ele preferiu o jeito original, então eu estou mantendo o que eu tinha, que é infundido com um pouco de sua verborragia.

San Jacinto
fonte
1
Muito obrigado, San. Eu vinculei a sua resposta :) Na verdade, eu pretendia que você pegasse minha resposta + sua resposta e a publicasse como uma resposta separada, para manter o fluxo. Realmente não me importo se você reutilizar qualquer parte da minha resposta. De qualquer maneira, não estamos fazendo isso por nós mesmos.
Nav
13

O melhor tutorial de tópicos que eu conheço está aqui:

https://computing.llnl.gov/tutorials/pthreads/

Gosto que ele seja escrito sobre a API, e não sobre uma implementação específica, e fornece alguns bons exemplos simples para ajudar você a entender a sincronização.

R .. GitHub PARE DE AJUDAR O GELO
fonte
Concordo que é definitivamente um bom tutorial, mas há muitas informações em uma única página e os programas são longos. A pergunta que postei é a versão mutex do discurso "Eu tenho um sonho", em que os novatos encontravam uma maneira simples de aprender sobre mutexes e entender como a sintaxe não intuitiva funciona (essa é uma explicação que falta em todos os tutoriais) .
Nav
7

Encontrei este post recentemente e acho que ele precisa de uma solução atualizada para o mutex c ++ 11 da biblioteca padrão (ou seja, std :: mutex).

Eu colei alguns códigos abaixo (meus primeiros passos com um mutex - eu aprendi simultaneidade no win32 com HANDLE, SetEvent, WaitForMultipleObjects etc).

Como é minha primeira tentativa com std :: mutex e amigos, adoraria ver comentários, sugestões e melhorias!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}
comida de peixe
fonte
1
Super! Obrigado por publicar. Embora, como mencionei antes, meu objetivo fosse apenas explicar o conceito de mutex. Todos os outros tutoriais dificultaram muito os conceitos adicionais de variáveis ​​de condição e consumidor do produtor, etc., o que dificultou a compreensão do que estava acontecendo.
Nav
4

A função pthread_mutex_lock()quer adquire o mutex para o segmento de chamada ou bloqueia o segmento até que o mutex pode ser adquirido. O relacionado pthread_mutex_unlock()libera o mutex.

Pense no mutex como uma fila; todo encadeamento que tentar adquirir o mutex será colocado no final da fila. Quando um encadeamento libera o mutex, o próximo encadeamento na fila sai e agora está em execução.

Uma seção crítica refere-se a uma região de código onde o não-determinismo é possível. Geralmente, isso ocorre porque vários threads estão tentando acessar uma variável compartilhada. A seção crítica não é segura até que algum tipo de sincronização esteja em vigor. Um bloqueio mutex é uma forma de sincronização.

chrisaycock
fonte
1
É garantido que exatamente o próximo thread de tentativa entrará?
Arsen Mkrtchyan
1
@Arsen Não há garantia. É apenas uma analogia útil.
Chrisaycock
3

Você deve verificar a variável mutex antes de usar a área protegida pelo mutex. Portanto, seu pthread_mutex_lock () pode (dependendo da implementação) esperar até o mutex1 ser liberado ou retornar um valor indicando que o bloqueio não pode ser obtido se alguém já o tiver bloqueado.

O Mutex é realmente apenas um semáforo simplificado. Se você ler sobre eles e entendê-los, entenderá mutexes. Existem várias perguntas sobre mutexes e semáforos no SO. Diferença entre o semáforo binário e o mutex , quando devemos usar o mutex e quando devemos usar o semáforo e assim por diante. O exemplo do banheiro no primeiro link é um exemplo tão bom quanto se pode imaginar. Todo o código faz é verificar se a chave está disponível e, se estiver, reserva-a. Observe que você realmente não reserva o banheiro em si, mas a chave.

Makis
fonte
1
pthread_mutex_locknão pode retornar se outra pessoa segurar a trava. Bloqueia neste caso e esse é o ponto. pthread_mutex_trylocké a função que retornará se o bloqueio for mantido.
R .. GitHub Pare de ajudar o gelo 14/02
1
Sim, eu não percebi no início qual é a implementação.
Makis
3

Para quem procura o exemplo mutex do shortex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}
Vazio
fonte