Diviser et conquérir


Lorsque je travaillais avec une base de données (en particulier avec PostgreSQL), j'ai eu l'idée de sélectionner les données d'une table en parallèle (en utilisant Go GoP). Et je me suis demandé, "est-il possible de scanner des lignes d'échantillons dans des goroutines individuelles?"

Il s'est avéré que func (* Rows) Scan ne peut pas être appelé simultanément dans les gourutins. Sur la base de cette limitation, j'ai décidé d'effectuer d'autres processus en parallèle avec le balayage des chaînes, en particulier la préparation des données résultantes.

Parce que Scan empile les données selon les pointeurs, j'ai décidé de faire deux tranches (j'expliquerai pourquoi deux plus tard), entre lesquelles je vais basculer Scan, tandis que le reste des gourutins traitera les données déjà sélectionnées.

Au départ, j'ai besoin de connaître le nombre d'exemples de colonnes:

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

Ensuite, je crée deux tranches avec des valeurs et des pointeurs vers ces valeurs (où j'ajouterai des données lors de l'analyse des lignes):

 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] } 

Dans cet exemple, j'ajouterai le résultat de la sélection à la chaîne de chaîne [chaîne], où les noms de colonne seront les clés. Vous pouvez utiliser une structure spécifique indiquant les types, mais depuis le but de cette publication est de découvrir à partir de la habrasociety à quel point l'approche proposée est viable, arrêtons-nous sur la sélection sur la carte.

Ensuite, je sépare deux gorutins, dont l'un formera la carte résultante:

 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 } 

Et la seconde bascule entre deux tranches avec les valeurs générées par Scan et les envoie au canal de la gourutine précédente (qui forme le résultat):

 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 } } } } 

Le processus principal basculera entre les tranches de pointeurs et sélectionnera les données:

 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 } 

Dans la base de données, j'ai formé un tableau avec 32 colonnes et y ai ajouté 100 000 lignes.
À la suite du test (lors de l'échantillonnage des données 50 fois), j'ai obtenu les données suivantes:
Temps passé: 1m8.022277124s - échantillonnage du résultat à l'aide d'une seule tranche
Temps passé: 1m7.806109441s - échantillonnage du résultat à l'aide de deux tranches

Avec une augmentation du nombre d'itérations à 100:
Temps passé: 2m15.973344023s - sélection du résultat à l'aide d'une seule tranche
Temps passé: 2m15.057413845s - échantillonnage du résultat à l'aide de deux tranches

La différence augmente avec l'augmentation du volume de données et l'augmentation des colonnes dans le tableau.
Cependant, le résultat inverse a été observé avec une diminution de la quantité de données ou avec une diminution du nombre de colonnes du tableau, ce qui, en principe, est compréhensible, car les frais généraux des étapes préparatoires et le département gourutin «dévorent» le temps précieux et le résultat est nivelé.

En ce qui concerne deux tranches et deux gorutins: j'ai effectué des tests avec un grand nombre de tranches, mais le temps d'échantillonnage a augmenté, car, évidemment, les fonctions getData et transferData traitent les données plus rapidement que l'analyse des valeurs de la base de données. Par conséquent, même avec un plus grand nombre de cœurs, cela n'a aucun sens d'ajouter de nouvelles tranches pour Scan et des goroutines supplémentaires (sauf pour les volumes de données très sauvages).

Dans le code github, je donne un exemple de travail de cette approche. Mes tâches utilisent également d'autres packages, que j'ai supprimés de ce qui précède, mais l'idée principale ne devrait pas en souffrir.

En général, j'attends des critiques constructives de la communauté intéressée. Je vous remercie!

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


All Articles