第十四章 多线程编程

第十四章 多线程编程


14.1 Linux线程概述

线程的实现方式可分为三种模式:

  1. 完全在用户空间实现.
  2. 完全由内核实现.
  3. 双层调度(two level scheduler).

关于每一点的解释:

  1. 无须内核的支持,内核甚至根本不知道这些线程的存在
    优点有
    1)创建和调度线程都无须内核的干预,因此速度相当快.
    2)不占用额外的内核资源,所以创建了很多线程不会有很明显的影响;
    缺点是
    1)由于内核是按照其最小调度单位来分配的CPU,所以一个进程的多个进程无法运行在不同CPU上.
    2)线程的优先级只对同一个进程中的线程有效,比较不同进程中的优先级没有意义.

  2. 优缺点和"完全在用户空间"交换,现代Linux内核已经大大增强了对线程的支持,完全有内核调度的这种实现方式满足M:N=1:1,即一个用户空间线程被映射为一个内核线程.

  3. 1和2的混合体,内核调度M个内个线程,线程库调度N个用户线程.线程切换速度快,同时可以充分利用多个处理器的优势.

自Linux内核2.6以来,提供了真正的内核线程,之前有过"管理线程"的概念,但这增加了额外的系统开销.也有过"用进程模拟内核线程"的概念,但会有许多与线程所要求概念不同的语义问题.

现在主要使用的是NPTL(Native POSIX Thread Library),优势有:

  • 内核线程不再是一个进程.

  • 摒弃了管理线程,终止,回收线程堆栈都可由内核完成.

  • 可运行在不同CPU上.

  • 线程同步由内核完成.

Linux上有名的线程库是LinuxThreads和NPTL,它们都是采用1:1的方式实现的.


14.2 创建线程和结束线程

线程的相关操作如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

void pthread_exit(void *retval);

int pthread_join(pthread_t thread, void **retval);

int pthread_cancel(pthread_t thread);

// 目标线程可以决定是否允许取消以及如何取消.
int pthread_setcancelstate(int state, int *oldstate);

int pthread_setcanceltype(int type, int *oldtype);

pthread_attr_t 结构体定义了一套完整的线程属性,而且线程库定义了一系列函数来操作pthread_attr_t类型的变量,以方便我们获取和设置线程属性.

int pthread_attr_init(pthread_attr_t *attr);

int pthread_attr_destroy(pthread_attr_t *attr);

int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t cpusetsize, cpu_set_t * cpuset);

int pthread_attr_getaffinity_np(const pthread_attr_t *attr, size_t cpusetsize, cpu_set_t * cpuset);

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

int pthread_attr_setname_np(const pthread_attr_t *attr, const char * name, void * arg);

int pthread_attr_getname_np(const pthread_attr_t *attr, char * name, int len);

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);

int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);

int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);

int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit);

int pthread_attr_setscope(pthread_attr_t *attr, int scope);

int pthread_attr_getscope(const pthread_attr_t *attr, int *scope); 

需要的时候可以查手册.

下面介绍三种专门用于线程同步的机制1)POSIX信号量2)互斥锁3)条件变量.


14.4 POSIX信号量

pthread_join也可以看作一种简单的线程同步方式,不过很显然,它无法高效的实现复杂的同步需求,比如控制对共享资源独占式访问,或者是在某个条件满足后唤醒一个线程.POSIX信号量函数都以sem_开头,并不像大多数函数那样以pthread_开头.

int sem_init(sem_t *sem, int pshared, unsigned int value);

int sem_wait(sem_t * sem);

int sem_timedwait(sem_t * sem, const struct timespec *abstime);

int sem_trywait(sem_t * sem);

int sem_post(sem_t * sem);

int sem_post_multiple(sem_t * sem, int number);

int sem_getvalue(sem_t * sem, int * sval);

int sem_destroy(sem_t * sem); 

14.5 互斥锁

当进入关键代码段时,需要获得互斥锁并将其加锁(二进制信号量的P操作),当离开关键代码段时,需要对互斥锁解锁(二进制信号量的V操作),以唤醒其他等待该互斥锁的线程.

互斥锁的相关操作:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_consistent(pthread_mutex_t *mutex);

int pthread_mutex_destroy(pthread_mutex_t *mutex); 

同样提供了pthread_mutexattr_t结构体定义了一套完整的互斥锁属性,故线程库提供了一系列操作以方便获取或设置互斥锁属性.

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);

int pthread_mutexattr_setkind_np(pthread_mutexattr_t *attr, int type);

int pthread_mutexattr_getkind_np(const pthread_mutexattr_t *attr, int *type);

int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);

int pthread_mutexattr_getrobust(pthread_mutexattr_t *attr, int *robust); 

14.6 死锁

使用互斥锁的一个噩耗是死锁(这个我还遇到过一次,排错真的是挺麻烦的,需要把整体逻辑理清).

提供一个死锁的案例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int a = 0;
int b = 0;

pthread_mutex_t   mutex_a;
pthread_mutex_t   mutex_b;

