LINUX多线程

2019-07-14 09:39发布

对线程的理解
线程与进程的区别与联系,哪个好
线程安全概念,线程的同步与互斥

线程的概念

线程的定义
LINUX下的线程是由进程的pcb模拟的,进程与线程的结构体相同。其实pcb是一个个线程,这些线程pcb相较于传统的pcb更加轻量级,多个线程组成线程组,线程组的组id(tgid)等于主进程pid值,这些线程指向同一块虚拟地址空间。
线程是进程中的一个执行流,线程是cpu调度的基本单位,进程是资源分配的基本单位
一个进程至少都有一个执行线程,进程中至少有一个pcb。
进程中的线程是运行在同一个虚拟地址空间上的。

进程和线程选哪个好?

分析优缺点,视应用场景而定。
线程中没有子线程,只有主线程和普通线程 线程的优点
  • **1.**因为线程组都使用同一个虚拟地址空间,因此线程创建/销毁的成本更低
  • **2.**线程间通信更加方便
  • 3.线程的调度成本更加低
  • 4.线程的执行粒度更加细致
线程的缺点
  • 1.因为缺乏访问控制,因此编码需要注意的问题更多。
  • 2.稳定性低

线程控制

因为操作系统没有提供线程控制的接口,因此由一套库函数进行线程的控制,在编译时需要连接库
因为线程的接口代码都是在用户空间(共享区),因此也说使用这套接口创建的线程是用户态的线程,但是线程在我们操作系统内部对应了一个轻量级进程作为执行调度的载体。
  • 线程创建int pthread_creat(pthread_t *thread,const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
    ps-l查看轻量级进程信息,tid表示线程地址空间首地址
    *thread:输出型参数,用于获取用户态线程id
    *attr:线程属性,通常设置null
    *start_routine:线程的入口函数
    arg:通过线程的入口函数,传递给线程的参数
    返回值:成功为0,失败非0
    以获得线程自身的id : pthread_t pthread_self(void);
  • 线程终止
    终止的方式:return,但是主线程(main函数中)不能return,在主线程return了,进程就结束了
    pthread_exit(pthread_t arg) 谁调用谁退出,自己退出
    pthread_cancel(void *retval) 取消的是别人 别人退出,返回值不能作为标准结果,返回值为-1
  • 线程等待:等待指定线程的退出
    只有处于joinable状态的线程才是可以被等待的,否则就会报错。
    线程退出后,为了保存退出返回值,因此操作系统不会直接自动回收资源,需要其他线程等待,获取返回值,并允许操作系统回收资源,如果不等待则会造成资源泄漏。
    等待的方法:pthread_join(pthread_t thread,void **retval)
    thread:指定线程
    retval:获取返回值
    等待指定的线程退出,并且获取返回值
  • 线程的分离:设置线程的分离属性,被设置的线程,退出后将直接回收资源,不需要被等待
    将线程的joinable属性设置为detach状态,线程如果处于detach状态,则线程不能被等待
    方法:pthread_detach(pthread_t thread)
在进程pcb的虚拟地址空间中有一片共享区,每一个线程都会在共享区被分配一片独立的地址空间,装载线程的独立数据。 线程数据的独有与共享
  • 共享:共享代码段和数据段
    文件状态信息表
    信号的处理方式
    当前工作目录,用户id组id
  • 独有:栈
    一组寄存器(上下文数据,程序计数器)
    errno
    信号屏蔽字

线程安全

线程之间对临界资源的安全访问。因为在cpu足够情况下,多个线程的运行可能是并行的,因此对临界资源的访问,就有可能造成争抢操作,然而这种争抢会造成数据二义性。线程安全就是讨论如何保证线程对临界资源的安全访问。(使用同步与互斥解决) 互斥:对临界资源的同一时间的唯一访问性;
实现互斥:互斥锁,通过互斥锁保证临界资源的安全访问;互斥锁也是一个临界资源,因此互斥锁本身必须是原子操作。
非原子的操作:先将数据加载到寄存器处理,再将处理后的数据返回放入内存
原子操作:先将寄存器置0,再将其内容与内存互换
互斥锁就是一个只有0/1的计数器:1->有资源能操作;0->没资源,阻塞操作。
加锁:计数为1,则置零,继续操作;为0则阻塞或报错返回
解锁:计数置1.
  • 互斥锁的操作
    初始化互斥锁:int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)
    mutex:互斥锁变量
    attr:互斥锁属性,通常设置为null
    返回值:成功返回0,失败返回errno
  • 互斥锁的销毁: 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_t unlock(pthread_mutex_t *mutex)
    加锁之后,任何有可能退出的地方都应该解锁
  • 死锁:线程或进程因为同时使用多个锁,因一直获取不到锁资源导致阻塞卡死的情况。
    死锁产生的四个必要条件:1.互斥条件----我加锁了别人就不能加锁2.不可剥夺条件----我的锁别人不能解3.请求与保持条件----拿着第一个请求第二个,拿不到第二个,但是第一个也不放手4.环路等待条件—一个线程拿着锁a请求锁b,另一个线程拿着锁b请求锁a,陷入了互相等待。
    预防死锁:破坏必要条件
    避免死锁:银行家算法啊,死锁检测算法
