zoukankan      html  css  js  c++  java
  • Linux系统编程(28)——线程间同步


    多个线程同时访问共享数据时可能会冲突,这跟前面讲信号时所说的可重入性是同样的问题。比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:

    从内存读变量值到寄存器

    寄存器的值加1

    将寄存器的值写回内存

    假设两个线程在多处理器平台上同时执行这三条指令,则可能导致下图所示的结果,最后变量只加了一次而非两次

    我们通过一个简单的程序观察这一现象。上图所描述的现象从理论上是存在这种可能的,但实际运行程序时很难观察到,为了使现象更容易观察到,我们把上述三条指令做的事情用更多条指令来做:

                       val= counter;
                       printf("%x:%d
    ", (unsigned int)pthread_self(), val + 1);
                       counter= val + 1;


    我们在“读取变量的值”和“把变量的新值保存回去”这两步操作之间插入一个printf调用,它会执行write系统调用进内核,为内核调度别的线程执行提供了一个很好的时机。我们在一个循环中重复上述操作几千次,就会观察到访问冲突的现象。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
     
    #define NLOOP 5000
     
    int counter;                /* incremented by threads */
     
    void *doit(void *);
     
    int main(int argc, char **argv)
    {
             pthread_ttidA, tidB;
     
             pthread_create(&tidA,NULL, &doit, NULL);
             pthread_create(&tidB,NULL, &doit, NULL);
     
           /* wait for both threads to terminate */
             pthread_join(tidA,NULL);
             pthread_join(tidB,NULL);
     
             return0;
    }
     
    void *doit(void *vptr)
    {
             int    i, val;
     
             /*
              * Each thread fetches, prints, and incrementsthe counter NLOOP times.
              * The value of the counter should increasemonotonically.
              */
     
             for(i = 0; i < NLOOP; i++) {
                       val= counter;
                       printf("%x:%d
    ", (unsigned int)pthread_self(), val + 1);
                       counter= val + 1;
             }
     
             returnNULL;
    }


    我们创建两个线程,各自把counter增加5000次,正常情况下最后counter应该等于10000,但事实上每次运行该程序的结果都不一样,有时候数到5000多,有时候数到6000多。

    对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

    Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:

    #include <pthread.h>
     
    int pthread_mutex_destroy(pthread_mutex_t*mutex);
    int pthread_mutex_init(pthread_mutex_t*restrict mutex,
          const pthread_mutexattr_t *restrict attr);
    pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;

    返回值:成功返回0,失败返回错误号。

    pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省属性,本章不详细介绍Mutex属性,感兴趣的读者可以参考[APUE2e]。用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下列函数:

    #include <pthread.h>
     
    int pthread_mutex_lock(pthread_mutex_t*mutex);
    int pthread_mutex_trylock(pthread_mutex_t*mutex);
    int pthread_mutex_unlock(pthread_mutex_t*mutex);

    返回值:成功返回0,失败返回错误号。

    一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

    如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

    现在我们用Mutex解决先前的问题:

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
     
     
    #define NLOOP 5000
     
    int counter;                /* incremented by threads */
    pthread_mutex_t counter_mutex =PTHREAD_MUTEX_INITIALIZER;
     
    void *doit(void *);
     
    int main(int argc, char **argv)
    {
             pthread_ttidA, tidB;
     
             pthread_create(&tidA,NULL, doit, NULL);
             pthread_create(&tidB,NULL, doit, NULL);
     
           /* wait for both threads to terminate */
             pthread_join(tidA,NULL);
             pthread_join(tidB,NULL);
     
             return0;
    }
     
    void *doit(void *vptr)
    {
             int     i, val;
     
             /*
              * Each thread fetches, prints, and incrementsthe counter NLOOP times.
              * The value of the counter should increasemonotonically.
              */
     
             for(i = 0; i < NLOOP; i++) {
                       pthread_mutex_lock(&counter_mutex);
     
                       val= counter;
                       printf("%x:%d
    ", (unsigned int)pthread_self(), val + 1);
                       counter= val + 1;
     
                       pthread_mutex_unlock(&counter_mutex);
             }
     
             returnNULL;
    }

    这样运行结果就正常了,每次运行都能数到10000。


    那么挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。

    一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,有没有可能死锁的问题将会变得复杂和难以判断。

    写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用,以免死锁。

  • 相关阅读:
    Inno Setup区段之Dirs篇
    Inno Setup区段之Tasks篇
    leetcode刷题-69x的平方根
    7.27 判断子序列
    7.26 矩阵中的最长递增路径
    PMP | 备考笔记
    数据结构--数组存储二叉树(Java)
    数据结构--哈希表(Java)
    查找--斐波那契查找(Java)
    牛客网--字节跳动面试题--特征提取
  • 原文地址:https://www.cnblogs.com/new0801/p/6176974.html
Copyright © 2011-2022 走看看