El grupo de corrutinas implementado mediante el uso de la cola desbloqueada es simple pero no simple

Prefacio

Como todos sabemos, la creación, destrucción y programación de corrutinas de golang son muy livianas, pero incluso si son livianas, el costo no se puede ignorar cuando la escala es grande. Por ejemplo, al usar una corrutina para procesar solicitudes http, cada solicitud es procesada por una corrutina, cuando el QPS es de decenas de miles, el consumo de recursos sigue siendo relativamente grande.

El grupo de corrutinas es el mismo que el grupo de subprocesos, el grupo se llena de corrutinas calientes, que se sacan cuando se necesitan y se devuelven después de su uso, evitando la creación y destrucción de alta frecuencia. Al mismo tiempo, el grupo de corrutinas también puede destruir las corrutinas de tiempo de espera inactivo para liberar recursos, y tiene ciertas capacidades de protección Al establecer el número máximo de corrutinas, la creación interminable de corrutinas puede evitar agotar los recursos del sistema.

En resumen, aunque golang proporciona un entorno de programación de corrutinas muy ligero y fácil de usar, los diferentes escenarios de aplicación tienen diferentes requisitos para el uso de corrutinas.El grupo de corrutinas es un escenario de aplicación muy común.

Cola sin bloqueo

Antes de presentar la implementación del grupo de corrutinas, debemos explicar brevemente la cola sin bloqueos. Hay muchos artículos sobre la implementación de la cola sin bloqueos en Internet. A continuación, se incluyen algunos contenidos clave relacionados con la implementación de este artículo. : Operación CAS-Compare & Set, o Compare & Swap, casi todas las instrucciones de la CPU ahora admiten operaciones atómicas CAS, y X86 corresponde a las instrucciones de ensamblaje CMPXCHG. Con esta operación atómica, puede usarla para implementar varias estructuras de datos sin bloqueo.Este artículo usa atomic.CompareAndSwapPointer.

lograr

La conexión de código fuente abierto implementada en este artículo es: https://github.com/jindezgm/gopool.git , y luego ingrese el modo CAAD (código como documento), ¡todo el artículo solo tiene código y comentarios!

interfaz

// Pool定义了协程池的接口
type Pool interface {
    Name() string             // 获取协程池的名字,当有多个协程池对象的时候可以用名字区分不同的协程池
    Capacity() int32          // 获取协程池的容量,即最大协程数量
    Tune(size int32)          // 修改协程池的容量
    Status() Status           // 获取协程池的状态,关于状态下面有定义
    Go(Routine) error         // 执行(阻塞)Routine,关于Routine下面也有定义
    GoNonblock(Routine) error // 非阻塞执行Routine,当协程数量达到最大值且无空闲协程时立刻返回
    Close()                   // 关闭协程池
}

// Routine定义了协程池执行的函数,context避免协程池关闭的时候协程被阻塞,也就是说协程池的使用者
// 需要将函数实现成Routine形式才能被协程池调用,
type Routine func(context.Context)

// Status定义了协程池的状态
type Status struct {
    Runnings int32 // 运行中的协程数量
    Idles    int32 // 空闲的协程数量
}

Coroutine

// coroutine 定义了协程
type coroutine struct {
    rc     chan Routine  // Routine的chan,Pool.Go(Routine)通过rc传递给协程执行
    pool   *pool         // 协程池指针,每个协程通过pool指向协程池(pool是Pool的实现)
    active time.Time     // 活跃时间,最后一次执行完Routine的时间,用于清理空闲超时的协程
    next   *coroutine    // 下一个协程,所谓无锁队列就是用这个变量将协程形成了队列
}

// run是协程的运行函数
func (c *coroutine) run() {
    // 此处只需要知道pool有一个sync.WaitGroup的成员变量wg,用来等待所有协程退出,
    // 所以协程退出的时候需要调动Done
    defer c.pool.wg.Done()

    // 前面提到了,通过chan Routine获取函数
    for r := range c.rc {
        // 空指针表示协程需要退出,比如协程池关闭或者协程空闲超时都会收到nil
        if r == nil {
            return
        }

        // 执行Routine,此处传入了协程池的context,建议Routine的实现select该context
        r(c.pool.ctx)

        // 执行完函数,将该协程放到协程池的空闲队列,此处开始进入本文的核心内容了
        c.pool.pushIdle(c)
    }
}

Cola sin bloqueo

