宜昌做网站哪家最便宜,爱站网站长百度查询权重,网络推广目标,网站创建桌面快捷方式十、多线程中线程间的独立
1.线程在代码段通过执行不同的函数#xff0c;实现代码段的独立#xff1b;
2.新线程通过在共享区划分不同的管理属性和不同的栈空间#xff0c;实现栈的独立#xff0c;而主线程使用的是栈空间#xff1b;
3.线程通过获…十、多线程中线程间的独立
1.线程在代码段通过执行不同的函数实现代码段的独立
2.新线程通过在共享区划分不同的管理属性和不同的栈空间实现栈的独立而主线程使用的是栈空间
3.线程通过获取到不同的堆空间地址实现堆的独立
总结线程是通过对共享资源的划分来实现资源的独立的但是只要将其他线程的资源地址添加到当前线程当前线程也是可以访问到的下图main thread访问了thread-2的局部变量 const int num 10;
void *threadRoutine(void *args)
{const char *s static_castconst char *(args);int cnt 1;while (cnt){cnt;cout thread name: s pid: getpid() endl;if (cnt 10)break;}return nullptr;
}
string str;int main()
{vectorpthread_t vtid;for (int i 0; i num; i){pthread_t tid;str thread;str : ;str to_string(i);pthread_create(tid, nullptr, threadRoutine, (void *)str.c_str());vtid.push_back(tid);}for (int i 0; i vtid.size(); i){pthread_join(vtid[i], nullptr);}return 0;
}十一、线程局部存储
11.1__thread
__thread是编译器提供的一个编译选项让被修饰变量在线程的局部存储里都开辟一份本质上就不是同一个变量了需要注意的是**__thread只能修饰内置类型**通常是用来存储线程的属性tid、pid之类的
__thread int val_
//定义一个全局变量被__thread修饰全局变量对于每一个线程都是各自私有一份
//此技术叫做线程的局部存储11.2局部存储与独立栈的区别
独立栈主要是用来保证调用链的安全里面放的是临时变量
而局部存储类似全局数据区存放的数据是随线程的整个生命周期的
十二、线程分离
类似进程非阻塞等待
默认创建的新线程是joinable需要被等待的不进行等待无法释放资源会造成内存泄露主线程不关心新线程的退出情况那是由进程来关心的主线程关心的是函数执行完之后的返回值如果主线程不想知道新线程的返回值就不需要阻塞等待此时要进行线程分离
线程分离即新线程自己将内核管理的LWP结构和库管理的tcb结构释放
注意1.线程分离之后是不可以被join的2.必须保证主线程是最后一个退出的否则会导致整个进程的退出
一个线程是否被分离是需要被记录下来的所以一个线程被分离本质就是将本线程的tcb里的线程可标志位设置为1
12.1线程分离接口
#include pthread.h
int pthread_detach(pthread_t thread);
//即不需要主线程阻塞等待执行完之后自己释放
//可以由主线程进行分离
pthread_t tid;
pthread_detach(tid);
//新线程自己释放自己
pthread_detach(pthread_self());十三、线程的同步与互斥
多线程并发访问共享资源的时候是有数据不一致问题的
互斥任何时候访问资源只有允许有一个执行流
同步按照一定的顺序性获取资源
原子性在代码上表现为只有一个汇编指令只有执行前和执行后两种状态
对一个全局变量进行多线程并发–/操作是不安全的因为如–操作需要进行三步操作1.将内存中的数据读取到CPU当中2.在CPU内部进行–操作3.将计算结果写回内存
即CPU执行是按照汇编走的应用层的一行代码会被转换成多条汇编指令所以一行代码不具有原子性
CPU的寄存器保存着线程的上下文当线程被换走的时候要从寄存器中拿走上下文换回是要将上下文恢复到寄存器中对于–操作最后是要将寄存器中的内容写入到内存当中的对于共享资源多线程并发访问时当前线程因为时间片结束导致数据没有被修改并将数据保存到上下文中其他线程对数据成功进行了修改当线程恢复上下文是就会用自己的上下文修改数据但是此时的数据已经不是刚从内存读取到的数据了这时修改内存数据就会有问题
总结多线程并发访问时会使得数据同时被修改造成数据不一致问题
解决方式任何时候共享数据只允许有一个执行流即互斥这样某个线程执行的时候其他线程就不会修改共享数据使用锁的方式实现互斥
互斥场景下1.由于线程对于锁的竞争能力不一样会导致其他线程饥饿问题2.由于大量线程是被阻塞的一旦资源就绪默认所有线程就会都从阻塞态变成运行态竞争锁但是最多只有一个线程可以访问其他线程是无效的被伪唤醒了而且会导致被唤醒的线程CPU都要执行一次检测逻辑这就叫做惊群问题
解决方式1.所有的线程必须排队2.释放完锁的线程不能立马申请锁排到队尾
这样可以让线程获取锁有一定的顺序至少保证了每个线程都有机会获得锁是相对公平的
十四、互斥锁
锁是一种共享资源是通过保证申请和释放锁的原子性来保证访问锁是安全的
先定义一把锁然后初始化和销毁期间对临界区加锁和解锁
14.1锁的初始化和释放
#include pthread.h
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//参数是锁的地址
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//第一个参数是锁的地址第二个参数是锁的属性一般为nullptr
pthread_mutex_t/*是库里面提供的一种数据类型*/ mutex PTHREAD_MUTEX_INITIALIZER;
//使用全局或者静态的方式进行初始化之后是不需要destroy的14.2加锁和解锁
#include pthread.h
int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁申请成功可以访问失败阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);//非阻塞加锁解决死锁问题;
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁;底层设计是为了解决死锁问题14.3加锁后的缺陷
加锁会导致加锁区域进行串行访问本质上是以时间换取安全所以应该尽量保证临界区即加锁的区域的代码要少好处是执行时间少串行比率降低其他线程等待的时间减少
加锁产生饥饿问题
14.4加锁的细节
1.申请锁成功才可以继续往后执行不成功就会阻塞等待资源就绪
2.加锁之后必须解锁否则会导致锁资源不就绪引发其他线程一直阻塞的问题所以要保证锁一定可以被解锁保证不会因为进行跳转不能执行到解锁
3.不同线程对锁的竞争能力不同不同线程对锁的访问是并发的为了保证竞争公平要进行同步
4.如果线程长时间得不到锁就会导致饥饿问题即纯互斥环境锁资源分配不够合理导致其他进程的饥饿问题
5.加锁后的线程也是可以被切换的但是此线程还是持有锁没有释放锁其他线程是无法访问临界区的因为其他线程只关心当前线程是持有锁还是释放锁即是此线程对于其他线程是原子的
14.5互斥锁的底层实现
大部分芯片在设计时一定要内置汇编指令集才能执行各种复杂的逻辑
互斥锁底层使用了swap这样的指令作用是将内存和寄存器中的值进行交换一条指令所以是原子的即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期因为多处理器也只有一套总线和内存连接CPU访问内存是要经过硬件仲裁器决定有哪一个CPU去访问内存即多处理器CPU访存还是串行的只是计算是并发的
伪代码
每一个线程的上下文都是私有的即寄存器中的数据都是属于线程的线程的结构中上下文是软件上下文在寄存器中是硬件上下文锁本质上就是一个数字1只有一个swap就可以保证锁申请成功其他指令都是后续动作
交换的本质就是将内存中的数据(共享数据)交换到寄存器中也就是线程的硬件上下文(私有数据)中如果当前线程继续是申请锁就会导致唯一的1也变成0即没人能够申请锁了死锁了
lock://8086的ax寄存器是16位的后来eax是32位e是expand扩展的意思分为了ah和al各16位movb $0%al//将寄存器al的值设为0xchgb al mutex//将寄存器的值交换到变量mutex里原本mutex是1变为了0这就是申请锁的过程if(al寄存器的内容 0)//申请成功此时寄存器内部就是1否则就是失败挂起等待{return 0;)else挂起等待;goto lock;
unlock:movb $1 mutex//将内存中的mutex变量置为1这样的实现就可以使得其他线程来释放锁而不是必须当前线程来释放锁这样就可以实现两个线程的同步如果使用的是swap就必须保证是申请锁成功的线程来释放锁为解决死锁问题提供了方案唤醒等待Mutex的线程;return 0;14.6锁的应用——封装
#include pthread.hclass mutex
{
public:mutex(pthread_mutex_t *lock) : lock_(lock) {}void lock(){pthread_mutex_lock(lock_);}void unlock(){pthread_mutex_unlock(lock_);}~mutex() {}private:pthread_mutex_t *lock_;
};
class lockguard
{
public:lockguard(pthread_mutex_t *lock) : mutex_(lock){mutex_.lock();}~lockguard(){mutex_.unlock();}private:mutex mutex_;
};
//可以使用代码块{}来划分对象的的生命周期14.7死锁
同一把锁申请两次就会形成死锁原因是底层锁其实就是1申请了两次使得整个过程中1丢失了所以死锁了
14.7.1概念
死锁是指在一组执行流中的各个执行流均占有不会释放的资源但因互相申请被其他执行流所占用不会释放的资源而处于的一种永久等待状态。
14.7.2死锁四个必要条件
互斥条件一个资源每次只能被一个执行流使用
请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放 默认使用的lock接口就是请求与保持的
不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
缺一不可
14.7.3解决死锁问题
a.破坏死锁的必要条件如
1.不使用互斥2.使用pthread_mutex_trylock(pthread_mutex_t *mutex)申请失败返回然后将自己的锁释放3.由于释放锁的底层是直接赋1所以可以由其他线程释放当前线程的锁的4.每一个线程申请锁的顺序要一致否则容易形成环路申请问题
b.加锁顺序一致
c.避免锁未释放的场景
d.资源一次性分配
相关算法1.死锁检测算法2.银行家算法
十五、线程安全问题
线程安全多个线程并发同一段代码时不会出现不同的结果。
常见的线程不安全的情况
1.不保护共享变量的函数
2.函数状态随着被调用状态发生变化的函数
3.返回指向静态变量指针的函数
4.调用线程不安全函数的函数
重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。
一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。