七叶笔记 » golang编程 » linux多线程并发编程的一些本质问题

linux多线程并发编程的一些本质问题

Linux 多线程相关视频解析:

正文

这是个老掉牙的话题,但基本上绝大多数的讨论都跑偏了。

绝大多数讨论的核心在于 如何设计一把锁来同步共享变量的访问。 这事实上完全是本末倒置:

  • 我们需要设计的一个立交桥,而不是为了设计一个红绿灯!
  • 事实上,多线程编程就不应该访问共享变量,如果真的要在多线程访问共享变量,唯一高效的方案就是 严格控制时序。 嗯,先来后到是唯一的方法。至于说设计这样那样的锁,那完全是惰政,只是为了防止出问题而已。

    早在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 TARGET0xffffff
    
     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;
    
    }
    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869  

    然后我们看看更加普遍的做法,即加锁的方案:

     #include <stdio.h>
    #include <pthread.h>
    
    pthread_spinlock_t spinlock;
    
    unsigned long cnt = 0;
    #define TARGET0xffffff
    
    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();
    }
    12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455  

    【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

    现在比较一下二者的效率差异:

     [ root @localhost linux]# time ./pv
    3fffffc
    
    real0m0.171s
    user0m0.165s
    sys0m0.005s
    [root@localhost linux]# time ./spin
    3fffffc
    
    real0m4.852s
    user0m19.097s
    sys0m0.035s  

    和直觉相反,也许你会觉得,第一个例子那不就是退化成串行操作了吗?多处理器的优势岂不是无法发挥了?第二个才是多线程编程正确的姿势啊!

    事实上,对于共享变量,无论如何它都是必须要串行访问的。这种代码根本就无法多线程化。所以,真正的多线程程序设计:

  • 一定要消除共享变量。
  • 若非要共享变量,那就一定严格控制访问时序,而不是靠抢锁来控制并发。
  • 现在来看看 Linux内核 ,大量的spinlock并不是真的让内核多线程化了,而纯粹是为了 “如果不引入spinlock就会出问题…”

    RSS,percpu spinlock似乎是正确的把式,但若要将已经被揉成乱麻的Linux内核彻底抽丝剥茧般的共享变量串行化,似乎并不容易,更何况,中断是无法控制其时序的,那么中断处理线程化如何呢?似乎效果也不是很好。

    遇到并发效率问题,如果你去设计一把牛逼的锁,那事实上你是承认了问题的所在但并不想去解决问题,这是一种消极的应对。

    锁,万恶之源。取消共享变量或者控制时序才是真理。

    相关文章