// pushIdle把协程放入空闲协程队列
func (p *pool) pushIdle(c *coroutine) {
    // 此时协程已经执行完Routine,需要记录一下最后的活跃时间
    c.active = time.Now()
    for {
        // 获取空闲队列的第一个协程,即队列头,clean表示协程池是否正在清理空闲队列
        head, clean := p.idleHead()
        if clean {
            // 如果协程池正在清理空闲协程,需要等清理完毕后再把协程放入到空闲队列中,
            // 如何才能知道协程池清理完了呢?chan或者sync.Cond应该是比较容易想到的方案,
            // 笔者采用了自旋的方案,因为清理空闲协程非常快且不频繁,自旋是性能最好的方法。
            // 此处使用了runtime.Gosched()实现自旋,此时立查询清理是否完成多半还是在清理中,
            // 倒不如把时间片让出来给其他协程,实在没事干了再去查询清理状态会更有效的利用CPU。
            // runtime.Gosched()会让协程释放CPU时间片,笔者此处问一个问题,如果不调用该函数,
            // 采用死循环的方式自旋查询清理状态(即把runtime.Gosched()注释掉)是否可行,
            // 答案是不行的,原因读者应该能够想明白。
            runtime.Gosched()
            continue
        }

        // 到这里说明协程池不在清理状态,c.storeNext(head)是用c.next->head(当前),
        // p.casIdleHead(head, unsafe.Pointer(c))利用CAS操作实现p.idles->c,
        // 相当于把c放入了队列头,c.next指向了以前的队列头。因为CAS是原子操作,无需用锁互斥
        // 就可以把协程放入队列,这也是无锁队列的由来
        if c.storeNext(head); p.casIdleHead(head, unsafe.Pointer(c)) {
            // 运行中的协程数量-1,通过原子操作计数,因为执行上面是多个协程并发执行的
            // 此处需要注意,在清理超时协程的时候会插入cleaning协程,不能计为运行中的协程
            if c != cleaning {
                atomic.AddInt64(&p.count, int64(-1)<<32)
            }
            break
        }
    }
}

// casIdleHead利用CAS实现协程池头指针的操作,casIdleHead不仅可以实现插入协程到队列头,
// 同时可以将队列头协程弹出,详情见下面的popIdle()
func (p *pool) casIdleHead(o, n unsafe.Pointer) bool {
    // 实现非常简单,就是利用了atomic.CompareAndSwapPointer()函数,p.idles指向了第一个协程,
    // 目标是让p.idles指向n,o是以前的队列头,CAS就是如果p.idles==o则p.idles=n,否则返回false
    return atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&p.idles)), o, n)
}

// popIdle弹出队列的第一个协程
func (p *pool) popIdle() *coroutine {
    for {
        // 和插入队列一样,都要判断是否为清理状态,此处就不多解释了。因为队列可能为空,
        // 所以要判断是否为空
        head, cleaning := p.idleHead()
        if nil == head {
            return nil
        } else if cleaning {
            runtime.Gosched()
            continue
        }

        // 下面的操作让p.idles指向head.next,等同于将head从队列中移除
        c := (*coroutine)(head)
        if next := c.loadNext(); p.casIdleHead(head, next) {
            // 返回队列头部的协程,不难发现协程队列其实是个栈(FILO),而队列应该是FIFO,
            // 其实是栈还是队列并不重要,重要的是无锁队列是一个广为人知的名字,熟悉无锁队列
            // 的读者可以立刻想象到本文所描述的实现方案。
            c.storeNext(nil)
            return c
        }
    }
}

var cleaning = &coroutine{}

// idleHead返回队列第一个协程,即队列头
func (p *pool) idleHead() (unsafe.Pointer, bool) {
    // p.idles指向了第一个协程,用原子的方式读取队列头,因为多个协程都在操作p.idles实现
    // 队列的push和pop操作
    head := atomicLoadCoroutine(&p.idles)
    // 这句就是本文标题中简约而不简单的部分了,cleaning是全局变量,上面有定义,如果队列头指向
    // cleaning表示协程池正在执行清理函数。那么问题来了,为什么要用这种方式?因为所有的协程
    // 都在用CAS的方式操作队列头,也就是说只有队列头实现了全局状态的一致性,但凡引入任何其他变量,
    // 都无法通过原子的方式同时操作队列和该变量,此时就必须要加锁,这不是笔者想要的。有的同学可能
    // 会问,空闲协程已经通过队列的方式组织起来了,直接遍历不就完了?答案肯定是不行的,因为遍历
    // 队列中的任何一个协程都需要多个操作,当判断协程超时的时候可能已经被调度了新的Routine,此时又
    // 要重头开始。前面也提到了,多协程并发的操作队列头,队列的前排是状态变化最频繁的,遍历队列的
    // 过程中可能一直在前排绕圈,因为他们一直都在变化。而在队列头部插入一个特殊的协程,那么所有
    // 操作队列头的协程(清理协程除外)都会进入等待状态(就是前面提到的自旋),直到这个特殊的协程
    // 被弹出。
    return head, cleaning == (*coroutine)(head)
}

