10. Concurrencia

Algunas personas comparan Go con el lenguaje C del siglo XXI. Primero, porque el diseño del lenguaje Go es simple. En segundo lugar, lo más importante en el siglo XXI es la programación paralela, y Go admite el paralelismo desde el nivel del lenguaje.

gorutina

Las gorutinas son el núcleo del diseño paralelo de Go. En el análisis final, goroutine es en realidad un hilo, pero es más pequeño que un hilo. Una docena de goroutines pueden reflejarse como cinco o seis hilos en el nivel inferior. El lenguaje Go le ayuda a realizar el intercambio de memoria entre estos goroutines. La ejecución de goroutine requiere muy poca memoria de pila (aproximadamente 4 ~ 5 KB) y, por supuesto, se escalará de acuerdo con los datos correspondientes. Debido a esto, se pueden ejecutar miles de tareas simultáneas simultáneamente. Goroutine es más fácil de usar, más eficiente y más liviano que el hilo.

Goroutine es un administrador de subprocesos administrado por el tiempo de ejecución de Go. Goroutine se implementa mediante la palabra clave go, que en realidad es una función normal.

Una gorutina se inicia mediante la palabra clave go. Veamos un ejemplo

package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
    
    
	for i := 0; i < 5; i++ {
    
    
		runtime.Gosched()
		fmt.Println(s)
	}
}

func main() {
    
    
	go say("world")  //开一个新的Goroutines执行
	say("hello")     //当前Goroutines执行
}

Producción:

hello
hello
world
hello
world
world
world
world
hello
hello

Podemos ver que la palabra clave go implementa fácilmente la programación concurrente. Las múltiples gorutinas anteriores se ejecutan en el mismo proceso y comparten datos de memoria, pero debemos seguir el diseño: no comunicarnos compartiendo, sino compartiendo comunicando .

runtime.Gosched() significa permitir que la CPU ceda el intervalo de tiempo a otros y reanude la ejecución de la rutina la próxima vez.

De forma predeterminada, el programador utiliza solo un subproceso, lo que significa que solo se implementa la concurrencia. Para aprovechar el paralelismo de los procesadores multinúcleo, necesitamos llamar explícitamente a runtime.GOMAXPROCS(n) en nuestro programa para indicarle al programador que use múltiples subprocesos al mismo tiempo. GOMAXPROCS establece el número máximo de subprocesos del sistema que pueden ejecutar código lógico simultáneamente y devuelve la configuración anterior. Si n < 1, la configuración actual no se cambiará. Esto se eliminará cuando se mejore la programación en futuras versiones de Go.

canales

Las gorutinas se ejecutan en el mismo espacio de direcciones, por lo que el acceso a la memoria compartida debe estar sincronizado. Entonces, ¿cómo comunicar datos entre gorutinas? Go proporciona un buen canal de mecanismo de comunicación. Un canal se puede comparar con una tubería bidireccional en un shell Unix: puedes enviar o recibir valores a través de él. Estos valores sólo pueden ser de un tipo específico: tipo de canal. Cuando define un canal, también necesita definir el tipo de valor enviado al canal. Tenga en cuenta que debe utilizar make para crear el canal:

ci := make(chan int) 
cs := make(chan string) 
cf := make(chan interface{
    
    }) 

El canal recibe y envía datos a través del operador <-

ch <- v // 发送v到channel ch. 
v := <-ch // 从ch中接收数据,并赋值给v 

Apliquemos esto a nuestro ejemplo:

package main

import (
	"fmt"
)

