Corutina de programación avanzada de red Linux [biblioteca Tencent Libco]

Historia : El concepto de corrutina se ha vuelto bastante popular en los últimos años. En particular, ir tras la llegada del lenguaje, construido características de corrutina, blindando por completo los complejos detalles del hilo del sistema operativo, incluso para ir a la Asociación de Desarrolladores sólo tienen saber conducir, me pregunto si hay un hilo "a . Por supuesto, C ++ y Java no se quedan atrás. Si ha seguido los últimos desarrollos en el lenguaje C ++ , es posible que haya notado que en los últimos años, la gente ha estado proponiendo soluciones de soporte de rutina al Comité de Estándares de C ++ ; Java también tiene un
Se proponen algunas soluciones experimentales.

1. ¿Qué es una corrutina?

En resumen, una corrutina es un hilo ligero [ solo espacio de pila independiente ]. Por supuesto, como se mencionó anteriormente, un hilo también es un proceso ligero en Linux. Por supuesto, no dije que una corrutina sea un proceso ligero. Proceso ligero, pero puedes entenderlo de esta manera. Solo se está ejecutando una corrutina en un hilo al mismo tiempo

 2. Tipos de corrutina 【Explicación del método de dibujo】

1. Corutina asimétrica (corutina en libco / BOOST)

El resumen es que después de que se produce la corrutina, la propiedad de la cpu solo puede pasarse al llamador [aquí el llamador es la corrutina principal].  Una corrutina asimétrica solo puede volver a la corrutina que la llamó originalmente.

Aunque yield solo puede decir que la propiedad de la CPU se le da a la persona que llama, resume puede decir que la ejecución de la CPU se le da a cualquier corrutina. 

2. Corutina simétrica (corutina en lenguaje go)

Es decir, no existe una relación de llamada no llamada entre las corrutinas y la ejecución paralela. En el lenguaje GO, las gorutinas también se pueden migrar en varios subprocesos.

Como hilos y procesos . Como se muestra

 Las corrutinas ejecutadas por subprocesos en GO también se implementan mediante algoritmos de equilibrio de carga, y el número de corrutinas ejecutadas por cada subproceso es similar.

 

3. Las características de la corrutina

1. La forma de sincronización [el código parece más claro], 2. Eficiencia asincrónica [eficiencia y epoll + grupo de subprocesos alguna pelea, prueba de la gran vaca]

 

4. Nuestra biblioteca Libco [biblioteca de corrutinas C / C ++ de Tencent], vamos a conocer la corrutina.

Introducimos el código de consumidor del productor a través de una versión de la versión de rutina

#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <queue>
#include "co_routine.h"

using namespace std;

static int Debug = 1;

struct stTask_t {
    int id;
};
struct stEnv_t {
    stCoCond_t* cond;
    queue<stTask_t*> task_queue;
};

void* Producer(void* args)
{
	co_enable_hook_sys();
	stEnv_t* env=  (stEnv_t*)args;
	int id = 0;
	while (true){
		stTask_t* task = (stTask_t*)calloc(1, sizeof(stTask_t));
		task->id = id++;
		env->task_queue.push(task);
		printf("%s:%d produce task %d\n", __func__, __LINE__, task->id);
		co_cond_signal(env->cond);
		poll(NULL, 0, 1000);
	}
	return NULL;
}
void* Consumer(void* args)
{
	co_enable_hook_sys();
	stEnv_t* env = (stEnv_t*)args;
	while (true){
		if (env->task_queue.empty()){
			co_cond_timedwait(env->cond, -1);
			continue;
		}
		stTask_t* task = env->task_queue.front();
		env->task_queue.pop();
		printf("%s:%d consume task %d\n", __func__, __LINE__, task->id);
		free(task);
	}
	return NULL;
}

