[Golang Practical Combat] Resolvendo os problemas ignorados por Golang

Insira a descrição da imagem aqui

Referências

https://github.com/aceld/golang
https://zhuanlan.zhihu.com/p/597424646
https://www.cnblogs.com/xxswkl/p/14248560.html

1. Correndo entre WaitGroup e GoRoutine

package main

import (
	"sync"
	//"time"
)

const N = 10

var wg = &sync.WaitGroup{}

func main() {

	for i := 0; i < N; i++ {
		go func(i int) {
			wg.Add(1)
			println(i)
			defer wg.Done()
		}(i)
	}
	wg.Wait()

}

O resultado não é único e nem todo go pode ser executado: isso ocorre porque go é executado muito rápido, fazendo com que wg.Add(1) seja executado antes da execução da função principal.

As melhorias são as seguintes

package main

import (
	"sync"
)

const N = 10

var wg = &sync.WaitGroup{}

func main() {

    for i:= 0; i< N; i++ {
        wg.Add(1)
        go func(i int) {
            println(i)
            defer wg.Done()
        }(i)
    }

    wg.Wait()
}

2. Bloqueio mutex mutex e bloqueio de leitura e gravação mutex RWMutex

Mutex, bloqueio absoluto (bloqueio mutex), há apenas um
RWMutex, bloqueio de leitura e gravação, bloqueio de leitura RLock() ao mesmo tempo, pode haver vários bloqueios de leitura, bloqueio de gravação Lock(), a operação de gravação é completamente mutuamente exclusiva , quando uma goroutine escreve When , outros não podem escrever nem ler

var count int
var wg sync.WaitGroup
var rw sync.RWMutex
func main() {
    wg.Add(10)
    for i:=0;i<5;i++ {
        go read(i)
    }
    for i:=0;i<5;i++ {
        go write(i);
    }
    wg.Wait()
}
func read(n int) {
    // 读锁是RLock(),
    rw.RLock()
    fmt.Printf("读goroutine %d 正在读取...\n",n)
    v := count
    fmt.Printf("读goroutine %d 读取结束,值为:%d\n", n,v)
    wg.Done()
    rw.RUnlock()
}
func write(n int) {
    // 写锁是Lock()
    rw.Lock()
    fmt.Printf("写goroutine %d 正在写入...\n",n)
    v := rand.Intn(1000)
    count = v
    fmt.Printf("写goroutine %d 写入结束,新值为:%d\n", n,v)
    wg.Done()
    rw.Unlock()
}

O mapa pode ser transformado em um SynchronizedMap seguro para threads baseado em bloqueios de leitura e gravação

// 安全的Map
type SynchronizedMap struct {
   rw *sync.RWMutex
   data map[interface{}]interface{}
}
// 存储操作
func (sm *SynchronizedMap) Put(k,v interface{}){
   sm.rw.Lock()
   defer sm.rw.Unlock()

   sm.data[k]=v
}
// 获取操作  只有这个加的是读锁,
func (sm *SynchronizedMap) Get(k interface{}) interface{}{
   sm.rw.RLock()
   defer sm.rw.RUnlock()

   return sm.data[k]
}
// 删除操作
func (sm *SynchronizedMap) Delete(k interface{}) {
   sm.rw.Lock()
   defer sm.rw.Unlock()

   delete(sm.data,k)
}
// 遍历Map,并且把遍历的值给回调函数,可以让调用者控制做任何事情
func (sm *SynchronizedMap) Each(cb func (interface{},interface{})){
   sm.rw.RLock()
   defer sm.rw.RUnlock()
   for k, v := range sm.data {
       cb(k,v)
   }
}
// 生成初始化一个SynchronizedMap
func NewSynchronizedMap() *SynchronizedMap{
   return &SynchronizedMap{
       rw:new(sync.RWMutex),
       data:make(map[interface{}]interface{}),
   }
}

3. enquete, selecione, epoll

enquete

while true {
	for i in 流[] {
		if i has 数据 {
			读 或者 其他处理
		}
	}
}

selecione

while true {
	select(流[]); //阻塞

	//有消息抵达
	for i in 流[] {
		if i has 数据 {
			读 或者 其他处理
		}
	}
}

