Go language | bloqueo de sincronización y uso de grupos de espera en diseño concurrente

Hoy es el artículo 16 del tema de golang . Hablemos de algunos usos relacionados con la concurrencia en golang.

Aunque hemos terminado de introducir goroutines y canales, el mecanismo de concurrencia aún no está terminado. Solo las gorutinas y los canales a veces no son suficientes para completar nuestro problema. Por ejemplo, cuando varias gorutinas acceden a una variable al mismo tiempo, ¿cómo podemos asegurarnos de que estas gorutinas no entren en conflicto o se afecten entre sí? Esto puede requerir que bloqueemos recursos o realicemos otras acciones.

Bloqueo de sincronización

Hay dos bloqueos de uso común en golang, uno es sync.Mutex y el otro es sync.RWMutex. Hablemos primero de Mutex. Es el bloqueo de sincronización más simple y básico . Cuando una goroutine mantiene el bloqueo, otras goroutines solo pueden intentar mantenerlo después de que se suelta el bloqueo. RWMutex significa un bloqueo de lectura-escritura Admite una escritura y múltiples lecturas , lo que significa que múltiples goroutines pueden mantener un bloqueo de lectura al mismo tiempo, mientras que solo una goroutine puede mantener un bloqueo de escritura. Cuando una goroutine tiene un bloqueo de lectura, bloqueará las operaciones de escritura. Cuando una goroutine mantiene un bloqueo de escritura, tanto la lectura como la escritura se bloquearán.

Cuando lo usamos, debemos decidir en función de las características de nuestra escena.Si nuestra escena es una escena donde las operaciones de lectura son más que operaciones de escritura, entonces podemos usar RWMutex. Si la operación de escritura es la principal, cualquiera sea la misma.

Echemos un vistazo al caso de uso. Suponiendo que actualmente tenemos múltiples goroutines, pero solo queremos que se ejecute la goroutine que mantiene el bloqueo, podemos escribir:

var lock sync.Mutex

for i := 0; i < 10; i++ {
    go func() {
        lock.Lock()
        defer lock.Unlock()
        // do something
    }()
}

Aunque comenzamos 10 goroutines con un bucle for, debido a la existencia del mutex, solo se puede ejecutar una goroutine al mismo tiempo .

RWMutex distingue los bloqueos de lectura y escritura, por lo que tenemos un total de 4 apis, a saber, Lock, Unlock, RLock, RUnlock. Bloquear y desbloquear son el bloqueo y desbloqueo de bloqueos de escritura, mientras que RLock y RUnlock son naturalmente el bloqueo y desbloqueo de bloqueos de lectura. El uso específico es el mismo que el del código anterior, por lo que no entraré en detalles.

Operación global una vez

En algunos escenarios y algunos patrones de diseño, se nos pedirá que ejecutemos una determinada parte del código solo una vez. Por ejemplo, el conocido modo singleton consiste en diseñar una herramienta que usamos a menudo como singleton. No importa cuántas veces se inicialice durante la operación, se obtiene la misma instancia. El propósito de esto es reducir el tiempo para crear instancias, especialmente el proceso de creación de instancias, como conexiones a bases de datos y conexiones hbase, consume mucho tiempo.

Entonces, ¿cómo implementamos singletons en golang?

Algunos estudiantes pueden pensar que esto es muy simple ¿Solo necesitamos usar una variable bool para juzgar si la inicialización se completó o no? Por ejemplo:

type Test struct {}
var test Test
var flag = false

func init() Test{
    if !flag {
        test = Test{}
        flag = true
    }
    return test
}

Parece que no hay problema, pero después de una cuidadosa consideración, encontrará que algo anda mal. Porque el enunciado del juicio if no es atómico , es decir, puede ser accedido por muchas gorutinas al mismo tiempo. En este caso, es posible que la variable test se inicialice y sobrescriba muchas veces hasta que una de las goroutines establezca el indicador en verdadero. Esto puede dar lugar a diferentes pruebas obtenidas por las gorutinas visitadas al principio, lo que puede causar riesgos desconocidos.

Lograr un singleton es realmente muy simple, la biblioteca de sincronización nos proporciona una herramienta lista una vez. Puede pasar una función y solo permite la ejecución global de esta función una vez . Antes de que finalice la ejecución, otras goroutines se bloquearán cuando se ejecute la instrucción once, asegurando que solo una goroutine se esté ejecutando una vez. Cuando termine la ejecución once, el contenido de la instrucción once se saltará cuando se vuelva a ejecutar aquí. Combinemos el código para entenderlo. En realidad, es muy simple.

type Test struct {}
var test Test

func create() {
    test = Test{}
}

func init() Test{
    once.Do(create)
    return test
}

grupo de espera

Finalmente, les presentaré el uso de grupo de espera Cuando usamos goroutine, un problema es que no sabemos cuándo termina la ejecución de goroutine en el programa principal . Si solo necesitamos confiar en los resultados de la ejecución de la goroutine, por supuesto que podemos hacerlo a través de canales. Pero si claramente queremos esperar hasta que finalice la ejecución de la goroutine antes de ejecutar la siguiente lógica, ¿qué debemos hacer en este momento?

Algunas personas dicen que se puede usar el sueño, pero el problema es que no sabemos cuánto tiempo tarda en ejecutarse la gorutina ¿Cómo podemos saber de antemano cuánto tiempo tarda en dormir?

Para solucionar este problema, podemos utilizar otra herramienta sincronizada, que es grupo de espera.

El uso de grupo de espera es muy simple, solo hay tres métodos, uno es Agregar, otro es Listo y el último es Esperar . De hecho, waitgroup almacena internamente cuántas goroutines se están ejecutando actualmente. Cuando se llama a Add x una vez, significa que se generan x nuevas goroutines al mismo tiempo. Cuando se ejecutan estas goroutines, dejamos que se llame Done, lo que significa que la ejecución ha terminado para una goroutine. De esta forma, cuando todas las goroutines hayan terminado de ejecutarse Done, finalizará el bloqueo de espera.

Veamos un ejemplo:

sample := Sample{}

wg := sync.WaitGroup{}

go func() {
    // 增加一个正在执行的goroutine
    wg.Add(1)
    // 执行完成之后Done一下
    defer wg.Done()
    sample.JoinUserFeature() 
}()

go func() {
    wg.Add(1)
    defer wg.Done()
    sample.JoinItemFeature() 
}()

wg.Wait()
// do something

para resumir

Las herramientas y bibliotecas presentadas anteriormente son las que usamos a menudo en escenarios concurrentes, y también son una de las habilidades que un ingeniero de golang debe conocer . Hasta ahora, las funciones básicas del lenguaje golang son casi las mismas, y más adelante se presentarán algunas aplicaciones prácticas, así que estad atentos.

Supongo que te gusta

Origin blog.csdn.net/Flogod/article/details/108573202
Recomendado
Clasificación