Experiencia de aprendizaje de programación multiproceso de Linux

  Tabla de contenido



Prefacio

¿Por qué tener hilos?
        Si ahora eres propietario de una fábrica, hay una línea de producción en la fábrica. Ahora la oferta supera la demanda y es necesario ampliar la escala de producción. Desde una perspectiva de proceso, simplemente creo otra fábrica y copio las líneas de producción anteriores, pero ¿qué pasa si agrego directamente algunas líneas de producción a la fábrica original? Entonces, ¿la escala de esta expansión debería ser mucho menor? Este método es el método del hilo.




1. Introducción a los subprocesos de Linux

1. Procesos e hilos

        Se puede considerar que un proceso típico de UNIX/Linux tiene un solo hilo de control: un proceso solo hace una cosa al mismo tiempo. Con múltiples subprocesos de control, el proceso se puede diseñar para hacer más de una cosa al mismo tiempo durante el diseño del programa, y ​​cada subproceso maneja tareas independientes. Un proceso es una instancia de un programa durante la ejecución y es la unidad básica responsable de asignar los recursos del sistema (tiempo de CPU, memoria, etc.). En un sistema diseñado para subprocesos, el proceso en sí no es la unidad de ejecución básica, sino un contenedor de subprocesos. El programa en sí es solo una descripción de instrucciones, datos y su organización, y el proceso es la instancia real en ejecución del programa (esas instrucciones y datos).

        Un hilo es la unidad más pequeña que el sistema operativo puede realizar en la programación de operaciones. Está incluido en el proceso y es la unidad operativa real en el proceso. Un subproceso se refiere a un único flujo de control secuencial en un proceso. Se pueden ejecutar varios subprocesos al mismo tiempo en un proceso y cada subproceso realiza diferentes tareas en paralelo. El subproceso contiene la información necesaria para representar el entorno de ejecución dentro del proceso, incluido el ID del subproceso que representa el subproceso en el proceso, un conjunto de valores de registro, pila, estrategia y prioridad de programación, palabras de máscara de señal, constantes errno y datos privados del subproceso. . Toda la información sobre un proceso es compartida por todos los subprocesos del proceso, incluido el texto del programa ejecutable, la memoria global y la memoria dinámica del programa, la pila y los descriptores de archivos.

"Proceso - la unidad más pequeña de asignación de recursos, hilo - la unidad más pequeña de ejecución del programa"

        El proceso tiene un espacio de direcciones independiente. Después de que un proceso falla, no afectará a otros procesos en modo protegido y los subprocesos son simplemente rutas de ejecución diferentes en un proceso. Los subprocesos tienen sus propias pilas y variables locales, pero los subprocesos no tienen espacios de direcciones separados. La muerte de un subproceso equivale a la muerte de todo el proceso. Por lo tanto, los programas multiproceso son más robustos que los programas multiproceso, pero Consume más recursos al cambiar de proceso. Más grande, menos eficiente. Sin embargo, para algunas operaciones concurrentes que requieren la operación simultánea y el uso compartido de ciertas variables, solo se pueden usar subprocesos, no procesos.

2. Razones para usar hilos

        La diferencia entre procesos y subprocesos. De hecho, estas diferencias son las razones por las que usamos subprocesos. En general, el proceso tiene un espacio de direcciones independiente y el subproceso no tiene un espacio de direcciones independiente (los subprocesos en el mismo proceso comparten el espacio de direcciones del proceso).

Una de las razones         para utilizar subprocesos múltiples es que es una forma muy "frugal" de realizar múltiples tareas en comparación con los procesos. Sabemos que bajo un sistema Linux, al iniciar un nuevo proceso, se le debe asignar un espacio de direcciones independiente y establecer numerosas tablas de datos para mantener su segmento de código, segmento de pila y segmento de datos, este es un proceso multitarea "costoso". Manera de trabajar. Varios subprocesos que se ejecutan en un proceso utilizan el mismo espacio de direcciones entre sí y comparten la mayoría de los datos. El espacio necesario para iniciar un subproceso es mucho menor que el espacio necesario para iniciar un proceso. Además, los subprocesos cambian entre sí. El tiempo requerido también es mucho menor que el tiempo requerido para cambiar entre procesos.