void *another(void *arg)
{
    pthread_mutex_lock(&mutex_b);
    printf("in child  thread, got mutex b, waiting for mutex a
");
    sleep(5);
    ++b;
    pthread_mutex_lock(&mutex_a);<- 卡在这,因为主线程没有释放mutex_a,它抢先获得了锁mutex_a
    b += a ++;
    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);
    pthread_exit(NULL);
}

int main(int argc, const char *argv[])
{
    pthread_t id;
    
    pthread_mutex_init(&mutex_a, NULL);
    pthread_mutex_init(&mutex_b, NULL);
    pthread_create(&id, NULL, another, NULL);

    pthread_mutex_lock(&mutex_a);
    printf("in parent thread, got mutex a, waiting for mutex b
");
    sleep(5);
    ++a;
    pthread_mutex_lock(&mutex_b); <- 卡在这,因为子线程没有释放mutex_b
    a+= b++;
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);

    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex_a);
    pthread_mutex_destroy(&mutex_a);

    return 0;
}

14.7 条件变量

互斥锁用于同步线程对于共享数据的访问,那么条件变量用于在线程之间同步共享数据的.

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程.

相关函数有:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

int pthread_cond_destroy(pthread_cond_t *cond); 

这里学习了一下pthread_cond_wait函数为什么要传入mutex.因为mutex用于保护条件变量的互斥锁,以确保pthread_cond_wait操作的原子性.在调用pthread_cond_wait之前,请先确保互斥锁mutex已被加锁,pthread_cond_wait函数执行的时候,首先把调用线程放入条件变量的等待队列中,然后将互斥锁解锁.可见,从pthread_cond_wait开始执行到其调用线程被放入条件变量队列之间的这段时间,pthread_cond_wait函数不会错过目标条件变量的任何变化(pthread_cond_signal和pthread_cond_broadcast等函数不会修改条件变量).


14.8 多线程环境

  • 若一个函数能被多个线程同时调用且不会发生竞态条件,则我们称它是线程安全的(thread safe),或者说是可重入函数.

  • 在多线程中使用库函数,一定要使用可重入版本.

  • 在Linux中,很多不可重入的库函数都提供了可重入版本(在名字后加了_r),不可重入的原因是在其内部使用了静态变量.

线程中的进程:

在一个多进程的某个线程调用了fork函数,那么新创建的子进程不会自动创建个父进程相同的线程,也不清楚从父进程那继承而来的互斥锁的具体状态.

pthread_mutex_t mutex;
//子线程运行的函数。它首先获得互斥锁mutex,然后暂停5s, 在释放该互斥锁
void *another(void *arg)
{
    printf("in child thread, lock the mutex
");
    pthread_mutex_lock(&mutex);
    sleep(5);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main(int argc, const char *argv[])
{
    pthread_mutex_init(&mutex, NULL);
    pthread_t id;
    pthread_create(&id, NULL, another, NULL);
    
    //父进程的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex
    sleep(1);
    int pid = fork();
    if(pid < 0)
    {
        pthread_join(id, NULL);
        pthread_mutex_destroy(&mutex);
        return 1;
    }
    else if (pid == 0) 
    {
        printf("I am in the child, want to get the lock
");
        //子进程从父进程继承了互斥mutex的状态,该互斥处于锁住的状态,这是由父进程中的子线程执行pthread_mutex_lock引起的,
        //因此,下面这句加锁操作会一直阻塞,尽管从逻辑上来说它是不应该阻塞的
        pthread_mutex_lock(&mutex);
        printf("I can not run to here, oop .....
");
        pthread_mutex_unlock(&mutex);
        exit(0);
    }
    else
    {
        wait(NULL);
    }
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

不过pthread提供了一个专门的函数pthread_afork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态.

int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void)); 
  • prepare:将在fork调用创建出子进程之前被执行.可以锁住所有父进程中的互斥锁.

  • parent:在fork调用创建出子进程之后,在fork返回之前,在父进程中被执行,可以用来释放所有在prepare锁住的互斥锁.

  • child:在fork返回之前,在子进程中被执行,可以用来释放所有在prepare中被锁住的互斥锁.


14.9 线程和信号

每个线程都可以独立的设置信号和掩码,但在多线程下应该使用pthread版本的sigprocmask

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

在某个线程中调用如下函数来等待信号并处理之:

int sigwait(const sigset_t *set, int *sig);

pthrad还提供了一个方法明确的将一个信号发送给指定的线程,也可以利用这种方式检测目标线程是否存在:

int pthread_kill(pthread_t thread, int sig);

关于第十四章的总结

  • 复习了线程创建与同步的相关函数,可以使用信号量,互斥锁和条件变量进行同步线程.

  • 复习了操作系统中的死锁.

  • 子进程对于父进程的子线程的态度,不会继承,但会继承父进程的锁状态.


From

Linux 高性能服务器编程 游双著 机械工业出版社

MarkdownPad2

Aaron-z/linux-server-high-performance

2017/2/12 17:46:53

原文地址:https://www.cnblogs.com/leihui/p/6394461.html