目录
1.铺垫
2.线程锁接口的认识
静态锁分配
动态锁的分配
互斥量的销毁
互斥量加锁和解锁
3.加锁版抢票
4.互斥的底层实现
1.铺垫
先提一个小场景,有1000张票,现在有4个进程,这四个进程疯狂的去抢这1000张票,看看会发生什么呢?
#include <iostream>
#include <thread>
#include <unistd.h>
#include <vector>
#include "thread.hpp"
class ticket
{
public:
static int tickets;//总共的票数
ticket(std::string &name)
:_name(name)
{}
std::string &name()
{
return _name;
}
int _count = 0;//抢到多少票
std::string _name;//线程的名字
};
int ticket::tickets = 1000;
void handler(ticket *t)
{
while(true)
{
if(t->tickets > 0)
{
usleep(10000);
std::cout<< t->name()<<"tickets-garbbing ticket:"<<t->tickets<<std::endl;
t->tickets--;
t->_count++;
}
else
{
break;
}
}
}
using namespace mythread;//自己封装的线程库
int count = 4;
int main()
{
std::vector<thread<ticket*>> threads;
// 创建一批线程
std::vector<ticket*> data;
for (int i = 0; i < count; i++)
{
std::string name = "thread" + std::to_string(i);
ticket *t = new ticket(name);
data.push_back(t);
threads.emplace_back(handler, t, name);
}
//启动一批线程
for(auto &t : threads)
{
t.start();
}
//等待一批线程
for(auto &t : threads)
{
std::cout <<t.name() <<" wait sucess "<<std::endl;
t.join();
}
//查看结果
for(auto p : data)
{
std::cout << p->_name<<" get tickets "<<p->_count<<std::endl;
sleep(1);
}
return 0;
}
我们发现这四个线程竟然把票数抢到负数了,代码中已经判断if(t->tickets > 0)为什么票数还会减为0呢?
假设当前tickets只剩下1时。
thread0进行判断,thread0发现票数是大于0的,他就会进入循环,但是这个时候thread0的时间片到了,thread0进入等待队列。
thread1开始执行,thread1进行判断,thread1发现票数也是大于0的,进入循环,这个时候hread1的时间片到了,thread1进入等待队列。
thread2和thread3同样。
当cpu再次调度到thread0的时候,thread0对thickets--, thickets = 0.
调度到thread1的时候,thread1对thickets--,tickets = -1.
thread2和thread3同样。
这也就解释了,为什票会抢到负数,究其原因就是我们抢票+判断的操作不是原子的,所以我们要通过互斥锁把这两个操作编程"原子"的,这个原子是在线程看来是原子的,不是真正意义上的原子。
也可以理解为把线程并行抢票,变成串行抢票,因为锁只有一把,一次只能有一个线程抢票。
2.线程锁接口的认识
线程锁有两种分配方法,静态全局锁和局部锁
静态锁分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态锁的分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)
mutex:是要初始化的互斥量。
attr: nullptr。
互斥量的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:1.使用PTHREAT_MUTEX_INITIALIZER初始化的静态锁不用销毁。
2.互斥量加锁了,就不要销毁了。
3.销毁的互斥量,就不要在加锁了。
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);返回值 : 成功返回 0, 失败返回错误码。
注意:lock的时候会有两种情况,一种是lock成功返回0。
另一种是互斥量已经被lock,这时候该线程会阻塞等待。
3.加锁版抢票
静态锁板
#include <iostream>
#include <thread>
#include <unistd.h>
#include <vector>
#include "thread.hpp"
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义一个全局锁
class ticket
{
public:
static int tickets; // 总共的票数
ticket(std::string &name)
: _name(name)
{
}
std::string &name()
{
return _name;
}
int _count = 0; // 抢到多少票
std::string _name; // 线程的名字
};
int ticket::tickets = 1000;
void handler(ticket *t)
{
// pthread_mutex_lock(&mutex);不能在这里上锁,在这里上锁,一个线程就把票抢完了
while (true)
{
pthread_mutex_lock(&mutex);
if (t->tickets > 0)
{
usleep(10000);
std::cout << t->name() << "tickets-garbbing ticket:" << t->tickets << std::endl;
t->tickets--;
t->_count++;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
using namespace mythread; // 自己封装的线程库
int count = 4;
int main()
{
std::vector<thread<ticket *>> threads;
// 创建一批线程
std::vector<ticket *> data;
for (int i = 0; i < count; i++)
{
std::string name = "thread" + std::to_string(i);
ticket *t = new ticket(name);
data.push_back(t);
threads.emplace_back(handler, t, name);
}
// 启动一批线程
for (auto &t : threads)
{
t.start();
}
// 等待一批线程
for (auto &t : threads)
{
sleep(1);
std::cout << t.name() << " wait sucess " << std::endl;
t.join();
}
// 查看结果
for (auto p : data)
{
std::cout << p->_name << " get tickets " << p->_count << std::endl;
sleep(1);
}
return 0;
}
动态锁板
在主函数定义一个局部锁
然后在ticket类中,增加一个互斥量,这个互斥量是要加引用的,为了所有的线程都能看见同一个锁。
#include <iostream>
#include <thread>
#include <unistd.h>
#include <vector>
#include "thread.hpp"
class ticket
{
public:
static int tickets; // 总共的票数
ticket(std::string &name, pthread_mutex_t &mutex)
: _name(name), _mutex(mutex)
{
pthread_mutex_init(&mutex, nullptr);
}
std::string &name()
{
return _name;
}
int _count = 0; // 抢到多少票
std::string _name; // 线程的名字
pthread_mutex_t &_mutex;//让所有的线程看到同一个锁
};
int ticket::tickets = 1000;
void handler(ticket *t)
{
// pthread_mutex_lock(&mutex);不能在这里上锁,在这里上锁,一个线程就把票抢完了
while (true)
{
pthread_mutex_lock(&t->_mutex);
if (t->tickets > 0)
{
usleep(10000);
std::cout << t->name() << "tickets-garbbing ticket:" << t->tickets << std::endl;
t->tickets--;
t->_count++;
pthread_mutex_unlock(&t->_mutex);
}
else
{
pthread_mutex_unlock(&t->_mutex);
break;
}
}
}
using namespace mythread; // 自己封装的线程库
int count = 4;
int main()
{
std::vector<thread<ticket *>> threads;
// 创建一批线程
pthread_mutex_t mutex;
std::vector<ticket *> data;
for (int i = 0; i < count; i++)
{
std::string name = "thread" + std::to_string(i);
ticket *t = new ticket(name, mutex);
data.push_back(t);
threads.emplace_back(handler, t, name);
}
// 启动一批线程
for (auto &t : threads)
{
t.start();
}
// 等待一批线程
for (auto &t : threads)
{
sleep(1);
t.join();
std::cout << t.name() << " wait sucess " << std::endl;
}
// 查看结果
for (auto p : data)
{
std::cout << p->_name << " get tickets " << p->_count << std::endl;
sleep(1);
}
return 0;
}
运行结果
票是不会抢到负数了,但是出现了个问题。
为什么有的线程一个票也没抢到?
这个是因为不同的线程竞争能力不同,竞争能力强的就可以一直抢到锁,而竞争能力不强的就只能等待。
这个需要是用条件变量解决,下次介绍。
4.互斥的底层实现
互斥的底层是依赖swap 和exchange这两条指令的,这两条指令是原子的。
正常交换两个变量都需要,定义一个临时变量。
但是swap 和exchange这两条指令不用,可以直接交换,cpu寄存和内存的内容进行交换。
将lock和unlock的过程转化为伪代码(粗略只为了解原理)。
假设内存中存在一个mutex锁,mutex = 1时是解锁状态,mutex = 0是上锁状态
我们发现1只有一个,哪个线程拿到1,哪个线程能继续执行代码,否则就要挂起等待。
重要的话
放在内存中的数据是所有线程共享的,但是一旦被加载到cpu中,就变成cpu的上下文数据,变成了线程私有的数据