La segunda razón         para utilizar subprocesos múltiples es el conveniente mecanismo de comunicación entre subprocesos. Para diferentes procesos, tienen espacios de datos independientes y los datos solo se pueden transferir a través de la comunicación, lo que no solo lleva mucho tiempo, sino que también es muy inconveniente. Este no es el caso de los subprocesos: dado que el espacio de datos se comparte entre subprocesos en el mismo proceso, los datos de un subproceso pueden ser utilizados directamente por otros subprocesos, lo cual no solo es rápido, sino también conveniente. Por supuesto, el intercambio de datos también trae algunos otros problemas. Algunas variables no pueden ser modificadas por dos subprocesos al mismo tiempo. Algunos datos declarados como estáticos en subrutinas tienen más probabilidades de provocar golpes catastróficos en programas multiproceso. Esto es correcto. Esto es Lo más importante a lo que hay que prestar atención al escribir programas multiproceso.

En comparación con los procesos, los programas multiproceso, como método de trabajo multitarea y concurrente, ciertamente tienen las siguientes ventajas:

  • Mejorar la capacidad de respuesta de las aplicaciones. Esto es especialmente significativo para los programas de interfaz gráfica. Cuando una operación lleva mucho tiempo, todo el sistema esperará la operación. En este momento, el programa no responderá a las operaciones del teclado, el mouse y el menú. El uso de tecnología de subprocesos múltiples tomar mucho tiempo Colocar la operación (que requiere mucho tiempo) en un nuevo hilo puede evitar esta situación embarazosa.
  • Haga que los sistemas de múltiples CPU sean más eficientes. El sistema operativo garantizará que cuando la cantidad de subprocesos no sea mayor que la cantidad de CPU, se ejecuten diferentes subprocesos en diferentes CPU.
  • Mejorar la estructura del programa. Un proceso largo y complejo puede dividirse en múltiples subprocesos y convertirse en varias partes en ejecución independientes o semiindependientes, lo que hará que dicho programa sea más fácil de entender y modificar.





2. Descripción general de la API de desarrollo de subprocesos en Linux

 El desarrollo de subprocesos múltiples ya es compatible con la biblioteca pthread madura en la plataforma Linux. Los conceptos más básicos involucrados en el desarrollo de subprocesos múltiples incluyen principalmente tres puntos: subprocesos, bloqueos mutex y condiciones. Entre ellas, las operaciones de subprocesos se dividen en tres tipos: creación de subprocesos, salida y espera. Las cerraduras Mutex incluyen 4 operaciones: creación, destrucción, bloqueo y desbloqueo. Hay 5 tipos de operaciones condicionales: crear, destruir, activar, transmitir y esperar.

Consulte la siguiente tabla para obtener más detalles:

1. API relacionada con el hilo mismo 

        1. Creación de hilos       

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
// 返回:若成功返回0,否则返回错误编号

        Cuando pthread_create regresa con éxito, la unidad de memoria a la que apunta tidp se establece en el ID del subproceso recién creado. El parámetro attr se utiliza para personalizar varios atributos de hilo. Puede configurarlo temporalmente en NULL para crear un hilo con atributos predeterminados.

        El hilo recién creado comienza a ejecutarse desde la dirección de la función start_rtn, que tiene solo un parámetro de puntero sin tipo arg. Si necesita pasar más de un parámetro a la función start_rtn, debe colocar estos parámetros en una estructura y luego pasar la dirección de esta estructura como parámetro arg, que puede permitir que el subproceso secundario opere.

void *func1(void *arg)
int param = 100;
pthread_t t1;
ret = pthread_create(&t1,NULL,func1,(void *)&param);

        2. Salidas del hilo

        Un único hilo puede salir de tres maneras, deteniendo su flujo de control sin terminar todo el proceso:

  1) El hilo simplemente regresa de la rutina de inicio y el valor de retorno es el código de salida del hilo.

  2) Los subprocesos pueden ser cancelados por otros subprocesos en el mismo proceso.

  3) El hilo llama a pthread_exit.

