Dividir e conquistar


Ao trabalhar com um banco de dados (em particular com o PostgreSQL), tive a ideia de selecionar dados de uma tabela em paralelo (usando Go GoP). E me perguntei: "é possível digitalizar linhas de amostra em goroutines individuais?"

Como se viu, o func (* Rows) Scan não pode ser chamado simultaneamente em gourutins. Com base nessa limitação, decidi realizar outros processos em paralelo com a varredura das strings, em particular a preparação dos dados resultantes.

Porque O Scan empilha os dados de acordo com os ponteiros. Decidi fazer duas fatias (explicarei o motivo mais tarde), entre as quais alternarei o Scan, enquanto o restante dos gourutins lidará com os dados já selecionados.

Inicialmente, preciso saber o número de colunas de amostra:

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

Em seguida, crio duas fatias com valores e com ponteiros para esses valores (onde adicionarei dados durante a varredura de linha):

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

Neste exemplo, adicionarei o resultado da seleção ao mapeamento [string] da string, onde os nomes das colunas serão as chaves. Você pode usar uma estrutura específica indicando os tipos, mas desde o objetivo desta publicação é descobrir da habrocociedade quão viável é a abordagem proposta, vamos nos debruçar sobre a seleção no mapa.

Em seguida, separo dois gorutins, um dos quais formará o 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 } 

E o segundo alternará entre duas fatias com os valores gerados pelo Scan e os enviará ao canal para o gourutin anterior (que forma o 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 } } } } 

O processo principal alternará entre fatias de ponteiros e selecionará dados:

 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 } 

No banco de dados, formei uma tabela com 32 colunas e adicionei 100 mil linhas a ela.
Como resultado do teste (ao amostrar dados 50 vezes), obtive os seguintes dados:
Tempo gasto: 1m8.022277124s - amostrando o resultado usando uma única fatia
Tempo gasto: 1m7.806109441s - amostrando o resultado usando duas fatias

Com um aumento no número de iterações para 100:
Tempo gasto: 2m15.973344023s - seleção do resultado usando uma única fatia
Tempo gasto: 2m15.057413845s - amostrando o resultado usando duas fatias

A diferença aumenta com o aumento do volume de dados e o aumento das colunas na tabela.
No entanto, o resultado oposto foi observado com uma diminuição na quantidade de dados ou com uma diminuição no número de colunas da tabela, o que, em princípio, é compreensível, porque a sobrecarga das etapas preparatórias e o departamento de gourutins “consomem” o tempo precioso e o resultado é nivelado.

Quanto a duas fatias e duas gorutins: realizei testes com um grande número de fatias, mas o tempo de amostragem aumentou, porque, obviamente, as funções getData e transferData processam os dados mais rapidamente do que os valores de varredura do banco de dados. Portanto, mesmo com um número maior de núcleos, não faz sentido adicionar novas fatias para o Scan e goroutines adicionais (exceto para volumes de dados muito selvagens).

No código do github, dou um exemplo prático dessa abordagem. Minhas tarefas também usam outros pacotes, que eu limpei dos itens acima quando, mas a idéia principal não deve sofrer com isso.

Em geral, espero críticas construtivas da comunidade interessada. Obrigada

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


All Articles