在这漫长的暑假,学习让我感到不再孤单,一起为了秋招努力吧!(有一位一起学习的朋友,最近拿到了腾讯的实习,好羡慕~)
目录
进程线程间的互斥相关背景概念
多执行流下没有互斥锁带来的问题
线程不安全的原因
互斥量接口
pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
pthread_mutex_lock
pthread_mutex_unlock
互斥锁&抢票逻辑代码
C++11里面的互斥锁
锁的原理
汇编代码层次理解
补充
静态的互斥锁使用
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
是不是感觉太官方了,我们来看一下稍微通俗的解释(方便将来面试来讲):
我们先不使用互斥锁,实现一个抢票逻辑看会出现什么问题。
代码测试:
test.cc
#include
#include
#include
using namespace std;
int tickes = 1000;
void* ThreadRun(void* args)
{
int id = *(int*)args;
delete (int*)args;
while(true)
{
if(tickes > 0)
{
//抢票
usleep(1000);
cout << "我是[" << id <<"] 我要抢的票是: " << tickes << endl;
tickes--;
}
else
{
break;
}
}
}
int main()
{
pthread_t tid[5];
for(int i = 0; i < 5; i++)
{
int* id = new int(i);
pthread_create(tid+i, nullptr, ThreadRun, (void*)id);
}
for(int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
Makefile
test:test.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f test
运行结果:
这时候我们会发现,多个线程在抢票过程中,会抢到负数的情况,这就意味着票卖多了。这种情况是商家不愿意看到的。所以这段代码在多执行流下存在线程安全问题!
实际上ticket--并非原子性的,它实际上要进行计算是要经过多步的,在这期间就有可能被多个线程重入,导致可能出现问题。
如何解决上面的问题呢?对临界区进行加锁!
1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
pthread_mutex_t* restrict mutex:传入要初始化锁的地址
const pthread_mutexattr_t* restrict attr:我们不用关心,设置成nullptr
pthread_mutex_t* mutex:传入要释放锁的地址
定义在全局的或静态的锁,不用再手动初始化和销毁,直接用就可以了。
pthread_mutex_t* mutex:传入锁的地址进行上锁
pthread_mutex_t:传入锁的地址,给对应的锁解开
test.cc
#include
#include
#include
using namespace std;
class Ticket
{
public:
Ticket()
:_tickes(1000)
{
pthread_mutex_init(&_mtx, nullptr);
}
~Ticket()
{
pthread_mutex_destroy(&_mtx);
}
bool GetTicket()
{
bool rets = true;//rets是每个线程私有的
//对临界区进行加锁保护
//执行这部分代码的执行流是互斥的,也是串行的
pthread_mutex_lock(&_mtx);
if(_tickes > 0)
{
//抢票
usleep(1000);
cout << "我是[" << pthread_self() << "] 我要抢的票是: " << _tickes << endl;
_tickes--;
}
else
{
//没有票
printf("票已经抢空了\n");
rets = false;
}
pthread_mutex_unlock(&_mtx);
return rets;
}
private:
int _tickes;
pthread_mutex_t _mtx;
};
void* ThreadRun(void* 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, ThreadRun, (void*)t);
}
for(int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
运行结果(部分):
这时候我们发现不会出现负数的情况了。
运行结果:
原子性:一行代码经过编译后,只有一行汇编代码。ticket--经过汇编是有多行代码的,但是互斥量在设计的时候,绝对不是 "--" 设计的,而是使用上面的方法完成原子性的逻辑。
实际上只有互斥还是存在一定的问题,比如一个线程申请锁处理完逻辑释放锁之后,它会再次竞争锁,甚至在其他线程还没唤醒之前又进行申请锁释放锁的逻辑。因为其它在挂起等待的线程被唤醒是有一定代价的,竞争力明显小于刚释放锁的线程,除非它的时间片到了或收到信号,这时候被挂起的线程才有可能竞争到锁。
实际上临界区我们加完锁之后,就不用担心重入的问题了。
test.cc
pthread_mutex_t mymtx = PTHREAD_MUTEX_INITIALIZER;
class Ticket
{
public:
bool GetTicket()
{
bool rets = true;//rets是每个线程私有的
//对临界区进行加锁保护
//执行这部分代码的执行流是互斥的,也是串行的
pthread_mutex_lock(&mymtx);
if(_tickes > 0)
{
//抢票
usleep(1000);
cout << "我是[" << pthread_self() << "] 我要抢的票是: " << _tickes << endl;
_tickes--;
}
else
{
//没有票
printf("票已经抢空了\n");
rets = false;
}
pthread_mutex_unlock(&mymtx);
return rets;
}
private:
int _tickes = 1000;
pthread_mutex_t _mtx;
};
void* ThreadRun(void* 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, ThreadRun, (void*)t);
}
for(int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
运行结果:
看到这里,给博主点个赞吧~