linux-多线程
1.linux 线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
线程概念
1.先说进程:传统操作系统中使用pcb来描述一个程序的运行pcb就是进程
2. linux下pcb用来模拟实现线程,因此linux下的pcb实际是一个轻量级进程LWP 线程
3.这个轻量级进程因为共用大部分进程资源,相较于传统进程更加轻量化
2.linux 进程VS线程
进程和线程
进程是资源分配的基本单位—因为程序运行时资源是分配给整个线程组(进程)的
线程是cpu调度的基本单位—因为linux下pcb是线程
线程之间资源的独有与共享:
独有:
- 栈,
- 寄存器,
- 信号屏蔽字,
- errno,
- 线程ID,
- 调度优先级
共享:
- 共享虚拟地址空间,
- 文件描述符表,
- 信号处理方式,
- 当前工作路径,
- 用户id/组id
任务的执行既可以使用多进程也可以使用多线程,各有优缺,视场景而定
多线程任务处理的优缺点:
多线程共用进程大部分资源
- 1.线程间通信处理进程间的方式之外还有更简单的就是全局数据/传参-线程间通信更加方便
- 2.创建/销毁-个线程的成本相较于进程要更低
- 3.线程间的调度相较于进程要更低
线程之间缺乏访问控制,有些系统调用/异常针对的是整个进程;稳定性相较于进程更低
使用场景: shell这种对主程序稳定安全性要求更高的程序就需要使用多进程,让子进程处理任务–背锅
3.linux 线程控制
线程控制:线程创建/终止/等待/分离
linux下操作系统并没有提供线程的控制系统调用接口;因此大佬们封装了一个套线程控制接口库
使用库函数实现创建的线程我们称之为用户态线程,这个用户态线程在内核中使用一个轻量级进程实现调度
linux下的线程:用户态线程+轻量级进程
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
创建线程
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)(void), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
int i;
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for(; ; ) {
printf("I'am main thread\n");
sleep(1);
}
}
线程中id的讨论:
- tid 线程地址空间首地址
- pcb -> pid 轻量级进程id
- pcb -> tgid 进程(线程组id),默认等于首线程id
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数:退出线程自身,谁调用谁退出
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:线程的退出返回值,value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
int_pthread_cancel(pthread_t thread);取消其它线程;让其它线程退出
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:要取消的线程ID
返回值:成功返回0;失败返回错误码
线程退出之后,默认不会自动释放资源,(保存自己的退出结果在线程独有的地址空间中) ;因此也会造成资源泄漏。
主线程退出,其它线程依然可以正常运行
线程等待
- 等待线程退出,获取退出线程的返回结果,释放退出线程资源;
一个线程创建出来,默认有一个属性joinable; - 处于joinable属性的线程退出后为了保存返回值,因此不会自动释放资源;
- 需要被其它线程等待,才能释放资源;
- 处于joinable属性的线程必须被等待,否则造成资源泄漏
一个线程也只有处于joinable状态的时候,才需要被等待
pthread_join函数:
功能:等待线程结束–>阻塞等待指定线程退出;通过retval获取返回值
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
线程分离
线程分离就是将线程joinable属性修改为detach属性线程若处于detach属性,则线程退出后将自动回收资源;
并且这个线程不需要被等待,等待是毫无意义的,因为线程退出返回值占用的空间已经被回收了
pthread_detach (pthread_t tid)
- 线程分离的适用场景:对线程的返回值不关心
- 线程分离可以在任意线程中实现
- 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
- joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
4.linux 线程安全
线程安全:多个线程同时对临界资源进行访问而不会造成数据二义
如何实现线程安全:同步+互斥
同步: 对临界资源访问的时序合理性
互斥:对临界资源同一时间访问的唯一性
可重入VS线程安全
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
线程间互斥的实现:互斥锁
1.定义互斥锁变量
pthread mutex_t
2.对互斥锁变量进行初始化
pthread_mutex_it(&mutex, &att)
3.对临界资源操作之前先加锁
pthread_mutex_lock(&mutex)
4.对临界资源操作完毕后进行解锁
pthread_ mutex_ unlock(&mutex);
- 销毁互斥锁
pthread mutex destroy(&mutex);
//售票系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
// sched_yield(); 放弃CPU
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}
线程间同步的实现:等待 +唤醒
操作条件不满足则等待,别人促使条件满足后唤醒等待
条件变量
条件变量实现同步:
线程在对临界资源访问之前,先判断是否能够操作;若可以操作则线程直接操作;
否则若不能操作;则条件变量提供等待功能;让pcb等待在队列上
其它线程促使操作条件满足,然后唤醒条件变量等待队列上的线程
1.定义条件变量
pthread _cond_t cond
2.条件变量初始化
pthread_cond_init(&cond, &attr);
3.用户在判断条件不满足的情况下提供等待功能
pthread_cond_wait(&cond, &mutex);
4.用户在促使条件满足后,唤醒等待
pthread_cond_signal(&cond) /pthread_cond_broadcast(&cond)
5.销毁条件变量
pthread_cond_destroy (&cond)
条件变量的条件判断应该是一个循环判断:
多个顾客线程若同时被唤醒,只有一个顾客可以加锁,其它的顾客线程将阻塞在加锁上(而不是条件变量的等待)
第一个加锁的顾客开始吃面,吃碗面后进行解锁,这时候,获取到锁的线程有可能是一个顾客线程,因为再次判断有没有面,因此直接吃面,但是面已经被第一个顾客线程吃掉了, 因此逻辑错误应该加锁之后重新再次判断是否有面
不同的角色应该等待在不同的条件交量上:
在有多个厨师线程和顾客线程的时候,若是顾客和厨师线程都等待在同一个条件变量的等待队列中,会导致厨师做了一碗面, 本应该唤醒顾客线程吃面,但是这时候有可能唤醒的是一个厨师线程,而厨师线程因为循环判断有没有面,因为已经有面而陷入等待(而顾客线程因为没有被唤醒而无法吃面)
死锁
死锁:多个线程对锁资源进行竞争访问,但是因为推进顺序不当,导致相互等待,使程序无法往下运行
死锁产生的四个必要条件:
- 1.互斥条件:一个锁只有一个线程可以获取
- 2.不可剥夺条件: 我加的锁别人不能解
- 3.请求与保持条件:拿着A锁,去请求B锁,但是获取不到B锁,也不释放A锁
- 4.环路等待条件:我拿着A锁请求B锁,对方拿着B锁清秋A锁
死锁预防:
- 破坏四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
死锁避免:
- 死锁检测算法
- 银行家算法