Problemas básicos de la programación concurrente multiproceso

Este es un tema pasado de moda, pero la mayoría de las discusiones salen mal.

La gran mayoría del núcleo de la discusión es cómo diseñar un candado para sincronizar el acceso a las variables compartidas. En realidad, esto es poner el carro delante del caballo:

  • ¡Necesitamos diseñar un paso elevado, no diseñar un semáforo!

De hecho, la programación multiproceso no debería tener acceso a una variable compartida, si realmente desea acceder a variables compartidas en multiproceso, la única solución efectiva es controlar estrictamente el tiempo. Bueno, el primero en llegar es el único camino. En cuanto al diseño de tales cerraduras, es completamente vago, solo para evitar problemas.

Ya hace más de 100 años, era posible transmitir diferentes canales de voz en la misma línea telefónica. Esto se benefició del estricto mecanismo de asignación de franjas horarias y multiplexación. Más tarde, a medida que avanzaban los tiempos, las cosas empeoraron. Esto se debe completamente a otra Un tipo de multiplexación de intervalos de tiempo es causado por la multiplexación estadística de intervalos de tiempo. Los sistemas operativos modernos y las redes de conmutación de paquetes modernas son practicantes leales de este método de multiplexación.

No creo que la reutilización estadística sea una forma eficiente, puede que sea simplemente una solución que debe adoptarse ante diversos escenarios. En mi opinión, no hay nada mejor que la multiplexación de franjas horarias estrictas si solo se habla de alta eficiencia.

Déjame dar un ejemplo. 4 hilos acceden a variables compartidas.

Primero mire un plan un poco más estricto, asignando estrictamente el orden de acceso:

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

sem_t sem1;
sem_t sem2;
sem_t sem3;
sem_t sem4;

unsigned long cnt = 0;
#define TARGET	0xffffff

void do_work()
{
    
    
	int i;
	for(i = 0; i < TARGET; i++) {
    
    
		cnt ++;
	}
}

void worker_thread1(void)
{
    
    
	sem_wait(&sem1);
	do_work();
	sem_post(&sem2);
}

void worker_thread2(void)
{
    
    
	sem_wait(&sem2);
	do_work();
	sem_post(&sem3);
}

void worker_thread3(void)
{
    
    
	sem_wait(&sem3);
	do_work();
	sem_post(&sem4);
}

void worker_thread4(void)
{
    
    
	sem_wait(&sem4);
	do_work();
	printf("%lx\n", cnt);
	exit(0);
}

int main()
{
    
    
    pthread_t id1 ,id2, id3, id4;

    sem_init(&sem1, 0, 0);
    sem_init(&sem2, 0, 0);
    sem_init(&sem3, 0, 0);
    sem_init(&sem4, 0, 0);

	pthread_create(&id1, NULL, (void *)worker_thread1, NULL);
	pthread_create(&id2, NULL, (void *)worker_thread2, NULL);
    pthread_create(&id3, NULL, (void *)worker_thread3, NULL);
    pthread_create(&id4, NULL, (void *)worker_thread4, NULL);

	sem_post(&sem1);

	getchar();
	return 0;

}

Luego miramos el enfoque más común, a saber, el esquema de bloqueo:

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

pthread_spinlock_t spinlock;

unsigned long cnt = 0;
#define TARGET	0xffffff

void do_work()
{
    
    
	int i;
	for(i = 0; i < TARGET; i++) {
    
    
		pthread_spin_lock(&spinlock);
		cnt ++;
		pthread_spin_unlock(&spinlock);
	}
	if (cnt == 4*TARGET) {
    
    
		printf("%lx\n", cnt);
		exit(0);
	}
}

void worker_thread1(void)
{
    
    
	do_work();
}

void worker_thread2(void)
{
    
    
	do_work();
}

void worker_thread3(void)
{
    
    
	do_work();
}

void worker_thread4(void)
{
    
    
	do_work();
}

int main()
{
    
    
    pthread_t id1 ,id2, id3, id4;

	pthread_spin_init(&spinlock, 0);

	pthread_create(&id1, NULL, (void *)worker_thread1, NULL);
	pthread_create(&id2, NULL, (void *)worker_thread2, NULL);
	pthread_create(&id3, NULL, (void *)worker_thread3, NULL);
    pthread_create(&id4, NULL, (void *)worker_thread4, NULL);
    
	getchar();
}

Ahora compare la diferencia de eficiencia entre los dos:

[root@localhost linux]# time ./pv
3fffffc

real	0m0.171s
user	0m0.165s
sys	0m0.005s
[root@localhost linux]# time ./spin
3fffffc

real	0m4.852s
user	0m19.097s
sys	0m0.035s

Contrariamente a su intuición, ¿podría pensar que el primer ejemplo degenera en una operación en serie? ¿No se aprovecharían las ventajas de los multiprocesadores? ¡El segundo es la postura correcta para la programación multiproceso!

De hecho, para las variables compartidas, debe accederse en serie de todos modos. Este tipo de código no puede ser multiproceso en absoluto. Por lo tanto, la programación multiproceso real:

  • Asegúrese de eliminar las variables compartidas.
  • Si tiene que compartir variables, debe controlar estrictamente el tiempo de acceso en lugar de controlar la concurrencia mediante la captura de bloqueos.

Ahora echemos un vistazo al kernel de Linux. Una gran cantidad de spinlocks realmente no hacen que el kernel sea multiproceso, sino únicamente con el propósito de "Si no introduce spinlocks, habrá problemas ..."

RSS, percpu spinlock parece ser la forma correcta de manejarlo, pero no parece fácil serializar el kernel de Linux que se ha arrugado en un lío de variables compartidas Además, las interrupciones no pueden controlar su tiempo. ¿Qué tal el procesamiento de interrupciones de subprocesos? Parece que el efecto no es muy bueno.

En el caso de problemas de eficiencia de concurrencia, si diseña una cerradura potente, de hecho ha admitido el problema pero no quiere resolverlo, esta es una respuesta negativa.

Lock, la fuente de todos los males. Cancelar variables compartidas o controlar el tiempo es la verdad.

¿Entonces cuál es la diferencia? La diferencia es solo un traje.


Los zapatos de cuero en Wenzhou, Zhejiang están mojados, por lo que no engordan con la lluvia.

Supongo que te gusta

Origin blog.csdn.net/dog250/article/details/108908750
Recomendado
Clasificación