Divide y vencerás


Cuando trabajaba con una base de datos (en particular con PostgreSQL), tuve la idea de seleccionar datos de una tabla en paralelo (usando Go GoP). Y me preguntaba: "¿es posible escanear líneas de muestra en goroutines individuales?"

Al final resultó que, func (* Filas) Scan no se puede llamar simultáneamente en gourutins. En base a esta limitación, decidí llevar a cabo otros procesos en paralelo con el escaneo de filas, en particular, la preparación de los datos resultantes.

Porque Scan apila los datos de acuerdo con los punteros, decidí hacer dos cortes (explicaré por qué dos más tarde), entre los cuales cambiaré Scan, mientras que el resto de las gourutinas se ocuparán de los datos ya seleccionados.

Inicialmente, necesito saber el número de columnas de muestra:

columns, err = rows.Columns() count := len(columns) 

A continuación, creo dos sectores con valores y punteros a estos valores (donde agregaré datos durante el escaneo de filas):

 values := make([]interface{}, count) valuesPtrs := make([]interface{}, count) values_ := make([]interface{}, count) valuesPtrs_ := make([]interface{}, count) for i := range columns { valuesPtrs[i] = &values;[i] valuesPtrs_[i] = &values;_[i] } 

En este ejemplo, agregaré el resultado de la selección para asignar la cadena [string], donde los nombres de las columnas serán las claves. Puede usar una estructura específica que indique los tipos, pero dado que El propósito de esta publicación es descubrir en la sociedad habana cuán viable es el enfoque propuesto, detengámonos en la selección en el mapa.

A continuación, separo dos gorutinas, una de las cuales formará el mapa resultante:

 func getData(deleteNullValues bool, check, finish chan bool, dbData chan interface{}, columns []string, data *[]map[string]string) { lnc := len(columns) for <-check { row := make(map[string]string) for i := 0; i < lnc; i++ { el := <-dbData b, ok := el.([]byte) if ok { row[columns[i]] = string(b) } else { if el == nil { if deleteNullValues == false { row[columns[i]] = "" } } else { row[columns[i]] = fmt.Sprint(el) } } } *data = append(*data, row) } finish <- true } 

Y el segundo cambiará entre dos sectores con los valores generados por Scan y los enviará al canal para la gourutina anterior (que forma el resultado):

 func transferData(values, values_ []interface{}, dbData chan interface{}, swtch, working, check chan bool) { for <-working { check <- true switch <-swtch { case false: for _, v := range values { dbData <- v } default: for _, v := range values_ { dbData <- v } } } } 

El proceso principal cambiará entre sectores de punteros y seleccionará datos:

 for rows.Next() { switch chnl { case false: if err = rows.Scan(valuesPtrs...); err != nil { fmt.Printf("rows.Scan: %s\n%s\n%#v\n", err, query, args) return nil, nil, err } default: if err = rows.Scan(valuesPtrs_...); err != nil { fmt.Printf("rows.Scan: %s\n%s\n%#v\n", err, query, args) return nil, nil, err } } working <- true swtch <- chnl chnl = !chnl } 

En la base de datos, formé una tabla con 32 columnas y le agregué 100k filas.
Como resultado de la prueba (al muestrear datos 50 veces), obtuve los siguientes datos:
Tiempo invertido: 1m8.022277124s - muestreando el resultado usando una sola porción
Tiempo empleado: 1m7.806109441s - muestreando el resultado usando dos rebanadas

Con un aumento en el número de iteraciones a 100:
Tiempo empleado: 2m15.973344023s - selección del resultado usando una sola porción
Tiempo invertido: 2m15.057413845s - muestreando el resultado usando dos rebanadas

La diferencia aumenta al aumentar el volumen de datos y aumentar las columnas en la tabla.
Sin embargo, el resultado opuesto se observó con una disminución en la cantidad de datos o con una disminución en el número de columnas de la tabla, lo que, en principio, es comprensible, porque la sobrecarga de los pasos preparatorios y el departamento de gourutin "comen" el tiempo precioso y el resultado se nivela.

En cuanto a dos rebanadas y dos gorutinas: realicé pruebas con una gran cantidad de rebanadas, pero el tiempo de muestreo aumentó, porque, obviamente, las funciones getData y transferData procesan los datos más rápido que los valores de escaneo de la base de datos. Por lo tanto, incluso con un mayor número de núcleos, no tiene sentido agregar nuevos segmentos para Scan y goroutines adicionales (excepto para volúmenes de datos muy salvajes).

En el código github, doy un ejemplo funcional de este enfoque. Mis tareas también usan otros paquetes, que eliminé de los anteriores cuando, pero la idea principal no debería sufrir de esto.

En general, espero críticas constructivas de la comunidad interesada. Gracias

Source: https://habr.com/ru/post/485056/


All Articles