#include <pthread.h>
int pthread_exit(void *rval_ptr);
//rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。
//进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

        3. Hilo en espera

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回:若成功返回0,否则返回错误编号

        El hilo que llama a esta función se bloqueará hasta que el hilo especificado llame a pthread_exit y regrese de la rutina de inicio. Si la rutina simplemente regresa de su rutina de inicio, rval_ptr contendrá el código de retorno.

        Los subprocesos se pueden colocar automáticamente en un estado desconectado llamando a pthread_join para que se puedan restaurar los recursos. Si el hilo ya está en un estado desconectado, la llamada pthread_join fallará y devolverá EINVAL.

        Si no está interesado en el valor de retorno del hilo, puede configurar rval_ptr en NULL. En este caso, llamar a la función pthread_join esperará a que finalice el subproceso especificado, pero no obtendrá el estado de terminación del subproceso.

  4. Desprendimiento del hilo

        Un hilo se puede unir (predeterminado) o desconectarse. Cuando finaliza un subproceso que se puede unir, su ID de subproceso y su estado de salida se conservan hasta que otro subproceso llame a pthread_join en él. Los subprocesos desconectados son como demonios: cuando terminan, todos los recursos relacionados se liberan y no podemos esperar a que terminen. Si un hilo necesita saber cuándo termina otro hilo, es mejor mantener la cita del segundo hilo.

La función pthread_detach cambia el hilo especificado al estado desconectado.

#include <pthread.h>
int pthread_detach(pthread_t thread);
// 返回:若成功返回0,否则返回错误编号

Esta función suele ser utilizada por un hilo que quiere separarse, como en la siguiente declaración:

pthread_detach(pthread_self());

        5. Adquisición y comparación de ID de hilo

#include <pthread.h>
pthread_t pthread_self(void);
// 返回:调用线程的ID

 Para la comparación de ID de subprocesos, para operaciones portátiles, no podemos simplemente tratar los ID de subprocesos como números enteros, porque diferentes sistemas pueden definir los ID de subprocesos de manera diferente. Deberíamos utilizar la siguiente función:

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回:若相等则返回非0值,否则返回0

  Para programas de subprocesos múltiples, a menudo necesitamos sincronizar estos subprocesos múltiples. La sincronización se refiere a permitir que solo un hilo acceda a un determinado recurso dentro de un cierto período de tiempo. Durante este tiempo, otros subprocesos no pueden acceder al recurso. Podemos sincronizar recursos mediante mutex, variable de condición y bloqueo lector-escritor.

2. API relacionada con el bloqueo mutex

        Un mutex es esencialmente un bloqueo: el mutex se bloquea antes de acceder a un recurso compartido y el bloqueo del mutex se libera una vez completado el acceso. Después de bloquear el mutex, cualquier otro subproceso que intente bloquear el mutex nuevamente será bloqueado hasta que el subproceso actual libere el bloqueo mutex. Si se bloquean varios subprocesos cuando se libera el mutex, todos los subprocesos bloqueados en el mutex se volverán ejecutables. El primer subproceso que se vuelva ejecutable puede bloquear el mutex y otros subprocesos Verá que el mutex todavía está bloqueado y solo puede regresa y espera a que vuelva a estar disponible. De esta manera, sólo se puede ejecutar un hilo a la vez.

        Las variables mutex están representadas por el tipo de datos pthread_mutex_t. Una variable mutex debe inicializarse antes de usarla. Puede configurarla en la constante PTHREAD_MUTEX_INITIALIZER (solo para mutex asignados estáticamente), o puede inicializarla llamando a la función pthread_mutex_init. Si el mutex se asigna dinámicamente (por ejemplo, llamando a la función malloc), es necesario llamar a pthread_mutex_destroy antes de liberar la memoria.     

 pthread_mutex_init(&mutex,NULL);
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  1. Crear y destruir bloqueos mutex