Limpiar

// 终于到了清理函数了,来看看清理函数有没有前面提到的非常快
func (p *pool) clean(now time.Time) {
    // 经过无锁队列章节的说明,这句就非常好理解了,把cleaning这个特殊的协程放入队列头部
    p.pushIdle(cleaning)

    // 从cleaning.next开始遍历,cleaning.next就是pushIdle之前的队列头,此处需要注意,
    // from是c的前一个协程,即from.next==c.
    // 需要了解一点,下面这个for循环相当于只有一个清理协程在工作,其他的协程都在自旋状态,
    // 理论上可以不用原子操作。
    for from, c := cleaning, cleaning.next; nil != c; from, c = c, c.next {
        // 这个应该不用解释了,判断空闲时间是不是超时?
        if now.Sub(c.active) >= p.IdleTimeout {
            // from之后的所有协程全部被删除,为什么?前面提到过,协程池的数据结构是栈,越
            // 靠后面的协程是越先被插入,也就是空闲的时间越长,所以只要某一个协程超时,那么
            // 该协程后面的所有协程肯定都超时。
            from.storeNext(nil)

            // 遍历所有超时协程并通知退出
            var count int32
            for c != nil {
                c.rc <- nil
                c, c.next = c.next, nil
                count++
            }

            // 从协程的总数中减去已经退出的协程数量
            atomic.AddInt64(&p.count, -int64(count))
            break
        }
    }

    // 把cleaning协程从队列中弹出,恢复状态,不难看出,清理协程的函数最多就是遍历一次所有空闲
    // 协程,总体来看是比较快的
    atomicStoreCoroutine(&p.idles, unsafe.Pointer(cleaning.next))
}

El grupo de corrutinas implementado con colas sin bloqueo es simple pero no simple

Prefacio

Como todos sabemos, la creación, destrucción y programación de corrutinas de golang son muy livianas, pero incluso si son livianas, el costo no se puede ignorar cuando la escala es grande. Por ejemplo, al usar una corrutina para procesar solicitudes http, cada solicitud es procesada por una corrutina, cuando el QPS es de decenas de miles, el consumo de recursos sigue siendo relativamente grande.

El grupo de corrutinas es el mismo que el grupo de subprocesos, el grupo se llena de corrutinas calientes, que se sacan cuando se necesitan y se devuelven después de su uso, evitando la creación y destrucción de alta frecuencia. Al mismo tiempo, el grupo de corrutinas también puede destruir las corrutinas de tiempo de espera inactivo para liberar recursos, y tiene ciertas capacidades de protección Al establecer el número máximo de corrutinas, la creación interminable de corrutinas puede evitar agotar los recursos del sistema.

En resumen, aunque golang proporciona un entorno de programación de corrutinas muy ligero y fácil de usar, los diferentes escenarios de aplicación tienen diferentes requisitos para el uso de corrutinas.El grupo de corrutinas es un escenario de aplicación muy común.

Cola sin bloqueo

Antes de presentar la implementación del grupo de corrutinas, debemos explicar brevemente la cola sin bloqueos. Hay muchos artículos sobre la implementación de la cola sin bloqueos en Internet. A continuación, se incluyen algunos contenidos clave relacionados con la implementación de este artículo. : Operación CAS-Compare & Set, o Compare & Swap, casi todas las instrucciones de la CPU ahora admiten operaciones atómicas CAS, y X86 corresponde a las instrucciones de ensamblaje CMPXCHG. Con esta operación atómica, puede usarla para implementar varias estructuras de datos sin bloqueo.Este artículo usa atomic.CompareAndSwapPointer.

lograr

El código fuente y la conexión de fuente abierta implementados en este artículo es: https://github.com/jinde-zgm/gopool.git , y luego ingrese el modo CAAD (código como documento), ¡todo el artículo solo tiene código y comentarios! Necesito explicar que el código fuente abierto está escrito en mi tiempo libre por mi interés y no incluye ningún contenido relacionado con el trabajo.

