【Linux网络编程】多线程编程

总结《Linux高性能服务器编程》14章

第14章 多线程编程

Linux线程概述

  • 线程模型

    进程:资源分配的最小单位

    线程:程序执行的最小单位

    • 线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体;

    • 根据运行环境和调度者的身份,线程可分为==内核线程和用户线程==

      • 内核线程(也称LWP):运行在内核空间,由内核调度
      • 用户线程:运行在用户空间,由线程库来调度
    • 当进程的一个内核线程获得CPU的使用权时就加载并运行一个用户线程,内核线程相当于用户线程运行的“容器”;

    • 一个进程可以拥有M个内核线程和N个用户线程,其中M≤N,按照M:N的取值,线程的实现方式可分为三种模式:

      • M=1:完全在用户空间实现的线程
        • 无须内核支持,由线程库负责管理;
        • 一个进程的所有执行线程共享该进程的时间片,对外表现出相同的优先级,即该内核线程就是进程本身
        • 优点:创建和调度线程都无须内核干预,速度快,不占用额外内核资源;
        • 缺点:一个进程的多个线程无法运行在不同的CPU上,线程的优先级只对同一个进程中的线程有效;
      • M:N=1:1:完全由内核调度的模式
        • 创建、由内核调度线程,运行在用户空间的线程库无须执行管理任务;
        • 优缺点与M=1时的情况互换;
      • 双层调度模式:前两种实现模式的混合体
        • 内核调度M个内核线程,线程库调度N个用户线程;
        • 结合优点:不会消耗过多内核资源,线程切换速度较快,可以充分利用多处理器优势;
  • Linux线程库
    • Linux上两个最有名的线程库是LinuxThreadsNPTL,均采用1:1的方式实现;
      • LinuxThreads线程库的内核线程是用clone系统调用创建的进程模拟的,但存在语义问题;
      • 现代Linux上默认使用的线程库是NPTL;
        • 内核线程不再是一个进程,避免使用进程模拟内核线程的语义问题;
        • 摒弃管理线程,终止线程、回收线程堆栈等工作都可以由内核来完成;
        • 一个进程的线程可以运行在不同的CPU上,从而充分利用多处理器系统的优势;
        • 线程的同步由内核来完成,可实现跨进程的线程同步;

创建线程和结束线程

  • 创建和结束线程的基础API都定义在pthread.h头文件中;

  • 创建线程的函数pthread_create

    #include<pthread.h>
    int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine) (void*), void* arg);
    
    • thread参数是新线程的标识符,pthread_t是一个整型类型;
    • attr参数用于设置新线程的属性;
    • start_routine和arg参数分别指定新线程将运行的函数及其参数;
    • 一个用户可以打开的线程数量不能超过RLIMIT_NPROC软资源限制,系统上所有用户能创建的线程总数也不得超过/proc/sys/kernel/threads-max内核参数所定义的值;
  • 结束线程的函数pthread_exit

    #include<pthread.h>
    void pthread_exit(void* retval);
    
    • 通过retval参数向线程的回收者传递其退出信息;
    • 执行完之后不会返回到调用者,而且永远不会失败;
  • 回收其他线程的函数pthread_join

    #include<pthread.h>
    int pthread_join(pthread_t thread,void**retval);
    
    • 类似于回收进程的wait和waitpid系统调用;
    • 该函数会一直阻塞,直到被回收的线程结束为止;
  • 取消线程的函数pthread_cancel

    #include<pthread.h>
    int pthread_cancel(pthread_t thread);
    
    • 接收到取消请求的目标线程可以决定是否允许被取消以及如何取消

      #include<pthread.h>
      int pthread_setcancelstate(int state,int* oldstate); //state设置是否允许被取消
      int pthread_setcanceltype(int type,int* oldtype);//type设置如何被取消(同步异步)
      

线程属性

  • pthread_attr_t结构体定义了一套完整的线程属性,全部包含在一个字符数组中

    #include<bits/pthreadtypes.h>
    #define__SIZEOF_PTHREAD_ATTR_T 36
    typedef union
    {
    char__size[__SIZEOF_PTHREAD_ATTR_T];
    long int__align;
    }pthread_attr_t;
    
  • 线程库定义了一系列函数来获取和设置线程属性

    #include<pthread.h>
    /*初始化线程属性对象*/
    int pthread_attr_init(pthread_attr_t*attr);
    /*销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用*/
    int pthread_attr_destroy(pthread_attr_t*attr);
    /*下面这些函数用于获取和设置线程属性对象的某个属性*/
    int pthread_attr_getdetachstate(const pthread_attr_t*attr,int*detachstate);
    int pthread_attr_setdetachstate(pthread_attr_t*attr,int detachstate);
    int pthread_attr_getstackaddr(const pthread_attr_t*attr,void**stackaddr);
    int pthread_attr_setstackaddr(pthread_attr_t*attr,void*stackaddr);
    int pthread_attr_getstacksize(const pthread_attr_t*attr,size_t*stacksize);
    int pthread_attr_getstack(const pthread_attr_t*attr,void**stackaddr,size_t*stacksize);
    
  • 线程属性的含义
    • detachstate:线程的脱离状态,可以指定为 [可被回收] 或 [脱离与进程中其他线程的同步] ;
    • stackaddrstacksize:线程堆栈的起始地址和大小;
      • 可以使用ulimt-s命令来查看或修改堆栈空间大小(一般是8 MB);
    • guardsize:保护区域大小,作为保护堆栈不会被错误覆盖的区域;
    • schedparam:线程调度参数,表示线程允许优先级;
    • schedpolicy:线程调度策略;
    • inheritsched:是否继承调用线程的调度属性;
    • scope:线程间竞争CPU的范围,即线程优先级的有效范围;

