线程安全问题
因为多个线程是共享地址空间的,也就是很多资源都是共享的。
- 优点:通信方便
- 缺点:缺乏访问控制
因为一个线程的操作问题,给其他线程造成了不可控,或者引起崩溃,异常,逻辑不正确等这种现象:线程安全。
创建一个函数没有线程安全问题的话,尽量不要使用全局,stl,malloc,new等会在全局内有效的数据。
使用的话,需要访问控制。
线程有自己的独立栈结构。
线程崩溃的影响一定是有限的,因为线程在进程内部,而进程具有独立性。
访问控制之一 —— 互斥
由于线程安全问题,需要引入访问控制:互斥、同步。
几个概念。 临界资源、临界区、互斥、原子性、同步
通过下面这个抢票的例子,发现线程安全问题
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
//抢票逻辑,1000张票,5线程同时抢
int tickes = 1000;
void *ThreadRoutine(void *args){
// string tname = (char*)args;
int id = *(int*)args;
delete (int*)args;
while(true){
//临界区
if(tickes > 0){
//抢票
usleep(10000); //usleep 微秒 1s = 1000ms 1ms = 1000us
cout << "我是[" << id << "]我要抢的票是:" << tickes << endl;
tickes--;
}
else{
//没有票
break;
}
// cout << tname << "is running ... " << endl;
// sleep(1);
}
}
int main(){
pthread_t tid[5];
for(int i = 0; i < 5; i++){
int *id = new int(i);
pthread_create(tid+i, nullptr, ThreadRoutine, id); //i值可能会被主线程修改,所以此处用在堆区新建的id
// pthread_create(tid+i, nullptr, ThreadRun, (void *)"thread 1");
}
for(int i = 0; i < 5; i++)
pthread_join(tid[i], nullptr); //等待
return 0;
}
tickets--
在此处并不安全
因此,需要对临界区进行加锁。
mutex 互斥锁
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
//抢票逻辑,1000张票,5线程同时抢
//对临界区进行加锁
class Ticket{
private:
int tickets;
pthread_mutex_t mtx;
public:
Ticket():tickets(1000){
pthread_mutex_init(&mtx, nullptr);
}
bool GetTicket(){
pthread_mutex_lock(&mtx);
//执行这部分代码的执行流就是互斥的,串行执行的
if(tickets > 0){
//抢票
usleep(1000); //usleep 微秒 1s = 1000ms 1ms = 1000us
cout << "我是[" << pthread_self() << "]我要抢的票是:" << tickets << endl;
tickets--;
}
else{
cout << "票已经被抢空了" << endl;
return false;
}
pthread_mutex_unlock(&mtx);
return true;
}
~Ticket(){
pthread_mutex_destroy(&mtx);
}
};
void *ThreadRoutine(void *args){
// string tname = (char*)args;
// int id = *(int*)args;
// delete (int*)args;
Ticket *t = (Ticket*)args;
while(true){
if(!t->GetTicket()) break; //抢票失败,退出
}
}
int main(){
Ticket *t = new Ticket();
pthread_t tid[5];
for(int i = 0; i < 5; i++){
// int *id = new int(i);
pthread_create(tid+i, nullptr, ThreadRoutine, (void*)t);
// pthread_create(tid+i, nullptr, ThreadRun, (void *)"thread 1");
}
for(int i = 0; i < 5; i++)
pthread_join(tid[i], nullptr); //等待
return 0;
}
运行结果:
因为使用了互斥锁,线程之间不会造成访问干扰和重入问题。
除了使用原生线程库里的锁,也可使用C++提供的库内的锁,包含在头文件
#include <mutex>
内,修改代码如下
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <mutex>
using namespace std;
//抢票逻辑,1000张票,5线程同时抢
//对临界区进行加锁
class Ticket{
private:
int tickets;
pthread_mutex_t mtx; //原生线程库,系统级别
mutex mytex; //C++语言级别
public:
Ticket():tickets(1000){
pthread_mutex_init(&mtx, nullptr);
}
bool GetTicket(){
// pthread_mutex_lock(&mtx);
mytex.lock();
//执行这部分代码的执行流就是互斥的,串行执行的
if(tickets > 0){
//抢票
usleep(1000); //usleep 微秒 1s = 1000ms 1ms = 1000us
cout << "我是[" << pthread_self() << "]我要抢的票是:" << tickets << endl;
tickets--;
}
else{
cout << "票已经被抢空了" << endl;
return false;
}
// pthread_mutex_unlock(&mtx);
mytex.unlock();
return true;
}
~Ticket(){
pthread_mutex_destroy(&mtx);
}
};
void *ThreadRoutine(void *args){
// string tname = (char*)args;
// int id = *(int*)args;
// delete (int*)args;
Ticket *t = (Ticket*)args;
while(true){
if(!t->GetTicket()) break; //抢票失败,退出
}
}
int main(){
Ticket *t = new Ticket();
pthread_t tid[5];
for(int i = 0; i < 5; i++){
// int *id = new int(i);
pthread_create(tid+i, nullptr, ThreadRoutine, (void*)t); //i值可能会被主线程修改,所以此处用在堆区新建的id
// pthread_create(tid+i, nullptr, ThreadRun, (void *)"thread 1");
}
for(int i = 0; i < 5; i++)
pthread_join(tid[i], nullptr); //等待
return 0;
}
运行效果与上述相同。
也可定义静态锁,如下,静态锁的使用。
class Ticket{
private:
int tickets;
// pthread_mutex_t mtx; //原生线程库
// mutex mytex; //C++语言级别
public:
Ticket():tickets(1000){
// pthread_mutex_init(&mtx, nullptr);
}
bool GetTicket(){
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定义一把静态锁
//锁本身也是临界资源,如何保证其是安全的?
//lock、unlock是原子性的
//一行汇编,即为原子
pthread_mutex_lock(&mtx);
// mytex.lock();
//执行这部分代码的执行流就是互斥的,串行执行的
if(tickets > 0){
//抢票
usleep(1000); //usleep 微秒 1s = 1000ms 1ms = 1000us
cout << "我是[" << pthread_self() << "]我要抢的票是:" << tickets << endl;
tickets--;
}
else{
cout << "票已经被抢空了" << endl;
return false;
}
pthread_mutex_unlock(&mtx);
// mytex.unlock();
return true;
}
~Ticket(){
// pthread_mutex_destroy(&mtx);
}
};
访问临界资源的时候,需要先访问mtx,前提是所有线程必须得看到它。所以,锁本身也是临界资源。lock
,unlock
是原子的,故而,保证了锁本身是安全的。
通过接下来的内容了解互斥锁(互斥量)的原理。
互斥锁实现原理探究
一行汇编,即是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
下面是lock以及unlock底层实现的伪代码:
死锁
-
一个线程造成死锁:在一个加锁程序中,又进行一次加锁操作,并且这个锁不是可重入锁,就会导致死锁。内部锁在等外部锁释放,外部锁需要走完整个流程才能释放锁。
-
多个线程造成死锁:一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件
避免死锁:
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
避免死锁算法:
- 死锁检测算法
- 银行家算法