int32_t main(int32_t argc, const char* argv[])
{
    stEnv_t* env = new stEnv_t;
    env->cond = co_cond_alloc();

    stCoRoutine_t* consumer_routine;
    co_create(&consumer_routine, NULL, Consumer, env);
    co_resume(consumer_routine);

    stCoRoutine_t* producer_routine;
    co_create(&producer_routine, NULL, Producer, env);
    co_resume(producer_routine);

    co_eventloop(co_get_epoll_ct(), NULL, NULL);

    return 0;
}
El prototipo de la función es el mismo que el prototipo de la función de subproceso pthread . La diferencia es que un co_enable_hook_sys () es también llamado en la primera línea de la función . Además, en lugar de utilizar el sueño () para esperar, poll () se introduce en el segundo capítulo .
efecto:

 

Una vez que se crea la corrutina de libco, se vincula al hilo en el momento de la creación. No se admite la migración entre distintos hilos.

1. Primero echemos un vistazo a la estructura de stCoRoutine. Esta estructura es similar a task_struct [estructura de PCB del hilo o proceso]

struct stCoRoutine_t {
      stCoRoutineEnv_t * env; // El entorno en el que se ejecuta la corrutina [ este entorno es el entorno de ejecución de todas las corrutinas que pertenecen al mismo hilo ]
      pfn_co_routine_t pfn; // es la función actual de corrutina que se ejecutará
      void * arg; // parámetros de función
      coctx_t ctx; //  ctx es una estructura de tipo coctx_t, utilizada para guardar el contexto de la CPU (el contexto es el registro, x86 15 registros) cuando se cambia la corrutina; el llamado contexto
      char cStart;        
      char cEnd;
      char cIsMain;
      char cEnableSysHook;
      char cIsShareStack;
      void * pvEnv; // El nombre parece un poco confuso, sabemos por el momento que se trata de un puntero para guardar las variables de entorno del sistema del programa.
       // char sRunStack [1024 * 128];
      stStackMem_t * stack_mem; // 
      // guarda el búfer satck mientras confilct en el mismo stack_buffer;
      char * stack_sp; //  
      unsigned int save_size; // 
      char * save_buffer; //  
      stCoSpec_t aSpec [1024];
};
Este stack_mem es la memoria de la pila cuando se ejecuta la corrutina. A través de la anotación, sabemos que la memoria de la pila tiene un tamaño fijo de 128 KB . Podemos calcular que si cada corrutina tiene 128K de memoria , entonces un proceso necesita hasta 122GB para iniciar 1 millón de corrutinas . Los lectores pueden preguntarse, ¿no oye a menudo que las corrutinas son muy ligeras y cómo pueden ocupar tanta memoria?

La respuesta está en char * stack_sp; unsigned int save_size; char * save_buffer; entre estos tres miembros

 
Hay dos tecnologías para implementar corrutinas apiladas (a diferencia de una corrutina sin apilamiento): pilas de corrutinas separadas y Copiar la pila (también llamada pila compartida). En términos de detalles de implementación, el primero asigna una pila separada de tamaño fijo para cada corrutina ; mientras que el segundo asigna memoria de pila solo para la corrutina en ejecución, y cuando la corrutina está programada para cambiar, usará la pila real La copia de la memoria se guarda en un búfer asignado por separado; cuando la rutina cortada se programa para su ejecución nuevamente, la copia nuevamente restaura la memoria de la pila originalmente guardada en el espacio de memoria de la pila de tamaño fijo compartido . En circunstancias normales, el espacio de pila realmente ocupado por una corrutina (desde esp hasta la parte inferior de la pila ) es mucho más pequeño que el tamaño de pila preasignado (como 128 KB en libco ) [ al igual que un proceso de subproceso, nadie aparece Asignado a usted 8M, 4G ]; de esta manera, la memoria ocupada por el esquema de implementación de la pila de copia será mucho menor. Por supuesto, la sobrecarga de copiar memoria al cambiar de rutina también es muy alta en algunos escenarios. Por lo tanto, los dos esquemas tienen sus propias ventajas y desventajas, y libco implementa ambos esquemas al mismo tiempo, el primero se usa por defecto y el usuario puede especificar la pila compartida al crear una corrutina.

 