POSIX信号量

  • 用于线程同步的机制:POSIX信号量、互斥量和条件变量

  • 在Linux上,信号量API有两组,一组是第13章讨论过的System V IPC信号量,另外一组POSIX信号量,接口相似,语义相同,但不能保证互换;

  • POSIX信号量函数的名字都以**sem_**开头;

  • 常用的POSIX信号量函数(参数sem指向被操作的信号量):

    • 初始化一个未命名的信号量

      #include<semaphore.h>
      int sem_init(sem_t*sem,int pshared,unsigned int value);
      
      • pshared参数指定信号量的类型,0表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享;
      • value参数指定信号量的初始值;
    • 销毁信号量,释放其占用的内核资源

      #include<semaphore.h>
      int sem_destroy(sem_t*sem);
      
    • 以原子操作的方式将信号量的值减1

      #include<semaphore.h>
      int sem_wait(sem_t*sem); //信号量为0则阻塞
      int sem_trywait(sem_t*sem); //始终立即返回,当信号量为0将返回-1并设置errno为EAGAIN
      
    • 以原子操作的方式将信号量的值加1

      #include<semaphore.h>
      int sem_post(sem_t*sem);
      

互斥锁(量)

  • 互斥锁是用于同步线程对共享数据的访问

  • 互斥锁(也称互斥量)可以用于保护关键代码段,以确保其独占式访问

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

  • 互斥锁基础API
    #include<pthread.h>
    int pthread_mutex_init(pthread_mutex_t*mutex,const pthread_mutexattr_t*mutexattr);
    int pthread_mutex_destroy(pthread_mutex_t*mutex);
    int pthread_mutex_lock(pthread_mutex_t*mutex);
    int pthread_mutex_trylock(pthread_mutex_t*mutex);
    int pthread_mutex_unlock(pthread_mutex_t*mutex);
    
  • 互斥锁属性
    • pshared:指定是否允许跨进程共享互斥锁

      int pthread_mutexattr_getpshared(pthread_mutexattr_t*attr, int*pshared);
      int pthread_mutexattr_setpshared(pthread_mutexattr_t*attr, int pshared);
      
    • type:指定互斥锁的类型

      int pthread_mutexattr_gettype(pthread_mutexattr_t*attr,int*type);
      int pthread_mutexattr_settype(pthread_mutexattr_t*attr,int type);
      
  • 互斥锁类型
    • 普通锁:默认,保证资源分配公平性,但如果对已经加锁的普通锁再次加锁,将引发死锁
    • 检错锁:如果对已经加锁的检错锁再次加锁则加锁操作返回EDEADLK,对已解锁的检错锁再次解锁,将返回EPERM;
    • 嵌套锁:允许一个线程在释放锁之前多次对它加锁而不发生死锁,但获得这个锁需要执行相应次数的解锁操作;
  • 死锁
    • 对已经加锁的普通锁再次加锁,将引发死锁;
    • 两个线程互相占有且等待互斥锁,将引发死锁;

条件变量

  • 用于在线程之间同步共享数据的值

  • 当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程;

  • 条件变量API
    #include<pthread.h>
    int pthread_cond_init(pthread_cond_t*cond,const pthread_condattr_t*cond_attr);
    int pthread_cond_destroy(pthread_cond_t*cond);
    int pthread_cond_broadcast(pthread_cond_t*cond);
    int pthread_cond_signal(pthread_cond_t*cond);
    int pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);
    
    • 参数cond指向要操作的目标条件变量,条件变量的类型是pthread_cond_t结构体;
    • pthread_cond_broadcast函数以广播的方式唤醒所有等待目标条件变量的线程;
    • pthread_cond_wait函数用于等待目标条件变量;

多线程环境

  • 可重入函数
    • 可重入函数:一个函数能被多个线程同时调用且不发生竞态条件,则称它是线程安全的(thread safe);
    • linux库函数只有一小部分是不可重入的,如getservbyname和getservbyport函数等;
  • 线程和进程
    • 一个多线程程序的某个线程调用了fork函数,新创建的子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制;
    • 子进程将自动继承父进程中互斥锁(条件变量与之类似)的状态,但可能不清楚从父进程继承而来的互斥锁的具体状态;
    • pthread提供了一个pthread_atfork函数,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态;
  • 线程和信号
    • 每个线程都可以独立地设置信号掩码

      #include<pthread.h>
      #include<signal.h>
      int pthread_sigmask(int how,const sigset_t*newmask,sigset_t*oldmask);
      
    • 线程库将根据线程掩码决定把信号发送给哪个具体的线程;

    • 进程中的所有线程共享该进程的信号和信号处理函数,应该定义一个专门的线程来处理所有的信号。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值