interfaz

// Pool定义了协程池的接口
type Pool interface {
    Name() string             // 获取协程池的名字,当有多个协程池对象的时候可以用名字区分不同的协程池
    Capacity() int32          // 获取协程池的容量,即最大协程数量
    Tune(size int32)          // 修改协程池的容量
    Status() Status           // 获取协程池的状态,关于状态下面有定义
    Go(Routine) error         // 执行(阻塞)Routine,关于Routine下面也有定义
    GoNonblock(Routine) error // 非阻塞执行Routine,当协程数量达到最大值且无空闲协程时立刻返回
    Close()                   // 关闭协程池
}

// Routine定义了协程池执行的函数,context避免协程池关闭的时候协程被阻塞,也就是说协程池的使用者
// 需要将函数实现成Routine形式才能被协程池调用,
type Routine func(context.Context)

// Status定义了协程池的状态
type Status struct {
    Runnings int32 // 运行中的协程数量
    Idles    int32 // 空闲的协程数量
}

Coroutine

// coroutine 定义了协程
type coroutine struct {
    rc     chan Routine  // Routine的chan,Pool.Go(Routine)通过rc传递给协程执行
    pool   *pool         // 协程池指针,每个协程通过pool指向协程池(pool是Pool的实现)
    active time.Time     // 活跃时间,最后一次执行完Routine的时间,用于清理空闲超时的协程
    next   *coroutine    // 下一个协程,所谓无锁队列就是用这个变量将协程形成了队列
}

// run是协程的运行函数
func (c *coroutine) run() {
    // 此处只需要知道pool有一个sync.WaitGroup的成员变量wg,用来等待所有协程退出,
    // 所以协程退出的时候需要调动Done
    defer c.pool.wg.Done()

    // 前面提到了,通过chan Routine获取函数
    for r := range c.rc {
        // 空指针表示协程需要退出,比如协程池关闭或者协程空闲超时都会收到nil
        if r == nil {
            return
        }

        // 执行Routine,此处传入了协程池的context,建议Routine的实现select该context
        r(c.pool.ctx)

        // 执行完函数,将该协程放到协程池的空闲队列,此处开始进入本文的核心内容了
        c.pool.pushIdle(c)
    }
}

Cola sin bloqueo

// pushIdle把协程放入空闲协程队列
func (p *pool) pushIdle(c *coroutine) {
    // 此时协程已经执行完Routine,需要记录一下最后的活跃时间
    c.active = time.Now()
    for {
        // 获取空闲队列的第一个协程,即队列头,clean表示协程池是否正在清理空闲队列
        head, clean := p.idleHead()
        if clean {
            // 如果协程池正在清理空闲协程,需要等清理完毕后再把协程放入到空闲队列中,
            // 如何才能知道协程池清理完了呢?chan或者sync.Cond应该是比较容易想到的方案,
            // 笔者采用了自旋的方案,因为清理空闲协程非常快且不频繁,自旋是性能最好的方法。
            // 此处使用了runtime.Gosched()实现自旋,此时立查询清理是否完成多半还是在清理中,
            // 倒不如把时间片让出来给其他协程,实在没事干了再去查询清理状态会更有效的利用CPU。
            // runtime.Gosched()会让协程释放CPU时间片,笔者此处问一个问题,如果不调用该函数,
            // 采用死循环的方式自旋查询清理状态(即把runtime.Gosched()注释掉)是否可行,
            // 答案是不行的,原因读者应该能够想明白。
            runtime.Gosched()
            continue
        }

        // 到这里说明协程池不在清理状态,c.storeNext(head)是用c.next->head(当前),
        // p.casIdleHead(head, unsafe.Pointer(c))利用CAS操作实现p.idles->c.next,
        // 相当于把c放入了队列头,c.next指向了以前的队列头。因为CAS是原子操作,无需用锁互斥
        // 就可以把协程放入队列,这也是无锁队列的由来
        if c.storeNext(head); p.casIdleHead(head, unsafe.Pointer(c)) {
            // 运行中的协程数量-1,通过源自操作计数,因为执行上面是多个协程并发执行的
            // 此处需要注意,在清理超时协程的时候回插入cleaning协程,不能计为运行中的协程
            if c != cleaning {
                atomic.AddInt64(&p.count, int64(-1)<<32)
            }
            break
        }
    }
}