func sum(a []int, c chan int) {
    
    
	sum := 0
	for _, v := range a {
    
    
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
    
    
	a := []int{
    
    7, 2, 8, -9, 4, 0}
	c := make(chan int)
	go sum(a[:len(a)/2], c)
	go sum(a[len(a)/2:], c)
	x, y := <-c, <-c // receive from c
	fmt.Println(x, y, x+y)
}

De forma predeterminada, los canales se bloquean al recibir y enviar datos, a menos que el otro extremo esté listo, lo que facilita la sincronización de Goroutines sin la necesidad de bloqueos explícitos. El llamado bloqueo significa que si lee (valor: = <-ch), se bloqueará hasta que se reciban los datos. En segundo lugar, cualquier envío (ch<-5) se bloqueará hasta que se lean los datos. Los canales sin búfer son una gran herramienta para sincronizar múltiples gorutinas.

Canales almacenados en búfer

Arriba presentamos el tipo de canal predeterminado sin almacenamiento en caché, pero Go también le permite especificar el tamaño del búfer del canal, que es muy simple, es decir, cuántos elementos puede almacenar el canal. ch:= make(chan bool, 4), crea un canal de tipo bool que puede almacenar 4 elementos. En este canal se pueden escribir los primeros 4 elementos sin bloquear. Cuando se escribe el quinto elemento, el código se bloqueará hasta que otra rutina lea algunos elementos del canal para hacer espacio.

ch := make(chan type, value) 
value == 0 ! 无缓冲(阻塞) 
value > 0 ! 缓冲(非阻塞,直到value 个元素) 

Echemos un vistazo al siguiente ejemplo, puedes probarlo en tu propia máquina y modificar el valor correspondiente.

package main

import "fmt"

func main() {
    
    
	c := make(chan int, 2) //修改2为1就报错,修改2为3可以正常运行
	c <- 1
	c <- 2
	fmt.Println(<-c)
	fmt.Println(<-c)
}

Alcance y cierre

En el ejemplo anterior, necesitamos leer c dos veces, lo cual no es muy conveniente. Go tiene esto en cuenta, por lo que también puede operar canales de tipo caché a través de un rango como segmento o mapa. Consulte el ejemplo a continuación.

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
    
    
	x, y := 1, 1
	for i := 0; i < n; i++ {
    
    
		c <- x
		x, y = y, x+y
	}
	close(c)
}
func main() {
    
    
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
    
    
		fmt.Println(i)
	}
}

para i: = rango c puede leer continuamente los datos en el canal hasta que el canal se cierre explícitamente. En el código anterior, vemos que el canal se puede cerrar explícitamente: el productor cierra el canal mediante la función de cierre de palabras clave. Después de cerrar el canal, no se pueden enviar más datos. El consumidor puede usar la sintaxis v, ok := <-ch para probar si el canal está cerrado. Si ok devuelve falso, significa que el canal no tiene datos y ha sido cerrado.

Recuerde cerrar el canal al productor, no al consumidor, ya que esto fácilmente puede causar pánico.

Otra cosa para recordar es que, a diferencia de los archivos y similares, los canales no necesitan cerrarse con frecuencia, sólo cuando realmente no tienes ningún dato para enviar, o quieres finalizar explícitamente el bucle de rango o algo similar.

Seleccionar

Lo que hemos introducido anteriormente es el caso en el que solo hay un canal, entonces, ¿cómo debemos operar si hay varios canales? Go proporciona una selección de palabras clave, a través de la cual puede monitorear el flujo de datos en el canal.

Select está bloqueando de forma predeterminada. Solo se ejecutará cuando haya envío o recepción en el canal monitoreado. Cuando hay varios canales listos, select selecciona uno aleatoriamente para su ejecución.

package main

import "fmt"

func fibonacci(c, quit chan int) {
    
    
	x, y := 1, 1
	for {
    
    
		select {
    
    
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}
func main() {
    
    
	c := make(chan int)
	quit := make(chan int)
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

También hay una sintaxis predeterminada en select. Select es en realidad una función similar a switch. Default se ejecuta de forma predeterminada cuando el canal monitoreado no está listo (select ya no bloquea y espera el canal).

select {
    
     
    case i := <-c:
	// use i 
    default: 
    // 当c阻塞的时候执行这里 
}

se acabó el tiempo

A veces, la rutina se bloquea, entonces, ¿cómo evitamos que se bloquee todo el programa? Podemos usar select para configurar el tiempo de espera de la siguiente manera:

package main

import "time"

func main() {
    
    
	c := make(chan int)
	o := make(chan bool)
	go func() {
    
    
		for {
    
    
			select {
    
    
			case v := <-c:
				println(v)
			case <-time.After(5 * time.Second):
				println("timeout")
				o <- true
				break
			}
		}
	}()
	<-o
}

rutina de tiempo de ejecución

Hay varias funciones para procesar gorutinas en el paquete de tiempo de ejecución:

  • Irsalir

    Salga de la rutina que se está ejecutando actualmente, pero se seguirá llamando a la función de aplazamiento.

  • goshet

    Al renunciar al permiso de ejecución de la rutina actual, el programador organiza otras tareas en espera para que se ejecuten y reanuda la ejecución desde esta posición la próxima vez.

  • número de CPU

    Devuelve el número de núcleos de CPU.

  • NumGorutina

    Devuelve el número total de tareas en ejecución y en cola.

  • GOMAXPROCS

    Se utiliza para establecer la cantidad de núcleos de CPU que se pueden ejecutar.

Supongo que te gusta

Origin blog.csdn.net/u012534326/article/details/120399800
Recomendado
Clasificación