linux多线程编程的基本规则

  • 准则1:不依赖于信号收发的设计。 
    • 给其它进程以及自己发送异步信号并改变处理流程的设计不要做。
    • 不要把信号和线程一起使用,这将使得程序动作的预测和调试变得困难。
  • 准则2:要知道信号处理函数中可以做哪些处理。 
    • 在sigaction()函数登记的信号处理函数中可以做的处理是被严格限定的。仅允许:
      • 局部变量的相关处理
      • volatile sig_atomic_t类型的全局变量操作
      • 调用异步信号安全的相关函数
  • volatile提示编译器对此变量不要进行优化处理,因为优化后代码可能导致程序语句的顺序混乱。sig_atomic_t类型变量表示只需要一条机器指令就可以执行完成,这样保证了操作的原子性。
  • 可重入函数。简单来说,就是允许该函数有多个副本同时运行;可以在函数执行的任意时刻中断它,而返回时不会出现错误。不可重入函数由于使用/正占有一些系统资源(如全局变量/静态变量等等),如被中断则可能出现问题(如死锁),因此不能运行在多线程环境下。printf是一个不可重入函数,因为printf函数内部调用了malloc函数,而malloc之前会利用静态mutex对其进行lock。设想这样一种情况,某线程在调用printf(内部执行至对静态mutex加锁操作)时中断,而在信号处理函数中同样调用了printf,则会产生死锁。
  • 信号处理函数中调用的函数必须是可重入函数。
  • 异步信号安全(async-signal-safe)函数是指在该函数内部即使因为信号而正在被中断,在其它地方该函数再次被调用时也不会有任何问题。不可重入函数就不是异步信号安全函数。
准则3:多线程程序里不准使用fork。 
  • 一般fork做如下事情:
    • 父进程的内存数据会原封不动地copy到子进程中。
    • 子进程在单线程状态下被生成。
  • fork导致子进程死锁的典型情景:
    • 父进程调用某函数foo,foo内部锁定了自己的mutex
    • fork,子进程copy了父进程的mutex
    • 子进程调用foo,此时子进程的mutex处于锁定状态,而它是父进程mutex的一个副本,与父进程的mutex没有关系了,没人可以解开它了。
    • 子进程再次锁定已经处于锁定状态的子进程mutex,导致死锁产生。
  • 如何规避灾难:
    • 多纯种程序里不使用fork
    • fork之后紧跟exec,使得子进程的内存状态被exec指定的命令所重置。
准则4:不要做线程的异步撤消的设计。
     异步撤消是指:某个线程的执行立刻被其它线程所终止了。
     请不要单单为了让设计更简单或者看起来更简单而使用异步撤消设计。
     一个异步撤消设计导致死锁的场景:
          线程1调用malloc正在做内存分配时,线程2异步撤消了线程1的执行。
          线程1马上被撤消,但malloc中的mutex互斥锁没有被解除。
          后面任意一个线程调用malloc都会导致死锁。
     如何避免这些问题?
          使用pthread_cleanup_push函数,登记异步撤消时的线程数据消除的回调函数。但其对延迟撤消不能调用。
          不要执行异步撤消处理。
准则5:尽可能避免线程的延迟撤消处理。 
     和异步撤消不同,延迟撤消的撤消处理会一直延迟到代码上明示的撤消点之后才会被执行。
     能否成为撤消点跟具体的函数实现也有关系,因此延迟撤消的自由度较高,需要考虑OS和C库版本、运行环境等等。
     若撤消点位于lock-unlock之间,则可能会导致死锁。为了回避这个问题,注意利用pthread_cleanup_push函数在撤消时释放掉互斥锁。
     C++与延迟撤消的兼容度非常差,在使用C++的工程里不使用延迟撤消还是比较实际的。
准则6:遵守多线程编程的常识。 
     多线程环境下,非线程安全的函数,一定不要使用。
     要让自己编写的函数符合线程安全:
          在访问共享数据之前一定要先锁定。
          如果C++的话,一定要注意函数的同步方法。
     线程安全函数,是像下面这样:
          不要操作局部的静态变量和全局变量,并且其它的非线程安全函数不要调用。
          要操作这样的变量的话,就要用mutex进行同步处理,来限制多个线程同时对其进行操作。

你可能感兴趣的:(多线程,线程编程)