// casIdleHead利用CAS实现协程池头指针的操作,casIdleHead不仅可以实现插入协程到队列头,
// 同时可以将队列头协程弹出,详情见下面的popIdle()
func (p *pool) casIdleHead(o, n unsafe.Pointer) bool {
    // 实现非常简单,就是利用了atomic.CompareAndSwapPointer()函数,p.idles指向了第一个协程,
    // 目标是让p.idles指向n,o是以前的队列头,CAS就是如果p.idles==o则p.idles=n,否则返回false
    return atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&p.idles)), o, n)
}

// popIdle弹出队列的第一个协程
func (p *pool) popIdle() *coroutine {
    for {
        // 和插入队列一样,都要判断是否为清理状态,此处就不多解释了。因为队列可能为空,
        // 所以要判断是否为空
        head, cleaning := p.idleHead()
        if nil == head {
            return nil
        } else if cleaning {
            runtime.Gosched()
            continue
        }

        // 下面的操作让p.idles指向head.next,等同于将head从队列中移除
        c := (*coroutine)(head)
        if next := c.loadNext(); p.casIdleHead(head, next) {
            // 返回队列头部的协程,不难发现协程队列其实是个栈(FILO),而队列应该是FIFO,
            // 其实是栈还是队列并不重要,重要的是无锁队列是一个广为人知的名字,熟悉无锁队列
            // 的读者可以立刻想象到本文所描述的实现方案。
            c.storeNext(nil)
            return c
        }
    }
}

var cleaning = &coroutine{}

// idleHead返回队列第一个协程,即队列头
func (p *pool) idleHead() (unsafe.Pointer, bool) {
    // p.idles指向了第一个协程,用原子的方式读取队列头,因为多个协程都在操作p.idles实现
    // 队列的push和pop操作
    head := atomicLoadCoroutine(&p.idles)
    // 这句就是本文标题中简约而不简单的部分了,cleaning是全局变量,上面有定义,如果队列头指向
    // cleaning表示协程池正在执行清理函数。那么问题来了,为什么要用这种方式?因为所有的协程
    // 都在用CAS的方式操作队列头,也就是说只有队列头实现了全局状态的一致性,但凡引入任何其他变量,
    // 都无法通过原子的方式同时操作队列和该变量,此时就必须要加锁,这不是笔者想要的。有的同学可能
    // 会问,空闲协程已经通过队列的方式组织起来了,直接遍历不就完了?答案肯定是不行的,因为遍历
    // 队列中的任何一个协程都需要多个操作,当判断协程超时的时候可能已经被调度了新的Routine,此时又
    // 要重头开始。前面也提到了,多协程并发的操作队列头,队列的前排是状态变化最频繁的,遍历队列的
    // 过程中可能一直在前排绕圈,因为他们一直都在变化。而在队列头部插入一个特殊的协程,那么所有
    // 操作队列头的协程(清理协程除外)都会进入等待状态(就是前面提到的自旋),直到这个特殊的协程
    // 被弹出。
    return head, cleaning == (*coroutine)(head)
}

Limpiar

// 终于到了清理函数了,来看看清理函数有没有前面提到的非常快
func (p *pool) clean(now time.Time) {
    // 经过无锁队列章节的说明,这句就非常好理解了,把cleaning这个特殊的协程放入队列头部
    p.pushIdle(cleaning)

    // 从cleaning.next开始遍历,cleaning.next就是pushIdle之前的队列头,此处需要注意,
    // from是c的前一个协程,即from.next==c.
    // 需要了解一点,下面这个for循环相当于只有一个清理协程在工作,其他的协程都在自旋状态,
    // 理论上可以不用原子操作。
    for from, c := cleaning, cleaning.next; nil != c; from, c = c, c.next {
        // 这个应该不用解释了,判断空闲时间是不是超时?
        if now.Sub(c.active) >= p.IdleTimeout {
            // from之后的所有协程全部被删除,为什么?前面提到过,协程池的数据结构是栈,越
            // 靠后面的协程是越先被插入,也就是空闲的时间越长,所以只要某一个协程超时,那么
            // 该协程后面的所有协程肯定都超时。
            from.storeNext(nil)

            // 遍历所有超时协程并通知退出
            var count int32
            for c != nil {
                c.rc <- nil
                c, c.next = c.next, nil
                count++
            }

            // 从协程的总数中减去已经退出的协程数量
            atomic.AddInt64(&p.count, -int64(count))
            break
        }
    }

    // 把cleaning协程从队列中弹出,恢复状态,不难看出,清理协程的函数最多就是遍历一次所有空闲
    // 协程,总体来看是比较快的
    atomicStoreCoroutine(&p.idles, unsafe.Pointer(cleaning.next))
}

