Grundlegende Probleme der gleichzeitigen Multithread-Programmierung

Dies ist ein altmodisches Thema, aber die meisten Diskussionen gehen schief.

Die überwiegende Mehrheit des Kerns der Diskussion besteht darin, eine Sperre zu entwerfen, um den Zugriff auf gemeinsam genutzte Variablen zu synchronisieren. Dies stellt den Wagen tatsächlich vor das Pferd:

  • Wir müssen eine Überführung entwerfen, keine Ampel!

Tatsächlich sollte die Multithread-Programmierung keinen Zugriff auf eine gemeinsam genutzte Variable haben. Wenn Sie wirklich auf gemeinsam genutzte Variablen in Multithread-Programmen zugreifen möchten, besteht die einzig wirksame Lösung darin, das Timing streng zu steuern. Nun, wer zuerst kommt, ist der einzige Weg. Das Design solcher Schlösser ist völlig faul, nur um Probleme zu vermeiden.

Bereits vor mehr als 100 Jahren war es möglich, verschiedene Sprachkanäle auf derselben Telefonleitung zu übertragen. Dies profitierte von der strengen Zeitschlitzzuweisung und dem Multiplexmechanismus. Später, im Laufe der Zeit, wurde es schlimmer. Dies ist ausschließlich auf einen anderen zurückzuführen Eine Art von Zeitschlitzmultiplex wird durch statistisches Zeitschlitzmultiplex verursacht. Moderne Betriebssysteme und moderne Paketvermittlungsnetzwerke sind treue Praktiker dieser Multiplexmethode.

Ich denke nicht, dass statistische Wiederverwendung ein effizienter Weg ist, sondern möglicherweise nur eine Lösung, die angesichts verschiedener Szenarien angewendet werden muss. Meiner Meinung nach gibt es nichts Besseres als striktes Zeitfenster-Multiplexing, wenn nur über hohe Effizienz gesprochen wird.

Lassen Sie mich ein Beispiel geben. 4 Threads greifen auf gemeinsam genutzte Variablen zu.

Schauen Sie sich zunächst einen etwas strengeren Plan an, bei dem die Zugriffsreihenfolge streng zugewiesen wird:

#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;

}

Dann betrachten wir den allgemeineren Ansatz, nämlich das Sperrschema:

#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();
}

Vergleichen Sie nun den Wirkungsgradunterschied zwischen den beiden:

[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

Entgegen Ihrer Intuition könnten Sie denken, dass das erste Beispiel zu einer seriellen Operation ausartet? Wären die Vorteile von Multiprozessoren nicht ungenutzt? Die zweite ist die richtige Haltung für die Multithread-Programmierung!

Tatsächlich muss für gemeinsam genutzte Variablen ohnehin seriell darauf zugegriffen werden. Diese Art von Code kann überhaupt nicht multithreaded werden. Daher die echte Multithread-Programmierung:

  • Stellen Sie sicher, dass Sie gemeinsam genutzte Variablen entfernen.
  • Wenn Sie Variablen gemeinsam nutzen müssen, müssen Sie das Zugriffs-Timing streng steuern, anstatt die Parallelität durch das Ergreifen von Sperren zu steuern.

Schauen wir uns nun den Linux-Kernel an. Eine große Anzahl von Spinlocks macht den Kernel nicht wirklich multithreaded, sondern nur zum Zweck von "Wenn Sie keine Spinlocks einführen, wird es Probleme geben ...".

RSS, Percpu Spinlock scheint der richtige Weg zu sein, aber es scheint nicht einfach zu sein, den Linux-Kernel zu serialisieren, der in ein Durcheinander von gemeinsam genutzten Variablen zerknittert wurde. Außerdem können Interrupts ihr Timing nicht steuern. Wie wäre es mit Threading-Interrupt-Verarbeitung? Es scheint, dass der Effekt nicht sehr gut ist.

Wenn Sie bei Problemen mit der Parallelitätseffizienz ein leistungsfähiges Schloss entwerfen, haben Sie das Problem zwar zugegeben, möchten es aber nicht lösen. Dies ist eine negative Antwort.

Lock, die Quelle allen Übels. Das Abbrechen gemeinsamer Variablen oder das Steuern des Timings ist die Wahrheit.

Was ist der Unterschied? Der Unterschied ist nur ein Anzug.


Die Lederschuhe in Wenzhou, Zhejiang, sind nass, damit sie im Regen nicht fett werden.

Ich denke du magst

Origin blog.csdn.net/dog250/article/details/108908750
Empfohlen
Rangfolge