epoll

while true {
	可处理的流[] = epoll_wait(epoll_fd); //阻塞

	//有消息抵达,全部放在 “可处理的流[]”中
	for i in 可处理的流[] {
		读 或者 其他处理
	}
}

4. Quando é empilhar e empilhar?

O compilador fará a análise de escape. Quando for constatado que o escopo da variável não sai do escopo da função, ela pode estar na pilha, caso contrário, deverá ser alocada no heap.

5. Uso razoável de GoRoutine

Uma GoRoutine ocupa cerca de 2,5 KB

Recomendação 1: Método de combinação de sincronização de canal e sincronização

package main

import (
    "fmt"
    "math"
    "sync"
    "runtime"
)

var wg = sync.WaitGroup{}

func busi(ch chan bool, i int) {

    fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())

    <-ch

    wg.Done()
}

func main() {
    //模拟用户需求go业务的数量
    task_cnt := math.MaxInt64

    ch := make(chan bool, 3)

    for i := 0; i < task_cnt; i++ {
		wg.Add(1)

        ch <- true

        go busi(ch, i)
    }

	  wg.Wait()
}

Recomendação 2: Use canal sem buffer e método de separação de envio/execução de tarefas

package main

import (
    "fmt"
    "math"
    "sync"
    "runtime"
)

var wg = sync.WaitGroup{}

func busi(ch chan int) {

    for t := range ch {
        fmt.Println("go task = ", t, ", goroutine count = ", runtime.NumGoroutine())
        wg.Done()
    }
}

func sendTask(task int, ch chan int) {
    wg.Add(1)
    ch <- task
}

func main() {

    ch := make(chan int)   //无buffer channel

    goCnt := 3              //启动goroutine的数量
    for i := 0; i < goCnt; i++ {
        //启动go
        go busi(ch)
    }

    taskCnt := math.MaxInt64 //模拟用户需求业务的数量
    for t := 0; t < taskCnt; t++ {
        //发送任务
        sendTask(t, ch)
    }

	  wg.Wait()
}

6.GoRoutine sai normalmente

6.1 saída de notificação de fechamento do canal de dados

Adequado para tarefas simples. Para tarefas complexas, recomenda-se uma notificação de contexto separada.

// cancelFn 数据通道关闭通知退出
func cancelFn(dataChan chan int) {
    for {
        select {
        case val, ok := <-dataChan:
            // 关闭data通道时,通知退出
            // 一个可选是判断data=指定值时退出
            if !ok {
                log.Printf("Channel closed !!!")
                return
            }

            log.Printf("Revice dataChan %d\n", val)
        }
    }
}

6.2 saída de notificação de fechamento do canal de saída

Aplicável a alguns cenários simples

// exitChannelFn 单独退出通道关闭通知退出
func exitChannelFn(wg *sync.WaitGroup, taskNo int, dataChan chan int, exitChan chan struct{}) {
    defer wg.Done()

    for {
        select {
        case val, ok := <-dataChan:
            if !ok {
                log.Printf("Task %d channel closed !!!", taskNo)
                return
            }

            log.Printf("Task %d  revice dataChan %d\n", taskNo, val)

            // 关闭exit通道时,通知退出
        case <-exitChan:
            log.Printf("Task %d  revice exitChan signal!\n", taskNo)
            return
        }
    }

}

6.3Tempo limite de contexto ou saídas de notificação de cancelamento

Recomendação convencional

// contextCancelFn context取消或超时通知退出
func contextCancelFn(wg *sync.WaitGroup, taskNo int, dataChan chan int, ctx context.Context) {
    defer wg.Done()

    for {
        select {
        case val, ok := <-dataChan:
            if !ok {
                log.Printf("Task %d channel closed !!!", taskNo)
                return
            }

            log.Printf("Task %d  revice dataChan %d\n", taskNo, val)

        // ctx取消或超时,通知退出
        case <-ctx.Done():
            log.Printf("Task %d  revice exit signal!\n", taskNo)
            return
        }
    }

}

6.4WaitGroup/ErrGroup sai após julgar que todas as corrotinas estão fechadas

O mais comumente usado, consulte o seguinte

