Sincronización de Gorutinas Concurrentes

GolangGolangBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Este laboratorio tiene como objetivo demostrar cómo utilizar canales y gorutinas para sincronizar el acceso a un estado compartido entre múltiples gorutinas.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/ConcurrencyGroup(["Concurrency"]) go/ConcurrencyGroup -.-> go/stateful_goroutines("Stateful Goroutines") subgraph Lab Skills go/stateful_goroutines -.-> lab-15510{{"Sincronización de Gorutinas Concurrentes"}} end

Gorutinas con Estado

En la programación concurrente, es esencial sincronizar el acceso al estado compartido para evitar condiciones de carrera y corrupción de datos. Este laboratorio presenta un escenario en el que una sola gorutina posee el estado y otras gorutinas envían mensajes para leer o escribir el estado.

  • Utilice canales para emitir solicitudes de lectura y escritura a la gorutina que posee el estado.
  • Utilice los structs readOp y writeOp para encapsular solicitudes y respuestas.
  • Utilice un mapa para almacenar el estado.
  • Utilice canales resp para indicar éxito y devolver valores.
  • Utilice el paquete atomic para contar operaciones de lectura y escritura.
  • Utilice el paquete time para agregar un retraso entre operaciones.
## Ejecutar nuestro programa muestra que el ejemplo de
## administración de estado basado en gorutinas completa
## aproximadamente 80.000 operaciones en total.
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177

## En este caso particular, el enfoque basado en gorutinas
## fue un poco más complejo que el basado en mutex.
## Puede ser útil en ciertos casos, por ejemplo, donde
## haya otros canales involucrados o cuando la gestión
## de múltiples mutexes sea propensa a errores. Debe
## utilizar el enfoque que sea más natural para usted,
## especialmente con respecto a la comprensión de la
## corrección de su programa.

A continuación está el código completo:

// En el ejemplo anterior utilizamos un bloqueo explícito con
// [mutexes](mutexes) para sincronizar el acceso al estado
// compartido entre múltiples gorutinas. Otra opción es
// utilizar las características de sincronización integradas
// de las gorutinas y los canales para obtener el mismo
// resultado. Este enfoque basado en canales está en
// línea con las ideas de Go de compartir memoria
// comunicándose y que cada pieza de datos sea propiedad
// de exactamente 1 gorutina.

package main

import (
	"fmt"
	"math/rand"
	"sync/atomic"
	"time"
)

// En este ejemplo, nuestro estado será propiedad de una
// sola gorutina. Esto garantizará que los datos nunca se
// corrompan con el acceso concurrente. Para leer o
// escribir ese estado, otras gorutinas enviarán mensajes
// a la gorutina propietaria y recibirán respuestas
// correspondientes. Estos structs `readOp` y `writeOp`
// encapsulan esas solicitudes y una forma para que la
// gorutina propietaria responda.
type readOp struct {
	key  int
	resp chan int
}
type writeOp struct {
	key  int
	val  int
	resp chan bool
}

func main() {

	// Como antes, contaremos cuántas operaciones
	// realizamos.
	var readOps uint64
	var writeOps uint64

	// Los canales `reads` y `writes` serán utilizados por
	// otras gorutinas para emitir solicitudes de lectura y
	// escritura, respectivamente.
	reads := make(chan readOp)
	writes := make(chan writeOp)

	// Aquí está la gorutina que posee el `estado`, que es
	// un mapa como en el ejemplo anterior pero ahora
	// privado a la gorutina con estado. Esta gorutina
	// selecciona repetidamente en los canales `reads` y
	// `writes`, respondiendo a las solicitudes a medida
	// que llegan. Una respuesta se ejecuta primero
	// realizando la operación solicitada y luego enviando
	// un valor en el canal de respuesta `resp` para
	// indicar éxito (y el valor deseado en el caso de
	// `reads`).
	go func() {
		var state = make(map[int]int)
		for {
			select {
			case read := <-reads:
				read.resp <- state[read.key]
			case write := <-writes:
				state[write.key] = write.val
				write.resp <- true
			}
		}
	}()

	// Esto inicia 100 gorutinas para emitir lecturas a la
	// gorutina que posee el estado a través del canal
	// `reads`. Cada lectura requiere construir un `readOp`,
	// enviarlo a través del canal `reads` y luego recibir
	// el resultado a través del canal `resp` proporcionado.
	for r := 0; r < 100; r++ {
		go func() {
			for {
				read := readOp{
					key:  rand.Intn(5),
					resp: make(chan int)}
				reads <- read
				<-read.resp
				atomic.AddUint64(&readOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}

	// También iniciamos 10 escrituras, utilizando un
	// enfoque similar.
	for w := 0; w < 10; w++ {
		go func() {
			for {
				write := writeOp{
					key:  rand.Intn(5),
					val:  rand.Intn(100),
					resp: make(chan bool)}
				writes <- write
				<-write.resp
				atomic.AddUint64(&writeOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}

	// Deje que las gorutinas trabajen durante un segundo.
	time.Sleep(time.Second)

	// Finalmente, capture y reporte los conteos de
	// operaciones.
	readOpsFinal := atomic.LoadUint64(&readOps)
	fmt.Println("readOps:", readOpsFinal)
	writeOpsFinal := atomic.LoadUint64(&writeOps)
	fmt.Println("writeOps:", writeOpsFinal)
}

Resumen

Este laboratorio demostró cómo utilizar canales y gorutinas para sincronizar el acceso a un estado compartido. Al tener una sola gorutina que posea el estado y utilizar canales para emitir solicitudes de lectura y escritura, podemos evitar condiciones de carrera y corrupción de datos.