• 第十四章 多线程编程


    第十四章 多线程编程


    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

  • 相关阅读:
    【转】直方图中bins的理解及处理
    [LeetCode] 1. Two Sum
    数据结构课学到的常用知识总结概括
    Java核心技术第三章数据类型
    mysql 出现You can't specify target table for update in FROM clause错误的解决方法
    mysql 从一个表中查数据并插入另一个表实现方法
    使用ECharts,绘制柱状图
    mysql 查询1小时内_mysql查询一个小时之内的数据
    mysql查询表中所有字段的名字
    MySQL 时间函数总集
  • 原文地址:https://www.cnblogs.com/leihui/p/6394461.html
Copyright © 2020-2023  润新知