// 多个任务并行控制,等待所有任务完成
func TestTaskControl(t *testing.T) {
    dataChan := make(chan int)

    taskNum := 3

    wg := sync.WaitGroup{}
    wg.Add(taskNum)

    // 起多个协程,data关闭时退出
    for i := 0; i < taskNum; i++ {
        go func(taskNo int) {
            defer wg.Done()
            t.Logf("Task %d run\n", taskNo)

            for {
                select {
                case _, ok := <-dataChan:
                    if !ok {
                        t.Logf("Task %d notify to stop\n", taskNo)
                        return
                    }
                }
            }
        }(i)
    }

    // 通知退出
    go func() {
        time.Sleep(3 * time.Second)
        close(dataChan)
    }()

    // 等待退出完成
    wg.Wait()
}

7. A diferença entre marca e novo

Mesma
alocação de espaço de heap

Marca diferente
: usado apenas para inicialização de fatia, mapa e canal, insubstituível
novo: usado para alocação de memória de tipo (o valor de inicialização é 0), não comumente usado,
new não é comumente usado
, portanto, há uma função interna new, que pode alocar um pedaço de memória para nós. Nós o usamos, mas na codificação real, não é comumente usado. Geralmente usamos declarações curtas e literais de estrutura para atingir nossos objetivos, como:
i : =0
u := user{}
make é insubstituível.
Ao usar slice, map e channel, ainda temos que usar make. Inicialize-os antes de você pode operá-los.

8. Pool dinâmico de trabalhadores keep-alive

WorkerManager atua como a Goroutine principal e o trabalhador atua como a Goroutine filha

WorkerManager.go

type WorkerManager struct {
   //用来监控Worker是否已经死亡的缓冲Channel
   workerChan chan *worker
   // 一共要监控的worker数量
   nWorkers int
}

//创建一个WorkerManager对象
func NewWorkerManager(nworkers int) *WorkerManager {
   return &WorkerManager{
      nWorkers:nworkers,
      workerChan: make(chan *worker, nworkers),
   }
}

//启动worker池,并为每个Worker分配一个ID,让每个Worker进行工作
func (wm *WorkerManager)StartWorkerPool() {
   //开启一定数量的Worker
   for i := 0; i < wm.nWorkers; i++ {
      i := i
      wk := &worker{id: i}
      go wk.work(wm.workerChan)
   }

  //启动保活监控
   wm.KeepLiveWorkers()
}

//保活监控workers
func (wm *WorkerManager) KeepLiveWorkers() {
   //如果有worker已经死亡 workChan会得到具体死亡的worker然后 打出异常,然后重启
   for wk := range wm.workerChan {
      // log the error
      fmt.Printf("Worker %d stopped with err: [%v] \n", wk.id, wk.err)
      // reset err
      wk.err = nil
      // 当前这个wk已经死亡了,需要重新启动他的业务
      go wk.work(wm.workerChan)
   }
}

trabalhador.go

type worker struct {
   id  int
   err error
}

func (wk *worker) work(workerChan chan<- *worker) (err error) {
   // 任何Goroutine只要异常退出或者正常退出 都会调用defer 函数,所以在defer中想WorkerManager的WorkChan发送通知
   defer func() {
      //捕获异常信息,防止panic直接退出
      if r := recover(); r != nil {
         if err, ok := r.(error); ok {
            wk.err = err
         } else {
            wk.err = fmt.Errorf("Panic happened with [%v]", r)
         }
      } else {
         wk.err = err
      }
 
     //通知 主 Goroutine,当前子Goroutine已经死亡
      workerChan <- wk
   }()

   // do something
   fmt.Println("Start Worker...ID = ", wk.id)

   // 每个worker睡眠一定时间之后,panic退出或者 Goexit()退出
   for i := 0; i < 5; i++ {
      time.Sleep(time.Second*1)
   }

   panic("worker panic..")
   //runtime.Goexit()

   return err
}

3. Teste
main.go

func main() {
   wm := NewWorkerManager(10)

   wm.StartWorkerPool()
}