#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号

        Para inicializar un mutex con atributos predeterminados, simplemente establezca attr en NULL.

        2. Bloqueo y desbloqueo

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t mutex);
int pthread_mutex_unlock(pthread_mutex_t mutex);
int pthread_mutex_trylock(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号

        Si un hilo no quiere ser bloqueado, puede usar pthread_mutex_trylock para intentar bloquear el mutex. Si el mutex está en un estado desbloqueado cuando se llama a pthread_mutex_trylock, entonces pthread_mutex_trylock bloqueará el mutex sin bloquearlo y devolverá 0. De lo contrario, pthread_mutex_trylock fallará y no podrá bloquear el mutex y devolverá EBUSY.

        El uso de un bloqueo mutex causará el bloqueo. Sólo cuando se usan dos bloqueos, se producirá el estado de bloqueo. Si se usa un bloqueo, no ocurrirá. Cuando está bloqueado, el programa no puede continuar ejecutándose hacia adelante. El proceso de bloqueo es aproximadamente el siguiente: el hilo A adquiere el bloqueo 1, duerme durante 1 segundo, adquiere el bloqueo 2, el hilo B adquiere el bloqueo 2, duerme durante 1 segundo y adquiere el bloqueo 1.   

3. API relacionada con variables de condición

        

        Las variables de condición son otro mecanismo de sincronización disponible para subprocesos. Las variables de condición proporcionan un lugar para que se encuentren varios subprocesos. Las variables de condición, cuando se usan con mutex, permiten que los subprocesos esperen a que ocurra una condición específica sin carreras.

  La condición en sí está protegida por un mutex. El subproceso primero debe bloquear el mutex antes de cambiar el estado de la condición. Otros subprocesos no notarán este cambio hasta que obtengan el mutex, porque el mutex debe bloquearse antes de que se pueda evaluar la condición.

  Las variables de condición deben inicializarse antes de su uso. La variable de condición representada por el tipo de datos pthread_cond_t se puede inicializar de dos maneras. La constante PTHREAD_COND_INITIALIZER se puede asignar a la variable de condición asignada estáticamente, pero si la variable de condición se asigna dinámicamente, puede usar la Función pthread_cond_destroy para condicionar la variable de condición. Las variables se desinicializan.

        1. Las condiciones de creación y destrucción cambian.

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t cond);
// 返回:若成功返回0,否则返回错误编号

A menos que necesite crear una variable de condición con atributos no predeterminados, el parámetro attr de la función pthread_cont_init se puede establecer en NULL.

        2. Espera

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, cond struct timespec *restrict timeout);
// 返回:若成功返回0,否则返回错误编号

  pthread_cond_wait espera a que una condición se cumpla. Si la condición no se cumple dentro del tiempo determinado, se genera una variable de retorno que representa un código de error. El mutex pasado a pthread_cond_wait protege la condición y la persona que llama pasa el mutex bloqueado a la función. La función coloca el hilo que llama en la lista de hilos que esperan la condición y luego desbloquea el mutex. Ambas operaciones son operaciones atómicas. Esto cierra el canal de tiempo entre la verificación de la condición y el hilo se va a dormir para esperar a que cambie la condición, de modo que el hilo no pierda ningún cambio en la condición. Cuando pthread_cond_wait regresa, el mutex se bloquea nuevamente.

  La función pthread_cond_timedwait funciona de manera similar a la función pthread_cond_wait, excepto que hay un tiempo de espera más. timeout especifica el tiempo de espera, que se especifica a través de la estructura timespec.

        3. Disparador

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t cond);
int pthread_cond_broadcast(pthread_cond_t cond);
// 返回:若成功返回0,否则返回错误编号

        Estas dos funciones se pueden utilizar para notificar a los subprocesos que se han cumplido las condiciones. La función pthread_cond_signal despertará un hilo que espera la condición, y la función pthread_cond_broadcast despertará todos los procesos que esperan la condición.

  Tenga en cuenta que debe enviar una señal al hilo después de cambiar el estado condicional.

2. Ejemplo

Ejemplo 1: Crear un hilo simple : El siguiente programa crea un hilo simple t1, que es responsable de imprimir la variable parámetro en el hilo.

#include <stdio.h>

#include <pthread.h>

*func1(void *arg)
{
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));


}

int main()
{

        int ret;
        int param = 100;
        pthread_t t1;
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());

        return 0;

}

Resultados de ejecución del programa:

Obviamente, el proceso de este programa no ejecuta el hilo t1.

  La razón específica es que existe competencia entre el hilo principal y el nuevo hilo: el hilo principal necesita esperar a que el nuevo hilo termine de ejecutarse antes de que el hilo principal salga. Si el hilo principal no espera, puede salir, por lo que todo el proceso finalizará antes de que el nuevo hilo tenga la oportunidad de ejecutarse. Este comportamiento depende de la implementación de subprocesos del sistema operativo y de los métodos de programación.

