为什么会出现线程安全问题
众所周知,线程是共享同个内核的,并且用户区的一些资源也是共享的(除了栈区和.text区);那这就意味着堆区的数据以及全局变量存储的.bss和.data区都是共享的,那么当多个线程操作共享数据的时候,就会出现问题了;比如:
//用多线程实现卖票的案例
//有三个窗口,一共有100张票,3个窗口并发地卖100张票
#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
int i=100;
void *Sellticket(void *)
{
while(i>0)
{
usleep(6000);
cout<<pthread_self()<<" "<<"卖出的票:"<<i<<endl;
i--;
}
return NULL;
}
int main()
{
//创建3个子线程,子线程用来卖票
pthread_t pthid1,pthid2,pthid3;
pthread_create(&pthid1,NULL,Sellticket,NULL);
pthread_create(&pthid2,NULL,Sellticket,NULL);
pthread_create(&pthid3,NULL,Sellticket,NULL);
//回收子线程地资源
pthread_join(pthid1,NULL);
pthread_join(pthid2,NULL);
pthread_join(pthid3,NULL);
//退出主线程
pthread_exit(NULL);
return 0;
}
//出现线程无法同步地问题
会出现下面这样的结果:
那么为什么会出现这种情况呢?
假设A线程开始执行卖票,此时A线程就会对临界区的资源进行修改,那么此时A线程就会进行循环买票,此时CPU的资源正在被A线程占用,但是每次A线程卖票前都会执行usleep(6000),此时会释放锁占有的CPU资源,那么其他线程就会争相抢夺CPU资源,这个时候就会出现A线程进行到一半的时候,比如说A线程此时正在卖最后一张票,执行到usleep(6000)的时候,此时i=1;然后A线程释放资源,假设被B线程抢到了CPU资源,此时B线程又会执行usleep(6000),假设此时A线程还在睡眠中,那么CPU就会被C线程抢到并且进入睡眠状态,这个时候A线程的睡眠时间到了,A就会拿到CPU资源,并且执行i--,此时i就变味了0,按道理来说,票卖完了就不会在卖了,但是B线程和C线程都还处在循环内,所以当B拿到CPU资源后并不会去判断是否满足循环条件,所以此时会输出”卖出的票:0“,然后又执行i--;此时i就等于-1了,然后等C线程拿到CPU资源后,就会输出”卖出的票:-1“,但其实这是不可能的。那么这就是出现了数据安全问题了捏。
如何避免出现线程安全的问题呢?
自旋锁
”自旋“可以理解为”自我旋转“,这里的旋转是指循环,例如while或者for循环;自旋就是不停的循环直到达到退出循环的条件。
int flag=0;//flag=0表示锁没有被拿,1表示锁已经被拿走了
void Lock()
{
while(flag==1)
{
}
flag=1;
return;
}
void Unlock()
{
flag=0;
}
//额,自旋锁大概原理就是这样吧是这样吧
优点:
在有些场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果机器有多个CPU核心,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。
原文链接:https://blog.csdn.net/upstream480/article/details/122272332
缺点:
它最大的缺点就在于虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,这种开销也是水涨船高,后期甚至会超过线程切换的开销,得不偿失。
原文链接:https://blog.csdn.net/vincent_wen0766/article/details/108558656
互斥锁
互斥锁与自旋锁不同的点就在于当一个线程拿不到互斥锁的时候,这个线程不会进行自我循环一直等待并且占用CPU资源,而是阻塞,CPU去执行其他线程
互斥锁的一些API
//互斥锁类型
pthread_mutex_t
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutex_t *attr)
参数:
pthread_mutex_t *restrict mutex:需要初始化的互斥锁,restrict是C语言修饰符,被修饰的指针,不能由另一个指针进行操作;
pthread_mutex_t *attr:互斥量相关的属性,一般使用NULL(默认属性);
返回值:
成功返回0,失败返回-1
//摧毁互斥锁
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_unlock(pthread_mutex_t *mutex)
只要用了锁就一定没问题吗?其实并不是这样的,用锁的时候必须注意出现死锁的问题,什么是死锁?
死锁
所谓死锁,大概意思就是指某个锁一直被一个线程占用而不解锁,那么此时无论哪个线程都无法再继续访问被锁上的资源了,那么这个锁就是死锁
出现死锁的情况
加了锁后忘记释放锁,此时就会出现无论是持有锁的线程还是其他想要访问被锁上的资源的线程都会被阻塞,那么这个锁就是死锁
有时,一个线程需要同时访问两个或者多个不同的共享资源,而每个资源又都由不同的互斥锁来管理。当超过一个线程加锁同一组资源的时候,就有可能发生死锁
类似两个人过独木桥,双方都不愿意让步,那么双方就永远过不去了;举个栗子:A线程拿了A资源的锁,然后又想要访问B资源,B线程拿了B资源的锁,并且它想要访问A资源,那么此时就出现了冲突,如果A线程想要访问B资源,那么就必须等B资源的锁被解开,也就是B线程必须先解开B资源的锁,但是B线程解开B资源的锁的前提条件是先拿到A资源的锁,但是A资源的锁又被A线程拿了,想要解开A资源的锁,就必须先让A线程拿到B资源的锁。如此下去就会陷入一个死循环,那么此时就会出现死锁。
用锁的时候需要尽量避免出现死锁的情况
读写锁
互斥锁只要有一个线程拿到,那么其他线程无论是读取还是修改被锁上的数据都没办法做到,只能乖乖阻塞等到解锁,但是实际上呢,如果这个线程只是在对悲伤所得资源进行读操作,那么其他线程只是想要读取数据并不会导致出现线程安全问题,所以就出现了读写锁
读写锁的特点:
如果有一个线程读数据,则允许其他线程执行读操作,但不允许进行写操作
如果一个线程进行数据的时候,其他线程都不允许进行读,写操作
写是独占的,写的优先级最高
读写锁的相关API
//读写锁的类型
pthread_rwlock_t
读写锁和互斥锁很像
//初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlock attr_t * restrict attr);
//摧毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
还有一些别的方法也可以防止线程数据安全问题,例如条件变量和信号量,这些就等明天再说啦
题外话:字节的青训营好难受啊,要学习一门新的GO语言,而且好抽象啊!很多东西完全听不懂,我现在也就只会弄个简单的词典项目,还是跟着别人做的,让我换一个翻译引擎我就完全做不出来了捏,我好菜啊,我妈还花了好多钱给我报一个帮忙规划如何进央企、国企的班,我又得每天背八股文,现在真是一个头还几个大,看来这个年得一直卷下去了
2023.1.18