Descobriremos que se o Goroutine filho sair devido à exceção panic() ou à saída Goexit(), ele será monitorado e reiniciado pelo Goroutine principal. Principalmente, podemos desempenhar a função keep-alive.Claro, e se o thread morrer? Processo morreu? Como podemos garantir isso? Não se preocupe, nosso desenvolvimento com Go é na verdade baseado no agendador de Go. A morte do processo e dos níveis de thread causará a morte do agendador e então toda a nossa estrutura básica entrará em colapso. Então depende de como os threads e processos são mantidos ativos, o que está além do escopo do nosso desenvolvimento em Go.

9. Depuração de bugs/análise de desempenho

9.1 Comando de tempo integrado do shell

hora de executar test2.go

9.2 superior e GODEBUG/gctrace

package main

import (
    "log"
    "runtime"
    "time"
)

func test() {
    //slice 会动态扩容,用slice来做堆内存申请
    container := make([]int, 8)

    log.Println(" ===> loop begin.")
    for i := 0; i < 32*1000*1000; i++ {
        container = append(container, i)
    }
    log.Println(" ===> loop end.")
}

func main() {
    log.Println("Start.")

    test()

    log.Println("force gc.")
    runtime.GC() //强制调用gc回收

    log.Println("Done.")

    time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
}

$go build -o snippet_mem && ./snippet_mem
$top -p $(pidof snippet_mem)

GODEBUG='gctrace=1' ./snippet_mem
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB objetivo , # P
gc # O número de vezes do GC, incrementado em
@#s para cada GC. O tempo desde o início da execução do programa
#% A porcentagem do tempo de execução ocupado pelo GC
#+…+# O tempo usado pelo GC
#- >#-># MB de início, fim do GC e tamanho atual da memória heap ativa, unidade M
# MB meta de tamanho da memória heap global
# P Número de processadores usados

9.3pprof

import(
	"net/http"
	_ "net/http/pprof"
)

go func() {
	log.Println(http.ListenAndServe("0.0.0.0:10000", nil))
}()

Digite o endereço: http://127.0.0.1:10000/debug/pprof/heap?debug=1

10. Marcação GC/Varredura

GoV1.3- Marcação comum e método claro, o processo geral requer o início do STW, o que é extremamente ineficiente.
GoV1.5-Método de marcação de três cores, o espaço de heap inicia a barreira de gravação, mas o espaço de pilha não inicia.Depois de todas as varreduras, a pilha precisa ser varrida novamente (requer STW), a eficiência é GoV1.8- comum Método de marcação de três cores, mecanismo híbrido de barreira de gravação
. O espaço de pilha não está ativado, mas o espaço de heap está ativado. Todo o processo quase não requer STW e é altamente eficiente.

11. Estouro de memória

11.1 Por exemplo, map[string]*ObjectA está em um determinado []*ObjectB e não foi reciclado.

map[string]*ObjectA = nil pode evitar o problema de memória não recuperada.

11.2 Novo tipo de erro

创建一个新的类型
type ErrNegativeSqrt float64
并为其实现
func (e ErrNegativeSqrt) Error() string在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题:fmt.Sprint(float64(e))。

Chamar fmt.Sprint(e) dentro do método Error fará com que o programa caia em um loop infinito. Este problema pode ser evitado convertendo e primeiro: fmt.Sprint(float64(e)).

12. Vazamento de memória

12.1 Vazamento de memória causado por substring

var s0 string // a package-level variable

// A demo purpose function.
func f(s1 string) {
    s0 = s1[:50]
    // Now, s0 shares the same underlying memory block
    // with s1. Although s1 is not alive now, but s0
    // is still alive, so the memory block they share
    // couldn't be collected, though there are only 50
    // bytes used in the block and all other bytes in
    // the block become unavailable.
}

func demo() {
    s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
    f(s)
}

solução:

func f(s1 string) {
    s0 = string([]byte(s1[:50]))
}

func f(s1 string) {
    s0 = (" " + s1[:50])[1:]
}

import "strings"

func f(s1 string) {
    var b strings.Builder
    b.Grow(50)
    b.WriteString(s1[:50])
    s0 = b.String()
}