Ejemplo 2: Para resolver el problema anterior, introduzca pthread_join(t1,NULL) en el hilo principal; el propósito es que el hilo principal entre en el estado de bloqueo después de ejecutarse aquí para esperar a que el nuevo hilo termine de ejecutarse.

#include <stdio.h>
#include <pthread.h>

void *func1(void *arg)
{
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));


}

int main()
{

        int ret;
        int param = 100;
        pthread_t t1;
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());


        pthread_join(t1,NULL);

        return 0;

}
 





Los resultados de ejecutar el programa son los siguientes: 

 Ejemplo 3: crear un hilo y llamar a pthread_exit((void *)&ret); con valor de retorno

#include <stdio.h>

#include <pthread.h>

void *func1(void *arg)
{
        static int ret = 10;
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));

        pthread_exit((void *)&ret);
}

int main()
{
        int *pret;

        int ret;
        int param = 100;
        pthread_t t1;
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());


        pthread_join(t1,(void **)&pret);
        printf("main:ti quit:%d\n",*pret);
        return 0;

}
   

resultado de la operación:

 También están disponibles cadenas de valor de retorno.

Ejemplo 4: crear varios hilos

#include <stdio.h>

#include <pthread.h>

void *func1(void *arg)
{
        static int ret = 10;
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));

        pthread_exit((void *)&ret);
}

void *func2(void *arg)
{
        static int ret = 10;
        printf("t2:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t2:paaram is %d\n",*((int *)arg));

        pthread_exit((void *)&ret);

}
int main()
{
        int *pret;

        int ret;
        int param = 100;
        pthread_t t1;
        pthread_t t2;

        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        ret = pthread_create(&t2,NULL,func2,(void *)&param);
        if(ret == 0){

                printf("main:creat t2 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());


        pthread_join(t1,(void **)&pret);
        pthread_join(t2,(void **)&pret);
        printf("main:ti quit:%d\n",*pret);
        return 0;

}

resultado de la operación:

 A juzgar por los resultados de la ejecución, los resultados de las dos ejecuciones son diferentes, lo que indica que hay competencia entre los dos subprocesos, lo que nos hace inconveniente dejar que el subproceso se ejecute primero. La forma de resolver este fenómeno es introducir un bloqueo mutex. .

Ejemplo 5: Deje que el subproceso 1 se ejecute y salga después de contar hasta 3. Se recomienda utilizar el bloqueo mutex junto con las condiciones. El siguiente ejemplo no es muy adecuado.

#include <stdio.h>

#include <pthread.h>

pthread_mutex_t mutex;
int g_data = 0;

void *func1(void *arg)
{
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));


        pthread_mutex_lock(&mutex);
        while(1){
                printf("t1:%d\n",g_data++);
                sleep(1);
                if(g_data == 3){
                        pthread_mutex_unlock(&mutex);
                        pthread_exit(NULL);
                }
        }
}
void *func2(void *arg)
{

        printf("t2:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t2:paaram is %d\n",*((int *)arg));

        while(1){
                printf("t2:%d\n",g_data);
                pthread_mutex_lock(&mutex);
                g_data++;
                pthread_mutex_unlock(&mutex);
               sleep(1);
        }
}

int main()
{
      
        int ret;
        int param = 100;
        pthread_t t1;
        pthread_t t2;
        pthread_mutex_init(&mutex,NULL);
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        ret = pthread_create(&t2,NULL,func2,(void *)&param);
        if(ret == 0){

                printf("main:creat t2 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());

        while(1){

                printf("t2:%d\n",g_data);

                sleep(1);
        }
        pthread_join(t1,NULL);
        pthread_join(t2,NULL);
        pthread_mutex_destroy(&mutex);
        return 0;
}                                                                        





Resumir

Libro de referencia "Programación avanzada en entorno UNIX"

Referencia: un estudio preliminar sobre la programación multiproceso de Linux - Fengzi_Looking up to the sunshine - Blog Garden (cnblogs.com)

Lo anterior es mi experiencia de aprendizaje sobre los subprocesos de Linux. Tengan paciencia conmigo si soy un novato. Si les resulta útil, denle un me gusta y apoyen.

Supongo que te gusta

Origin blog.csdn.net/qq_44848795/article/details/122012253
Recomendado
Clasificación