這是個老掉牙的話題,但基本上絕大多數的討論都跑偏了。
絕大多數討論的核心在於 如何設計一把鎖來同步共用變數的存取。 這事實上完全是本末倒置:
事實上,多執行緒程式設計就不應該存取共用變數,如果真的要在多執行緒存取共用變數,唯一高效的方案就是 嚴格控制時序。 嗯,先來後到是唯一的方法。至於說設計這樣那樣的鎖,那完全是惰政,只是為了防止出問題而已。
早在100多年前,就可以在同一根電話線上傳輸不同的話路,這得益於嚴格的時隙分配和複用機制,後來時代進步了,事情反而變得糟糕了,這完全是由於另一種時隙複用方式引起的,那便是時隙統計複用。現代作業系統和現代分組交換網是這種複用方式的忠實踐行者。
我並不認為統計複用是一種高效的方式,它也許只是面對多樣化場景而不得不採用的一種方案。在我看來,若單單討論高效,沒有什麼比嚴格的時隙複用更好的了。
我來舉一個例子。4個執行緒存取共用變數。
首先看一個稍微嚴格的方案,嚴格分配存取順序:
#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;
}
然後我們看看更加普遍的做法,即加鎖的方案:
#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();
}
現在比較一下二者的效率差異:
[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
和直覺相反,也許你會覺得,第一個例子那不就是退化成序列操作了嗎?多處理器的優勢豈不是無法發揮了?第二個才是多執行緒程式設計正確的姿勢啊!
事實上,對於共用變數,無論如何它都是必須要序列存取的。這種程式碼根本就無法多執行緒化。所以,真正的多執行緒程式設計:
現在來看看Linux核心,大量的spinlock並不是真的讓核心多執行緒化了,而純粹是為了 「如果不引入spinlock就會出問題…」
RSS,percpu spinlock似乎是正確的把式,但若要將已經被揉成亂麻的Linux核心徹底抽絲剝繭般的共用變數序列化,似乎並不容易,更何況,中斷是無法控制其時序的,那麼中斷處理執行緒化如何呢?似乎效果也不是很好。
遇到並行效率問題,如果你去設計一把牛逼的鎖,那事實上你是承認了問題的所在但並不想去解決問題,這是一種消極的應對。
鎖,萬惡之源。取消共用變數或者控制時序才是真理。
那麼,差別是什麼?差別只是一套西服。
浙江溫州皮鞋溼,下雨進水不會胖。