同步:对临界资源的时序可控性;
实现同步:条件变量pthread_cond_t
初始化条件变量:pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond:条件变量
attr:条件变量属性
返回值:成功返回0,失败返回reeno
  • 限时等待:int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
    一直等待 : int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
    cond:条件变量
    mutex:互斥锁
    死等之前需要解锁,并且解锁和死等必须是原子操作,被唤醒后需要加锁,但是这个加锁不是阻塞的,意味着不管是否能加锁都会往下操作,访问临界资源,如果被唤醒的是多个线程,则会出现问题,因此需要循环的条件判断
    不同角 {MOD}的线程应该等待在不同的条件变量上,防止错误唤醒导致卡死。
  • 单个唤醒 int pthread_cond_signal(pthread_cond_t *cond);
    广播唤醒 int pthread_cond_broadcast(pthread_cond_t *cond);
生产者消费者模型:
生产者抓取数据,将数据放到缓冲区,消费者从缓冲区中拿到数据进行处理。(缓冲区需要考虑线程安全问题)
生产者消费者模型的优势:1.支持忙闲不均2.解耦合3.支持并发 一个场所,两类角 {MOD},三种关系
生产者与生产者的关系:互斥
消费者与消费者的关系:互斥
生产者与消费者的关系:同步+互斥 若使用c++实现生产者与消费者模型需要用到:队列(std::queue),互斥锁(mutex),条件变量

信号量(POSIX标准)

信号量是一个具有等待队列的计数器,计数器是一个资源计数,可以用来实现进程或线程间的同步与互斥。
若获取资源:计数大于0,表示有资源可以操作,计数不大于0,表示没有资源可供操作,陷入休眠。
若释放资源:计数加一,表示唤醒等待在队列上的pcb
  • 初始化信号量:int sem_innit(sem_t *sem,int pshared,unsigned int value);
    sem:信号量的变量
    pshared:应用范围:0–> 线程间同步与互斥;!0—>进程间同步与互斥
    value:信号量的初值
  • 销毁信号量 int sem_destroy(sem_t *sem)
  • 内部判断计数是否大于0,大于0–>立即正确返回;不大于0–>陷入等待
    int sem_wait(sem_t *sem);
    int sem_trywait(sem_t *sem);
    int sem_timewait(sem_t *sem,const struct timeapace *abs_timeout);
  • 信号量计数加一并且唤醒等待队列中的pcb int sem_post(sem_t *sem)

读写锁

能够实现写互斥,读共享的操作,读写锁是使用自旋锁实现的,通过两个计数器控制。
适用于操作时间短,读操作多,写操作少的场景。读写锁默认是读优先的。使用读写锁,如要写优先,需要设置读写锁属性(PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
write:写的时候判断写计数和读计数,若都为0则可以写操作加写锁,否则自旋等待。
read:读的时候只需要判断写计数,若为0,则可以读,加读锁,否则自旋等待。
读写锁操作:
  • 读写锁初识化:pthread_rwlock_init()
  • 销毁读写锁: pthread_rwlock_destroy()
  • 阻塞式加读锁: pthread_rwlock_rdlock()
  • 非阻塞式加读锁: pthread_rwlock_tryrdlock()
  • 解锁: pthread_rwlock_unlock();

线程池

启动一个线程处理任务请求,若同一时间因为大量请求而创建大量线程,有可能导致资源耗尽,程序崩溃,因此需要设置限制上限。
创建线程+处理任务+销毁线程=处理任务花费的总时间,若创建线程和销毁线程的比例较大,则创建或销毁线程占据了大量时间,对于程序效率来说,是非常不可取的,因此利用线程池创建线程来解决这些问题。
线程池:一个或多个线程+任务队列。
线程池的功能有:1.避免峰值压力,导致资源耗尽。2.避免大量的线程创建/销毁成本。
线程池的实现:线程的创建+线程安全的任务队列的实现+向队列添加任务。

线程安全的单例模式

单例模式是典型设计模式中的一种,指的是一个对象只能被初始化一次。
单例模式----饿汉模式
在启动阶段一次性初始化完毕,用户体验不太好,因此程序初始化阶段时间耗费长,后期运行流畅。 class obj{ static T *data; obj(){ mutex.lock(); if(data==null){ data=new T(); } mutex.unlock(); } }
单例模式----懒汉模式(用的更多)
将初始化分摊到各个阶段,使用的时候再初始化,启动节点用户体验比较好,但是第一次运行到某个模块的时候,流畅度不太好。