Quando dois ou mais Threads/Processos competem por um recurso.
Race Condition (Condição de Corrida)
Iremos utilizar goroutines que é a forma de executar uma atividade de maneira concorrente em go.
Problema:
Suponha que temos três clientes: um chamado A, outro chamado B e outro C. Todos estão interessados em comprar a mesma camisa em um e-commerce, porém existem apenas 10 unidades disponíveis dessa camisa. Crie uma goroutine para cada cliente e simule a compra dessa camisa, “printando” na tela qual cliente comprou a camisa, a quantidade comprada e qual a quantidade restante de camisas no estoque.
package main import ( "fmt" "strconv" "sync" "time"
) type itemType struct { itemId int name string price int unit int
} func (i *itemType) takeItem(customer *customerType, unit int) { // Simula um atraso para aumentar a chance de condição de corrida time.Sleep(50 * time.Millisecond) if i.unit > 0 && i.unit >= unit { i.unit = i.unit - unit fmt.Println("O Cliente " + customer.name + " comprou " + strconv.Itoa(unit) + " unidade(s) de " + i.name + " e a quantidade restante no estoque é " + strconv.Itoa(i.unit)) } else { fmt.Println("O Cliente " + customer.name + " não conseguiu comprar " + strconv.Itoa(unit) + " unidade(s) de " + i.name) }
} type customerType struct { customerId int name string
} var wg sync.WaitGroup func main() { shirt := itemType{441, "camisa", 1000, 10} customerA := customerType{1, "A"} customerB := customerType{2, "B"} customerC := customerType{3, "C"} wg.Add(3) go routineCustomer(&shirt, &customerA, 4) go routineCustomer(&shirt, &customerB, 5) go routineCustomer(&shirt, &customerC, 3) wg.Wait()
} func routineCustomer(shirt *itemType, customer *customerType, unit int) { shirt.takeItem(customer, unit) wg.Done()
}
Dado que a camisa tem apenas 10 unidades e três clientes querem comprar um total de 12 unidades. A primeira resposta é que o Cliente C comprou 3 unidades de camisa e a quantidade restante no estoque é de 2 unidades. Porém, se o total de camisas é 10 unidades e o Cliente C comprou 3 unidades, deveria haver 7 unidades no estoque. Após o Cliente C, temos o Cliente A que não conseguiu comprar 4 camisas e, por fim, o Cliente B que conseguiu comprar 5 unidades, mostrando na tela que ainda há 5 unidades em estoque. Isso demonstra um problema de race condition onde temos threads/processos competindo por um recurso.
Adicionando o controle de concorrência com mutex
Agora queremos que cada cliente que estiver com o item possa subtrair uma unidade desse item sem que nenhuma outra goroutine esteja acessando e modificando a quantidade do item.
package main import ( "fmt" "strconv" "sync" "time"
) type itemType struct { itemId int name string price int unit int
} func (i *itemType) takeItem(customer *customerType, unit int) { mu.Lock() // Simula um atraso para aumentar a chance de condição de corrida time.Sleep(50 * time.Millisecond) if i.unit > 0 && i.unit >= unit { i.unit = i.unit - unit fmt.Println("O Cliente " + customer.name + " comprou " + strconv.Itoa(unit) + " unidade(s) de " + i.name + " e a quantidade restante no estoque é " + strconv.Itoa(i.unit)) } else { fmt.Println("O Cliente " + customer.name + " não conseguiu comprar " + strconv.Itoa(unit) + " unidade(s) de " + i.name) } mu.Unlock()
} type customerType struct { customerId int name string
} var wg sync.WaitGroup
var mu sync.Mutex func main() { shirt := itemType{441, "camisa", 1000, 10} customerA := customerType{1, "A"} customerB := customerType{2, "B"} customerC := customerType{3, "C"} wg.Add(3) go routineCustomer(&shirt, &customerA, 4) go routineCustomer(&shirt, &customerB, 5) go routineCustomer(&shirt, &customerC, 3) wg.Wait()
} func routineCustomer(shirt *itemType, customer *customerType, unit int) { shirt.takeItem(customer, unit) wg.Done()
}
Agora temos como saída o Cliente B comprando 5 unidades e a quantidade do estoque sendo 5, logo após o Cliente C comprando 3 unidades e a quantidade do estoque sendo 2, e por fim, o Cliente A não consegue comprar 4 unidades porque só existem 2 unidades disponíveis.
Nesse caso, o que ocorre é o seguinte: quando uma goroutine tenta subtrair a unidade do item, ela deve chamar o método Lock do mutex para adquirir uma trava exclusiva. Se outra goroutine tiver adquirido a trava, essa operação ficará bloqueada até a outra goroutine chamar Unlock e a trava tornar-se disponível novamente. O mutex protege as variáveis compartilhadas.
A região de código entre Lock e Unlock, em que a goroutine é livre para ler e modificar as variáveis compartilhadas, é chamada de seção crítica.
Link do Código
Github: https://github.com/jcelsocosta/race_condition/tree/main
Referências
- DONOVAN, Alan A. A.; KERNIGHAN, Brian W. A Linguagem de Programação Go. 1. ed. São Paulo: Novatec, 2016. 400 p.