A desvantagem do terceiro método é que ele é um pouco detalhado. A boa notícia é que a partir de go1.12 podemos chamar a função strings.Repeat com um parâmetro de contagem 1 para clonar uma string. A partir de go1.12, a implementação subjacente da função strings.Repeat usará strings.Builder para evitar cópias desnecessárias.

12.2 Vazamentos de memória causados ​​por sub-slicing

var s0 []int

func g(s1 []int) {
    // Assume the length of s1 is much larger than 30.
    s0 = s1[len(s1)-30:]
}

Se quisermos evitar esse vazamento de memória, temos que copiar os 30 elementos de s0 para que a vivacidade de s0 não impeça a coleta do bloco de memória dos elementos s1.

func g(s1 []int) {
    s0 = append([]int(nil), s1[len(s1)-30:]...)
    // Now, the memory block hosting the elements
    // of s1 can be collected if no other values
    // are referencing the memory block.
}

12.3 Vazamento de memória causado pela não redefinição de ponteiros em elementos de fatia ausentes

No código abaixo, após chamar a função h, os blocos de memória alocados para o primeiro e o último elemento das fatias são perdidos.

func h() []*int {
    s := []*int{new(int), new(int), new(int), new(int)}
    // do something with s ...

    return s[1:3:3]
}

Enquanto a fatia retornada ainda existir, ela impedirá a coleta de quaisquer elementos de s, o que impede a coleta dos dois blocos de memória alocados para os dois valores int referenciados pelo primeiro e último elementos de s.
Se quisermos evitar esse tipo de vazamento de memória, devemos zerar o ponteiro armazenado no elemento ausente.

func h() []*int {
    s := []*int{new(int), new(int), new(int), new(int)}
    // do something with s ...

    // Reset pointer values.
    s[0], s[len(s)-1] = nil, nil
    return s[1:3:3]
}

12.4 Vazamento de memória causado por goroutine travada

Às vezes, algumas goroutines em um programa Go podem ser bloqueadas para sempre. Essas goroutines são chamadas de goroutines presas. O tempo de execução do Go não elimina goroutines suspensas, portanto, os recursos alocados para a goroutine suspensa (e os blocos de memória referenciados) nunca são coletados como lixo.
O tempo de execução do Go não elimina goroutines pendentes por dois motivos. Uma delas é que às vezes é difícil para o tempo de execução do Go saber se uma goroutine bloqueadora será permanentemente bloqueada. Outra coisa é que às vezes deixamos intencionalmente as goroutines travadas. Por exemplo, às vezes podemos deixar a goroutine principal de um programa Go travar para evitar a saída do programa.

12.5 Vazamento de memória causado por time.Ticker que não está mais em uso, mas não tem parada

Quando um valor time.Timer não for mais usado, ele será coletado como lixo após algum tempo. Mas isso não é verdade para um valor time.Ticker. Devíamos parar por um tempo. O valor da tag quando ela não estiver mais em uso.

12.6 Vazamentos de memória causados ​​pelo uso incorreto de finalizadores

Definir um finalizador em um valor que seja membro de um grupo de referência circular evita a coleta de todos os blocos de memória alocados para o grupo de referência circular. Este é um verdadeiro vazamento de memória.
Por exemplo, após a função a seguir ser chamada e encerrada, não é garantido que os blocos de memória alocados para x e y sejam coletados como lixo em coletas de lixo futuras.

func memoryLeaking() {
    type T struct {
        v [1<<20]int
        t *T
    }

    var finalizer = func(t *T) {
         fmt.Println("finalizer called")
    }

    var x, y T

    // The SetFinalizer call makes x escape to heap.
    runtime.SetFinalizer(&x, finalizer)

    // The following line forms a cyclic reference
    // group with two members, x and y.
    // This causes x and y are not collectable.
    x.t, y.t = &y, &x // y also escapes to heap.
}

Portanto, evite definir finalizadores para valores em grupos de referência circulares.
A propósito, não devemos usar finalizadores como destruidores de objetos.

13.Canal

Primeiro, vamos revisar quais são as características do Canal?
Enviar dados para um canal nulo, causando bloqueio permanente.
Receber dados de um canal nulo, causando bloqueio permanente.
Enviar dados para um canal fechado, causando pânico.
Receber dados de um canal fechado. Se o buffer estiver vazio, retorne um valor A de zero
significa que os canais sem buffer são síncronos, enquanto os canais com buffer são assíncronos.

