多线程
线程是什么?
1、线程是进程的执行分支,一个进程内部的控制程序
2、一个进程至少有一个执行线程
3、从CPU角度来看,线程就是一个更轻量化的线程
4、线程在进程内部运行,所以本质就是在进程地址空间上运行
注意:
一个程序至少有一个进程,一个进程至少有一个线程?
程序是静态的,进程是程序一次运行
线程异常
当一个进程中的某个线程出现除0或者野指针错误时,线程异常退出,会导致进程一起退出
线程是进程的执行分支,所以当线程出现异常,进程也会出现类似异常
进程VS线程
1、线程拥有系统资源吗?
进程是资源分配的基本单位,所以线程不拥有系统资源
进程是资源分配的基本单位,线程是调度的基本单位
2、线程和进程都可以并发执行
进程比线程安全的原因是:同一进程下的线程共享同一片资源,虽然每个线程看起来只能访问自己那部分资源,但其实线程之间资源是共享的,进程之间不会数据共享
线程函数
创建线程
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是一个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
第一个返回的线程ID和之前进程调度标识线程的ID不是一个东西
1、这里的第一个参数指向虚拟内存,内存单元的地址就是这个线程ID(tid)
所以这里pthread_t类型,本质就是进程地址空间上的一个地址
2、进程调度中的线程ID的意思是,线程是操作系统调度器的最小单位,所以需要一个数值来标识唯一的线程
获取线程ID
pthread_self函数
获取用户级线程的tid,不是轻量级进程的ID,用户级线程是由用户级空间实现的线程,而轻量级进程是由操作系统管理,一个线程可能映射一个或多个轻量级进程
线程终止
1、return返回,在main函数中return 就相当于pthread_exit
2、pthread_exit终止自己
这里参数不能指向局部变量,如果需要可以使用pthread_join()来接收
pthread_exit或者return返回的指针所指向的内存单元必须是全局或者malloc申请的,不能在线程函数的栈上分配,因为如果其他线程访问这个指针时,线程函数已经退出了,找不到指针
3、pthread_cancel
终止同一进程中的线程
返回值:成功返回0,失败返回非0
注意:
在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行
在有多个线程的情况下,主线程从main函数的return返回或者调用pthread_exit函数,则整个进程退出
线程等待
为什么需要线程等待?
退出的线程,不会释放进程地址空间中的内容
创建新线程不会复用之前退出线程的地址空间
退出方式不同,第二个参数获取的状态也会不同
1、return返回,获取线程函数的返回值
2、被别的线程pthread_cancel()之后,存放常数PTHREAD_CANCELED
3、pthread_exit()自己终止之后,获取传给pthread_exit的参数
4、不关心退出状态,设置为NULL
线程分离
默认情况下,新创建的线程都是joinable,使用pthread_join()来释放资源
但是如果对退出状态不关心时,pthread_join()就会成为负担,所以当线程退出时,自动释放资源
注意:线程分离和等待是冲突的,所以不能两个都使用
互斥
相关概念
临界资源:多线程执行流共享的资源
临界区:每个线程内部访问临界资源的代码
互斥:保证只有一个执行流进入临界区访问临时资源,对临时资源起保护作用
原子性:要么完成,要么没完成,没有正在做的概念
互斥量mutex
当一个线程访问共享数据时,可能其他线程也要访问共享数据,这个时候共享数据可能就会出现二义性
这就需要引入锁,也就是互斥量来解决上述问题
1、保证当一个线程访问共享资源,其他线程不能访问共享资源(加锁)
2、如果多个线程都要访问共享资源时,只允许一个线程访问
3、当一个线程没有访问共享资源时,不能阻止其他线程访问(解锁)
mutex简单理解成一个0\1计数器
0,表示已经有执行流加锁成功,资源不可访问
1,没有加锁,资源可以访问
初始化互斥量
1、静态初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
2、动态初始化
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, NULL);
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
静态创建的互斥量不需要销毁
动态创建的互斥连更需要使用销毁函数来销毁
加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
当一个线程加锁时,如果其他线程已经申请锁了,那么该线程加锁就会和其他线程竞争锁,陷入阻塞,等待解锁
可重入函数和线程安全
线程安全:多个线程并发同一片代码不会出现不同的结果,对静态或者全局变量进行操作,并且没有锁时,就可能会造成线程安全
可重入函数:当一个函数被不同的执行流调用,当一个函数流程还没有走完,其他线程进入,这称为重入,当一个函数再重入的情况下,结果不会出现任何不同或者任何问题,这就称为可重入函数
注意:
可重入函数是线程安全的,但是线程安全不一定是可重入函数
当对临时资源进行加锁,那么这个临时资源就是线程安全的,但是如果重入函数在加锁时,再次调用就可能会造成死锁,变成线程不安全的
死锁
死锁是指一组线程中各个线程占有不会释放的资源,但是因为互相申请别人占用的资源而处于永久等待状态
死锁的四个必要条件
1、互斥条件:一个资源只能被一个执行流使用
2、请求和保持条件:一个执行流申请资源而阻塞时,但是不释放自己的资源
3、不剥夺条件:当一个执行流获得资源,未使用完之前,不能强行剥夺
4、循环等待条件:若干执行流形成的一种头尾相连的循环等待资源关系
避免死锁
1、破环四个必要条件
2、加锁顺序一致(也就是破环循环等待条件,每个线程按照相同顺序加锁,可以避免循环等待)
3、避免锁未释放
4、资源一次性分配(避免加锁之后,再次申请资源,减少加锁场景)
Linux线程同步
条件变量:Linux同步机制,允许线程在条件不满足时进入等待状态,直到条件满足时,使线程可以进入睡眠状态,从而避免不必要占用CPU资源
同步:在保证数据安全的前提下,线程按照某种特定顺序访问临界资源,从而避免饥饿问题
竞态条件:由于操作执行顺序不当,造成的程序异常
饥饿问题:由于竞争条件不平等,导致一些线程访问共享资源被调度器忽略,从而无法访问共享资源
条件变量函数
初始化和销毁
pthread_cond_init()参数arr为NULL,标识使用默认属性
条件等待
pthread_cond_timedwait()多出来的参数就是等待条件变量超时时间,如果超时就会返回错误码
这里为什么等待要在加锁和解锁之间?
1、一个线程加锁之后进入等待,其他线程如何访问临界资源?
pthread_cond_wait()会自动解锁
2、如何知道线程是否需要休眠?
临界资源不就绪,临界资源也是有状态的
3、如何知道临界资源是否就绪?
通过判断,判断也是访问临界资源,所以等待必须在加锁之后
唤醒等待
pthread_cond_signal()唤醒指定线程
pthread_cond_broadcast()唤醒所有线程
伪唤醒
在没有明确条件触发的情况下被条件变量唤醒,这种情况就可能导致在唤醒时,条件不满足,导致程序异常
解决办法:在pthread_cond_wait()处,使用while循环来充当判断条件,当伪唤醒时,能够再次进行判断,如果条件不满足就会再次进入等待
生产消费者模型
使用场景
321原则
3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥和同步)
2个角色:生产者和消费者
一个交易场所:特定结构的内存空间
优点:
1、生产和消费者之间解耦合
2、支持忙先不均
3、支持并发
线程池
线程过多会带来调度开销,降低内存局部性和整体性能,线程池维护这多个线程,防止在处理多个较短任务时,对线程的申请和销毁。
同时也能够解决大量不受控制的线程占用资源导致资源耗尽
单例模式
饿汉模式
在程序初始化时,加载所有资源, 无论资源是否用到都要加载
懒汉模式
在程序初始化时,只加载核心资源,其他资源在使用时才加载
所以懒汉模式下的,“延迟加载”能够优化服务器的启动速度
这两种模式下执行流/线程都是共享同一份资源的
线程安全下的懒汉模式
1、volatile关键字防止inst被编译器优化,对inst修改不要放到寄存器中,直接反映到内存,让所有线程看到
2、双if,降低锁冲突的概率 ,提高性能
3、互斥锁,保证只会new一个inst
typename <class T>
class Singleton
{
public:
volatile static T* inst;
std::mutex lock;
T* Getinstance()
{
if(inst==NULL)
{
lock.lock();
if(inst==NULL)
inst=new T();
lock.unlock();
}
return inst;
}
};
STL容器默认不是线程安全的