Vamos

// 无论是Go还是GoNonblock,最终调用的都是goRoutine,无非是nonblocking是true还是false
func (p *pool) goRoutine(r Routine, nonblocking bool) error {
    // 如果协程池不在运行状态,返回协程池已关闭错误
    if !p.state.is(stateRunning) {
        return ErrPoolClosed
    }

    // 从空闲队列中弹出第一个协程
    var c *coroutine
    for c = p.popIdle(); nil == c; c = p.popIdle() {
        // 无空闲协程,就需要创建新的协程了,前提条件是协程数量没有超过最大值,
        if count := atomic.LoadInt64(&p.count); int32(count) >= p.Capacity() {
            // 如果协程总量已经达到最大值,如果是nonblock则直接返回协程满错误
            if nonblocking {
                return ErrPoolFull
            }
            
            // 否则自旋的方式再尝试获取空闲协程
            runtime.Gosched()
            
            // atomic.CompareAndSwapInt64(&p.count, count, count+1)就是协程总数+1,
            // 下面的语句如果执行失败,说明其他人抢在前面创建或者有新的空闲协程,因为协程
            // 计数发生变化,需要重新循环判断
        } else if atomic.CompareAndSwapInt64(&p.count, count, count+1) {
            // 创建新协程,此处cache是sync.Pool,可以避免频繁的申请和释放内存
            c = p.cache.Get().(*coroutine)
            // 创建了新协程,wg就要+1
            p.wg.Add(1)
            go c.run()
            break
        }
    }

    // 增加运行中的协程计数并把Routine传给协程
    atomic.AddInt64(&p.count, int64(1)<<32)
    c.rc <- r
    return nil
}

para resumir

El anterior es el código clave del grupo de corrutinas implementado por la cola sin bloqueo. Los otros códigos son principalmente para funciones auxiliares. No los explicaré uno por uno aquí. Si tiene alguna pregunta, comuníquese con el autor. Primero haz un resumen simple:

  1. La cola sin candados es solo un pronombre, es una pila sin candados en realidad;
  2. ¿Por qué es un puntero unidireccional (siguiente) en lugar de un puntero bidireccional (anterior, siguiente), porque CAS solo puede operar en un puntero, los estudiantes familiarizados con LevelDB deben saber que MemTable usa una lista de omisión (SkipList) en lugar de un mapa, la razón es saltar El puntero de la tabla también es un puntero unidireccional LevelDB usa tecnología de barrera de memoria en lugar de CAS, lo que evita las operaciones de bloqueo, porque el mapa no es seguro para los subprocesos;
  3. Toda espera es en realidad girando, incluida la espera de limpieza y la espera de corrutinas inactivas. Parece que no hay bloqueo, pero en realidad es un bloqueo de giro.

De hecho, hay una deficiencia en el esquema de este artículo, y es el giro. No hay nada esperando a que se limpie el centrifugado, después de todo, la limpieza es muy rápida y el ciclo de limpieza es mucho más largo que el ciclo de programación de la corrutina. El autor se refiere al giro en espera de corrutinas inactivas. Cuando el grupo de corrutinas está lleno y todas las corrutinas en ejecución están bloqueadas por ciertos eventos, todas las solicitudes en espera de las corrutinas inactivas están en la cola de consultas de giro, lo que equivale a inactivo. En este momento, la tasa de uso de la CPU debería ser muy baja, pero debido a estas corrutinas giratorias, la tasa de uso de la CPU es muy alta, pero esto no tendrá ningún impacto en el programa, siempre y cuando se active cualquier rutina, darán hasta el segmento de tiempo de la CPU. En pocas palabras, estas corrutinas en espera usan giros de CPU que otros no usan, aunque hay algunos arrepentimientos para el autor perfeccionista, son aceptables.

El autor ha realizado una prueba sencilla y el rendimiento de programación del esquema de grupo de corrutinas mencionado en este artículo sigue siendo relativamente alto (un 10% más alto que el de las hormigas). En cuanto a para qué se puede usar el grupo de corrutinas, el método de usar el grupo de corrutinas en la programación de tareas a gran escala se presentará en el futuro, así que estad atentos.

Supongo que te gusta

Origin blog.csdn.net/weixin_42663840/article/details/106244941
Recomendado
Clasificación