Linux—多线程

为什么要学习多线程呢?当然是为了提高程序的运行效率了。面对复杂的问题,单一的进程已经解决不了问题了,所以引用了多线程。

线程是什么?

1.线程也是内核创建出来的task_struct结构体(在内核当中创建了一个PCB)
2.从内核角度看,内核当中其实没有线程的概念,叫做轻量级进程(线程的概念是C库当中的概念)
3.内核创建出来的轻量级进程的PCB,在task_struct结构体当中内存指针也是指向进程的虚拟地址空间的
在这里插入图片描述
线程的优点:
1.创建一个线程的开销比创建一个进程小
2.创建一个线程所用的资源比较小
3.进程当中多个线程可以并行的运行
4.多线程程序可以提高程序运行效率
线程的缺点:
1.健壮性低/鲁棒性低
2.多线程程序当中有一个执行流异常的情况下,会导致整个程序异常
3.缺乏访问控制
4.编程难度高
5.性能损失(当线程切换的时候,有可能切换的成本比处理业务的成本还高,导致程序在切换线程的时候占用时间比较多,意味着线程并不是越多越好)
线程的独有和共享
独有:线程ID:tid 栈 信号屏蔽字 调度优先级 errno 一组寄存器
共享:进程虚拟地址空间 文件描述符表 共享当前进程工作路径 用户ID和用户组ID

线程控制

