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.