struct stCoRoutineEnv_t {
        stCoRoutine_t * pCallStack [128]; // La pila de llamadas Esta pila es el gráfico asimétrico mencionado anteriormente
        int iCallStackSize; // Conoces el tamaño de la pila de llamadas mirando el nombre
        stCoEpoll_t * pEpoll; //    Introducido en capítulos posteriores
        // para copiar el registro de pila lastco y nextco
        stCoRoutine_t * pendiente_co;
        stCoRoutine_t * ocupy_co;
};
Pending_co y ocupy_co son punteros nulos cuando no se usa el modo de pila compartida
Siempre que se inicia una corrutina ( reanudación ), su puntero de estructura de bloque de control de corrutina stCoRoutine_t se guarda en la "parte superior" de pCallStack , luego el "puntero de pila" iCallStackSize se incrementa en 1 , y finalmente el contexto se cambia a esperando para ser iniciado
Se ejecuta la corrutina activa. Cuando la corrutina quiere ceder (ceder) la CPU, saca su stCoRoutine_t de pCallStack, reduce el "puntero de pila" iCallStackSize en 1, y luego cambia el contexto a la corrutina en la parte superior de la pila actual (el llamador suspendido original) para reanudar la ejecución [ Todos aquí ciertamente tienen una pregunta: ¿Por qué dibujo el diagrama de arriba no es la ejecución de la CPU en la parte superior actual de la corrutina 1 de la pila , sino la corrutina principal ? Debido a que la corrutina 1 ha sido bloqueada por cond o io (el bloqueo de la corrutina: bloqueo disfrazado - introducido más adelante ), el estado Stat de la corrutina se ha establecido, por lo que la corrutina 1 ha renunciado a los derechos de ejecución de la CPU y finalmente la entregó. La corrutina principal ]. Este proceso de "empujar" y "hacer estallar la pila" se discutirá nuevamente en las funciones co_resume () y co_yield () .

 

Inicio de la corrutina co_resume ( stCoRoutine_t * co) )

Echemos un vistazo a este cuerpo de función primero

void co_resume(stCoRoutine_t *co) { 
             stCoRoutineEnv_t *env = co->env;    
             stCoRoutine_t *lpCurrRoutine = env->pCallStack[env->iCallStackSize -1];   
             if (!co->cStart) { // 协程的状态
                     coctx_make(&co->ctx, (coctx_pfn_t)CoRoutineFunc, co, 0); 
                     co->cStart = 1; 
              } 
             env->pCallStack[env->iCallStackSize++] = co;  // 入栈
             co_swap(lpCurrRoutine, co);   // 重点
}

 

La suspensión del co_yield de la co-rutina ( stCoRoutine_t * co )

void co_yield_env(stCoRoutineEnv_t *env) { 
        stCoRoutine_t *last = env->pCallStack[env->iCallStackSize - 2]; 
         stCoRoutine_t *curr = env->pCallStack[env->iCallStackSize - 1]; 
         env->iCallStackSize−−; 
         co_swap(curr, last);  // 上一个栈顶协程的上下文和当前协程的上下文切换
}

 

co_swap: Puede que no esté familiarizado con el cambio de hilo. Cambio de hilo __switch Hay más detalles en esta función además del cambio de contexto

Echemos un vistazo a la implementación de la función [implementada en ensamblador, porque necesita manipular el contexto de la CPU (registro)], no entiendo lo que significa: de todos modos, es tomar el valor de registro de la corrutina actual y poner el valor de registro de la corrutina anterior Entra . De esta manera, se logra el propósito de las corrutinas de conmutación de CPU . Anteriormente, introdujimos que la CPU interrumpe los procesos de conmutación de acuerdo con el tiempo [ La PCB del proceso también registra los valores de registro y los punteros cuando el proceso se está ejecutando en la CPU ], no dibujaré aquí. 

 

Supongo que te gusta

Origin blog.csdn.net/qq_44065088/article/details/109272505
Recomendado
Clasificación