接口:都是库函数,在使用这些接口的时候,需要连接pthread库,链接的时候增加-lpthread
线程创建:int pthread_create(pthread_t thread,const pthread_attr attr,void (start_routine)(void ),void arg);
thread:线程的标识符,不同于线程id,是一个出参,本质上是线程独有空间的首地址
attr:线程的属性,在创建线程的时候,如果为NULL,则采用默认属性
pthread_attr_t类型,结构体 线程的大小 线程的起始位置 线程的分离属性
start_routine:函数指针。保存线程入口函数的地址
arg:给线程入口函数传递参数(不能传递临时变量,而是需要传递堆上开辟的内存,void
可以传递任意类型,包含自定义数据类型,包含类的实例化指针,在线程入口函数内部进行强转,强转完成之后使用,但是堆上开辟的内存用完之后一定要记得释放,否则就会造成内存泄漏)
线程终止:
线程终止的方式:
1.从线程入口函数return返回
2.pthread_exit(void
retval)函数退出(谁调用谁退出)
retval:当前线程的退出信息,也可以去传递NULL值
3.pthread_cancel(pthread_t thread)(只要传递了线程的标识,就可以结束任意的线程)
pthread_self()获取自己线程的线程标识
线程等待
1.原因:线程在采用默认属性进行创建的时候,线程的属性是joinable,当线程退出的时候,如果为joinable属性,则需要其他线程来回收退出线程的资源,否则在共享区当中退出线程的空间还在保留,保留空间不能被复用,会造成内存泄漏。
2.接口:pthread_join(pthread_t, void
)(这个接口会阻塞)
pthread_t:等待的进程
void
:获取退出信息的
线程分离:
设置现成的属性为detach属性,当线程退出的时候,不需要其他线程来回收退出的资源,意味着不需要其他线程进行等待
int pthread_detach(pthread_t thread)
pthread_t:线程标识符,分离那一个线程
线程安全:
1.线程安全指的是多个线程同时运行,访问临界资源,不会导致程序的结果产生二义性。
临界资源:在同一时刻,该资源只能被一个执行流所访问
访问:在临界区对临界资源进行非原子在操作
原子操作:操作是一步完成的,当前操作只有两个结果,要么是完成了,要么是没完成
2.如何保证线程安全
互斥:保证同一时刻只有一个执行流访问临界资源
同步:保证程序对临界资源访问的合理性
互斥锁
1.使用互斥锁来保证互斥属性
2.互斥锁本质上是一个计数器,但这个计数器只有两个值,一个为0,一个为1(0代表无法获取互斥锁,1代表可以获取互斥锁)
3.在访问临界资源前,先获取互斥锁(如果能获取互斥锁,表示当前资源可以去访问,如果不能获取互斥锁,表示当前资源不可以被访问,也就意味着计数器当中的值为0,已经有一个线程在访问临界资源了)
4.使用流程
(1)初始化互斥锁
(2)加锁(将计数器当中的值变为0)如果计数器为1,可以正常加锁,如果计数器为0,不可以正常加锁,当前想要加锁的执行流被阻塞
(3)访问临界资源
(4)解锁(将计数器当中的值变为1)
5.操作接口
(1)定义互斥锁pthread_mutex_t:互斥锁变量类型
(2)初始化互斥锁int pthread_mutex_init(pthread_mutex_t* mutex,pthread_mutexattr_t* attr)
mutex:互斥锁变量,要初始化哪一个互斥锁变量,一般情况下,定义互斥锁变量,传入互斥锁对象的地址
attr:互斥锁的属性,一般情况下,我们直接置位NULL,采用默认属性
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
(3)加锁int pthread_mutex_t lock(pthread_mutex_t mutex)阻塞加锁操作
mutex:需要对哪一个互斥锁进行加锁,传递互斥锁对象的地址即可,该接口为阻塞接口,如果加锁成功则返回,如果加锁失败则阻塞,直到加锁成功int pthread_mutex_timedlock(pthread_mutet_t* mutex,const struct timespec* abs_timeout)
mutex:需要对哪一个互斥锁进行加锁传递互斥锁对象的地址即可
abs_timeout:加锁时间(在加锁的时候,如果超过时间还没有加上锁,则直接返回,会报错TIMEOUT,不会进行阻塞等待)
struct timespec:有两个变量,第一个变量为秒级,第二个变量为纳秒
int pthread_mutex trylock(pthread_mutex* mutex)非阻塞加锁操作
mutex:传入互斥锁变量的地址,来进行加锁操作(如果计数器值为1,意味着可以加锁,加锁之后、计数器的值从1变为0,如果计数器的值为0,意味着不可以加锁,该接口直接返回,不进行阻塞等待,返回EBUSY表示拿不到锁资源)
(4)解锁操作 int pthread_mutex_unlock(pthread_mutex* mutex)
不管是pthread_mutex_lock pthread_mutex_trylock pthread_mutex_timedlock加锁的,使用该接口都是可以进行解锁的
(5)销毁互斥锁int pthread_mutex_destory(pthread_mutex* mutex)
注意:
1.初始化互斥锁的位置一定要在创建线程之前
2.在访问临界资源之前进行加锁
3.使用完成互斥锁之后一定要记得释放互斥锁(如果 不释放互斥锁,则会阻塞其他进程,我们称之为死锁)
4.互斥锁只能被自己的线程锁释放,别的线程释放不了
5.在所有可能退出的地方都进行解锁操作
条件变量
本质:PCB等待队列+两个接口(等待接口+唤醒接口)
功能:当有资源的时候,可以让执行流直接访问资源,在没有自愿的情况下让执行流等待,等待的执行流的PCB被放到PCB等待队列当中去
条件变量的接口:
1.定义条件变量 pthread_cond_t条件变量的类型
2.初始化条件变量
动态初始化 int pthread_cond_init(pthread_cond_t* cond,pthread_condattr_t* attr)
cond:传入条件变量的地址就可以了
attr:传入NULL采用默认属性就可以了
静态初始化:pthread_cond_t cond=PTHREAD COND INITIALIZER
3.等待接口 :将调用该等待接口的执行流的PCB放到等待队列当中去,进行等待 int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex)
cond:传入条件变量的地址就可以了
mutex:传入互斥锁变量的地址
内部实现原理:
(1)将调用者的PCB放到PCB等待队列当中去
(2)解放互斥锁
(3)等待被唤醒
4.唤醒接口:通知PCB等待队列当中的执行流来访问临界资源
int pthread_cond_signal(pthread_cond_t* cond)唤醒至少一个PCB等待队列当中的执行流
int pthread_cond_broadcast(pthread_cond_t* cond)唤醒PCB等待队列当中所有的执行流
5.销毁接口 int pthread_cond_destory(pthread_cond_t* cond)
生产者与消费者模型
123规则:1个场所(队列)2种角色(生产线程和消费线程)3三种关系(生产者与生产者互斥,消费者与消费者互斥,生产者与消费者同步+互斥)
优点:
可以解耦合:生产者与消费者通过队列进行交互,生产者只关心队列当中是否有空闲的节点,消费者只关心队列当中是否又产生好的数据
支持忙闲不均:队列起到一个缓冲的作用,可以先生产数据到队列当中去,消费者可以直接从消费者中消费
支持并发:队列起到一个缓冲作用
Posix信号量
1.主要完成线程间或者进程间的同步与互斥功能
2.本质:资源计数器+PCB等待队列+等待和唤醒接口
3.操作接口
(1)定义 sem_t:posix版本信号量类型
(2)初始化(重要)sem_init(sem_t* sem,int pshared,int value)
sem:传入信号量地址
pshared:表示当前信号量是在进程间使用还是在线程间使用(0表示在线程间全局变量,1表示在进程间,初始化为进程的时候,内核会创建一块内存共享,来保存信号量的数据结构,其中的资源计数器,PCB等待队列都是在共享内存当中维护的。所以当我们调用唤醒或等待接口的时候,就是通过操作共享内存来实现不同进程间的同步和互斥的)
value:实际资源的数量,用于初始化信号量的资源计数器
(3)等待接口
sem_wait(sem_t* sem)//阻塞等待接口
sem_trywait(sem_t sem)//非阻塞等待
sem_tiemdwait(sem_tsem)//带有超市时间的等待
(4)唤醒int sem_post(sem_t
sem)发布信号量,表示资源使用完成了,需要归还资源或者生产者生产者重新生成一个资源,需要对资源部计数器进行加1操作,唤醒PCB等待队列当中的PCB
(5)销毁接口 int sem_destory(sem_tsem)
4.如何保证同步和互斥
互斥:初始化资源计数器的时候为1;
同步:初始化的时候,根据资源的数量来进行信号量当中的资源计数器
读写锁
1.适用的场景:少量写+大量度
特点:如果读写锁被以读模式占用(打开),其他线程线程也可以使用读模式占用这把读写锁
2.读写锁有三种种状态
读写模式下的加锁状态–>不改变临界资源的值
写模式下的加锁状态–>改变临界资源的值
不加锁的状态
3.加锁规则
(1)在同一时刻,只能有一个执行流以写模式占用读写锁(互斥情况)不能同时写
(2)在同一时刻,多个线程可以同时占用读模式下的读写锁
原理:在读写锁内部有一个引用计数,引用计数,引用计数标识当前以读模式占用读写锁的线程个数,为了方便在释放读写锁的时候是否真正是读模式下的读写锁
读模式加锁:对引用计数进行++操作
读模式解锁:对引用计数器进行–操作,直到减为0,则是读模式下的读写锁
4.接口:
(1)定义pthread_rwlock_t
(2)初始化pthread_rwlock_init(pthread_rwlock_t
,pthread_rwlockattr_t*)
(3)加锁
读模式 pthread_rwlock_rdlock(pthread_rwlock_t)
写模式(相当于互斥锁)pthread_rwlock_unlock(pthread_rwlock_t*)
(4)解锁pthread_rwlock_unlock(pthread_rwlock_t*)
(5)销毁pthread_rwlock_destory(pthread_rwlock_t*)
线程池
线程池=线程安全队列+一大堆的线程
在这里插入图片描述
如何让相同的入口函数,处理不同的请求:
队列当中的元素=待处理的数据+处理函数的地址
对于线程而言:只需要调用处理函数去处理待处理的数据
线程池当中的线程退出的时候,一定要将队列当中的数据处理完毕之后在退出

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值