Vaya resumen de errores | Obtener datos de carreras y condiciones de carrera correctas

Hola a todos, soy el pescador de "Go School". Hoy les hablaré sobre dos conceptos importantes en la concurrencia de Go: carrera de datos y condición de carrera.

Autor: Ir a la escuela

En programas simultáneos, los problemas de carrera pueden ser uno de los errores más difíciles y menos probables que enfrenta un programa. Como desarrollador de Go, es imperativo comprender las características clave de la competencia, como las carreras de datos y las condiciones de carrera. Echemos un vistazo a las características de las carreras de datos y las condiciones de carrera (también conocidas como carreras de recursos), y luego veamos cuándo ocurre cada una.

carrera de datos

Una carrera de datos ocurre cuando dos o más corrutinas acceden a la misma dirección de memoria al mismo tiempo y al menos una de ellas está escribiendo. Aquí hay un ejemplo de dos corrutinas haciendo +1 en la misma variable compartida:

i := 0
go func() {
    i++
}()

go func() {
    i++
}()
复制代码

Cuando lo ejecutamos go run -race main.go, mostrará el siguiente mensaje que indica que se ha producido una carrera de datos:

==================
WARNING: DATA RACE
Write at 0x00c00008e000 by goroutine 7:
 main.main.func2()
Previous write at 0x00c00008e000 by goroutine 6:
 main.main.func1()
==================
复制代码

Además, iel valor de , es impredecible. Podría ser 1, podría ser 2.

¿Cuál es el problema con este código? En realidad es i++una combinación de tres operaciones:

  • ileer valor de
  • el valor del valor+1
  • escribir el valor de nuevoi

Escenario 1: Goroutine1 termina de ejecutarse antes que Goroutine2

En este escenario, la situación sería la siguiente:

Gorutina1 Gorutina2 yo valoro
valor inicial 0
leer el valor de i 0
+1 el valor 0
escribir el valor a i 1
leer el valor de i 1
+1 el valor 1
escribir el valor a i 2

La primera rutina lee iel valor, luego +1opera sobre el valor y finalmente vuelve a escribir el valor en i. Luego, la segunda rutina comienza a ejecutarse nuevamente. Por lo tanto, iel resultado es 2.

Sin embargo, en el ejemplo anterior, no existe ningún mecanismo que garantice que la primera rutina debe completarse antes de la segunda lectura de la rutina. Veamos el siguiente escenario de concurrencia.

Escenario 2: Goroutine1 y Goroutine2 se ejecutan simultáneamente

En este escenario, la situación sería la siguiente:

Gorutina1 Gorutina2 I
0
leer el valor de i 0
leer el valor de i 0
+1 el valor 0
+1 el valor 0
escribir el valor a i 1
escribir el valor a i 1

首先,两个协程都从i中读取,得到结果都是0。然后,都将读到的值+1,然后将各自的值写回给i,结果是1。这是不符合我们预期的。

这是数据竞争造成的影响。如果两个协程同时访问同一块内存,并且至少有一个协程写入,就会导致一个不可预期的结果。

如何避免数据竞争的发生

第一种解决方案是让i++变成原子操作。如下:

Goroutine1 Goroutine2 i
0
读取值并+1操作 1
读取值并+1操作 2

使用这种方式,即使是协程2在协程1之前完成,最终结果也是2。

在Go中,原子操作可以使用atomic包。下面是一个具体使用的示例:

var i int64
go func() {
    atomic.AddInt64(&i, 1)
}()
go func() {
    atomic.AddInt64(&i, 1)
}()
复制代码

两个协程对i的操作都是原子性的。一个原子操作是不能被中断。因此,可以避免多个线程在同一时间访问同一共享数据。无论协程的执行顺序如何,i的最终结果都是2。

第二种解决方案是使用同步原语mutex。Mutex表示互斥,它确保最多一个goroutine访问所谓的关键部分。在Go中,sync包提供了Mutex类型:

i := 0
mutex := sync.Mutex{}
go func() {
    mutex.Lock()
    i++
    mutex.Unlock()
}()

go func() {
    mutex.Lock()
    i++
    mutex.Unlock()
}()
复制代码

在该示例中,对i进行+1操作是关键部分。无论协程的顺序如何,该示例中的i都会有一个确定的输出:2。

哪种方法好呢?首先,atomic包只能操作特定的类型(例如int32,int64等整数)。如果我们有一些其他类型的操作(比如,切片,map以及结构体),我们就不能依赖atomic包来解决问题了。

另一种避免同时读取同一块内存的方法是使用通道在多协程间进行通信。例如,我们可以创建一个channel,然后每个协程将要增加的值输入到通道中,如下:

i := 0
ch := make(chan int)
go func() {
    ch <- 1
}()

go func() {
    ch <- 1
}()

i += <-ch
i += <-ch
复制代码

该示例中,每个协程都将增量值(这里是1)依次输入到通道中。父协程管理通道并从通道中读取中对i进行算数加操作。因为只有一个协程在对i进行写操作,所以这种方法不存在数据竞争。

我们对上面做个小结。当多个协程同时访问同一块内存区域时,并且存在至少一个协程在进行写操作时,就会发生数据竞争(data-race)。我们共演示了3种避免数据竞争的方法:

  • 使用原子操作
  • 使用mutex对同一区域进行互斥操作
  • 使用通道进行通信以保证仅且只有一个协程在进行写操作

在这3种方法中,无论协程的顺序的执行如何,i的值都会是2。

那么,如果一个应用中没有数据竞争的存在,那么是否意味着一定能输出一个确定的结果呢?

竞争条件(race condition),也称资源竞争

我们先看一个示例。该示例中在两个协程中对变量i都进行直接赋值操作。我们使用mutex来避免数据竞争:

i := 0
mutex := sync.Mutex{}
go func() {
    mutex.Lock()
    defer mutex.Unlock()
    i = 1
}()

go func() {
    mutex.Lock()
    defer mutex.Unlock()
    i = 2
}()
复制代码

第一个协程把1赋给i,第二个协程把2赋给i

在该示例中会产生数据竞争吗?当然不会。两个协程虽然访问同一个变量,但由于我们使用了mutex机制,在同一时间只有一个协程能进行操作。那么,该示例的输出结果是确定的吗?当然不是确定。

变量i的结果依赖于协程的执行顺序,可能是1也可能是2。该示例不会产生数据竞争。但是,存在竞争条件(race condition),也称为资源竞争当程序的行为依赖于执行顺序或事件发生的时机不可控时就会发生竞争条件

在该示例中,事件发生的时机就是协程执行的顺序。

Asegurar el orden de ejecución entre rutinas es una cuestión de coordinación y orquestación. Si queremos asegurarnos de que el estado vaya de 0 a 1 y luego de 1 a 2, debemos encontrar una manera de garantizar que la corrutina se ejecutará en orden. Una forma es usar canales para resolver este problema . Además, si usamos canales para coordinación y orquestación, también podemos garantizar que solo una corrutina esté accediendo a la parte pública a la vez. Esto también significa que podemos eliminar el mutex.

Resumir

Cuando desarrollamos programas concurrentes, es importante comprender la diferencia entre carreras de datos y condiciones de carrera.

Una carrera de datos ocurre cuando varias corrutinas acceden a la misma ubicación de memoria al mismo tiempo y al menos una de ellas está escribiendo. Las carreras de datos significan un comportamiento indefinido.

Sin embargo, la ausencia de carreras de datos no significa que el resultado sea seguro. De hecho, incluso si no hay una carrera de datos en una aplicación, su comportamiento puede depender de un tiempo o un orden de ejecución incontrolables, que es una condición de carrera .

Conocer estos dos aspectos es esencial para diseñar con soltura aplicaciones concurrentes.

Supongo que te gusta

Origin juejin.im/post/7082753463339188260
Recomendado
Clasificación