As cinco características acima são coisas mortas, mas também podem ser memorizadas por meio de fórmulas:

"Leitura e gravação nula bloqueadas, gravação fechada com exceção, leitura fechada com zero nulo".

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	RightExample()
	ErrorExample()
}

func ErrorExample() {
	fmt.Println("ErrorExample")
	ch := make(chan int, 1000)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()
	go func() {
		for {
			a, ok := <-ch
			if !ok {
				fmt.Println("close")
				return
			}
			fmt.Println("a: ", a)
		}
	}()
	close(ch)
	fmt.Println("ok")
	time.Sleep(time.Second * 100)
}

var wg sync.WaitGroup = sync.WaitGroup{}

func RightExample() {
	fmt.Println("RightExample")
	ch := make(chan int, 1000)
	wg.Add(10)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()
	go func() {
		for {
			a, ok := <-ch
			if !ok {
				fmt.Println("close")
				return
			}
			fmt.Println("a: ", a)
			wg.Done()
		}
	}()

	wg.Wait()
	close(ch)
	fmt.Println("ok")
}

14.Cond.

1. Introdução
sync.Cond é uma variável de condição implementada com base no bloqueio mutex/bloqueio de leitura e gravação e é usada para coordenar aquelas Goroutines que desejam acessar recursos compartilhados. Quando o estado do recurso compartilhado muda, o sync.Cond pode ser usado para notificar a Goroutine que está bloqueada devido à ocorrência de condições de espera.

O sync.Cond é baseado no bloqueio mutex/bloqueio de leitura e gravação, então qual é a diferença entre ele e o bloqueio mutex?

O bloqueio mutex sync.Mutex geralmente é usado para proteger recursos críticos compartilhados, e a variável de condição sync.Cond é usada para coordenar Goroutines que desejam acessar recursos compartilhados. Quando o estado de um recurso compartilhado muda, o sync.Cond pode ser usado para notificar o Goroutine bloqueado.

2. Cenários de uso
O sync.Cond é frequentemente usado em cenários onde vários Goroutines estão aguardando e um Goroutine é notificado (o evento ocorre). Se for uma notificação ou uma espera, isso pode ser feito usando um mutex ou canal.

Vamos imaginar um cenário muito simples:

Uma corrotina está recebendo dados de forma assíncrona e as corrotinas restantes devem aguardar que essa corrotina receba os dados antes de poderem ler os dados corretos. Nesse caso, se você simplesmente usar chan ou um bloqueio mutex, apenas uma corrotina poderá esperar e ler os dados, e não há como notificar outras corrotinas para também lerem os dados.

Neste momento,uma variável global é necessária para marcar se a primeira corrotina concluiu a aceitação dos dados.As corrotinas restantes verificarão repetidamente o valor da variável até que os requisitos sejam atendidos. Ou crie vários canais, cada corrotina é bloqueada em um canal, e a corrotina que recebe os dados notificará um por um após o recebimento dos dados. Em suma, é necessária complexidade adicional para conseguir isso.

A linguagem Go possui um sync.Cond integrado na sincronização da biblioteca padrão para resolver esse tipo de problema.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	// 创建 cond
	cond := sync.NewCond(&mu)

	// 计数
	var count uint64

	// 报名表
	var stuSlice []int

	// 模拟学生报名参加课外活动
	for i := 0; i < 30; i++ {
		go func(i int) {
			cond.L.Lock()
			stuSlice = append(stuSlice, i)
			count++
			cond.L.Unlock()

			// Broadcast 唤醒所有等待此 cond 的 goroutine, Signal 只唤醒一个
			cond.Broadcast()
		}(i)
	}

	// 调用 Wait方法前, 调用者必须持有锁
	cond.L.Lock()
	for count != 30 {
		// 调用者被阻塞,并被放入 cond 的等待队列中
		cond.Wait()
	}
	cond.L.Unlock()

	fmt.Println(len(stuSlice), stuSlice)
}

Acho que você gosta

Origin blog.csdn.net/aaaadong/article/details/128835981
Recomendado
Clasificación