一、线程互斥
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
抢票代码:
一共有5000张票,创建4个线程,最后打印出每个线程抢到的票数
int g_ticket = 5000;
class DataThread
{
public:
DataThread(int& ticket, const string& name)
:_ticket(ticket), _name(name), _total(0)
{}
~DataThread()
{}
string _name;
int& _ticket;//全局的
int _total;
};
void route(DataThread* td)
{
while(true)
{
//抢票
if(td->_ticket > 0)
{
usleep(1000);
printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
td->_ticket--;
td->_total++;
}
else
{
break;
}
}
}
const int num = 4;
int main()
{
vector<Thread<DataThread*>> threads;
vector<DataThread*> datas;
for(int i = 0; i < num; i++)
{
string name = "thread-" + to_string(i+1);
DataThread* td = new DataThread(g_ticket, name);
threads.emplace_back(route, td, name);
datas.emplace_back(td);
}
//创建
for(auto& thread:threads)
{
thread.Satrt();
}
//等待
for(auto& thread:threads)
{
thread.Join();
}
//打印每个线程获取的票数
for(auto& data:datas)
{
cout << data->_name << " total is: " << data->_total << endl;
}
return 0;
}
为什么出现负数?因为ticket是共享资源,没有被保护,对它的判断不是原子的。在ticket等于1的时候,就可能有多个线程进入判断逻辑,然后都要执行把数据从内存读取到CPU,减法操作,再写回内存。这就导致出现了负数的情况
解决方法:加锁
定义的锁是全局的或静态的:
定义的锁是局部的:
加锁:
申请锁成功,函数就会返回,可以继续运行
申请锁失败,函数就会阻塞,不可以继续运行
函数调用失败,出错返回
解锁:
防止出现并发访问的问题,要保护全局共享资源,保护共享资源本质是保护临界区,访问临界资源的代码叫临界区。加锁是为了让多线程串行执行,即一个线程成功加上锁,让它访问临界资源,其他线程要等待,直到这个线程访问完才可以让其他线程竞争锁。这个过程叫互斥。
全局加锁代码:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局加锁
void route(DataThread* td)
{
while(true)
{
pthread_mutex_lock(&mutex);//加锁
//抢票
if(td->_ticket > 0)
{
usleep(1000);
printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
td->_ticket--;
pthread_mutex_unlock(&mutex);//解锁
td->_total++;
}
else
{
pthread_mutex_unlock(&mutex);//解锁
break;
}
}
}
局部加锁代码:
class DataThread
{
public:
DataThread(int& ticket, const string& name, pthread_mutex_t& mutex)
:_ticket(ticket), _name(name), _total(0), _mutex(mutex)
{}
~DataThread()
{}
string _name;
int& _ticket;//全局的
int _total;
pthread_mutex_t& _mutex;//共用一把锁
};
void route(DataThread* td)
{
while(true)
{
pthread_mutex_lock(&td->_mutex);//加锁
//抢票
if(td->_ticket > 0)
{
usleep(1000);
printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
td->_ticket--;
pthread_mutex_unlock(&td->_mutex);//解锁
td->_total++;
}
else
{
pthread_mutex_unlock(&td->_mutex);//解锁
break;
}
}
}
const int num = 4;
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);//初始化锁
vector<Thread<DataThread*>> threads;
vector<DataThread*> datas;
for(int i = 0; i < num; i++)
{
string name = "thread-" + to_string(i+1);
DataThread* td = new DataThread(g_ticket, name, mutex);
threads.emplace_back(route, td, name);
datas.emplace_back(td);
}
//创建
for(auto& thread:threads)
{
thread.Satrt();
}
//等待
for(auto& thread:threads)
{
thread.Join();
}
//打印每个线程获取的票数
for(auto& data:datas)
{
cout << data->_name << " total is: " << data->_total << endl;
}
pthread_mutex_destroy(&mutex);//释放锁
return 0;
}
优化代码,对线程和锁进行封装——RAII风格
#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__
#include <iostream>
#include <pthread.h>
using namespace std;
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);//构造加锁
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);//析构解锁
}
private:
pthread_mutex_t *_mutex;
};
#endif
void route(DataThread* td)
{
while(true)
{
LockGuard guard(&td->_mutex);//临时对象,构造即加锁,析构即解锁
//抢票
if(td->_ticket > 0)
{
usleep(1000);
printf("%s is run..., ticket: %d\n", td->_name.c_str(), td->_ticket);
td->_ticket--;
td->_total++;
}
else
{
break;
}
}
}
互斥的底层原理:
内存中有一个数据为1,CPU要处理线程1,首先,线程1会把0带入到寄存器中,然后寄存器中的数据与内存中的数据作交换,这样线程1得到了数据为1,这个过程线程1完成了加锁。如果要解锁,就换回去。又来了个线程2,此时线程1切换为线程2(注意:CPU执行线程1时是可以随时被切换的),线程1要带走寄存器中的1,然后线程2把0带入到寄存器中,再与内存的数据作交换,还是0,说明锁被其他线程占用,线程2就会挂起等待。再到线程1,1放会寄存器中,然后CPU处理线程1,加锁的只有线程1,所以线程1可以访问临界资源,即互斥。
CPU寄存器硬件只有一套,但是CPU寄存器内部的数据,可以有多套;
数据在内存里,所有线程都可以访问,是共享的,但是转移到CPU寄存器中就是一个线程私有的;
交换的本质:不是拷贝到寄存器,而是所有的线程竞争一把锁
正在访问临界区资源的线程可以被OS调度吗?
不能。一个线程正在访问临界区,还没有释放锁,对于其他线程来说,要竞争到锁,要么这个锁已经被释放,要么这个锁没有被前面那个线程申请成功。总结:加锁的线程访问临界区,不能被OS调度切换,此时(加锁的线程)访问临界区是原子的(是线程安全的)。
二、线程同步
概念:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
条件变量:
是什么?
栗子:一个人放苹果到盘子,但是不许说话;另一个人从盘子上拿苹果,但是蒙上眼睛(看不见);一个负责放,另一个负责拿,不能同时进行,是互斥的。放苹果的要申请到锁,才能放苹果,拿苹果的也要申请到锁,才能去盘子上拿苹果(可能盘子上没有苹果)。总之就是两个人要竞争一把锁,如果拿苹果的人竞争性更强,每次都是他申请到锁,放苹果的人就没机会放苹果了,这也导致拿苹果的人每次申请到锁后去盘子上总是没有苹果拿,但是他又一直占用锁(每次申请到锁的都是他),所以放苹果的不能申请到锁放苹果,拿苹果的在盘子上也没有苹果拿,这就导致整个过程很不合理。
解决方式:放苹果的人有一个铃铛,他申请到锁,并往盘子上放了苹果,即确定盘子上有苹果时,他就会敲响一下铃铛,提醒拿苹果的人可以来申请锁并且去拿苹果了。如果没有铃铛没有响,拿苹果的人就不要一直去申请锁,这样放苹果的人就有机会去申请锁然后放苹果。
拿苹果的人不止有一个,可以有多个,组成一个队列。铃铛响多次,表示让多个人来申请锁,竞争到锁的去拿苹果,然后返回队列,没竞争到锁的继续等待。条件变量等价于这个铃铛和多个线程组成的队列(保证顺序性)。
认识接口:
全局条件变量:
初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,constpthread_condattr_t *restrict attr);
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待:
int pthread_cond_wait(pthread_cond_t *restrictcond,pthread_mutex_t *restrict mutex);
唤醒一个线程:
int pthread_cond_signal(pthread_cond_t *cond);
唤醒所有等待的线程:
int pthread_cond_broadcast(pthread_cond_t *cond);
验证线程同步代码:
一个Master控制其他线程,条件变量+锁,然后将等待的线程唤醒
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;//条件变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁
void *MasterCore(void *args)
{
string name = static_cast<char *>(args);
while (true)
{
sleep(3);
cout << "Master开始工作.."<<endl;
//将线程一个一个唤醒
pthread_cond_signal(&gcond);
cout<<"唤醒一个线程:"<<endl;
sleep(1);
}
return nullptr;
}
void *SlaverCore(void *args)
{
string name = static_cast<char *>(args);
while (true)
{
//加锁
pthread_mutex_lock(&gmutex);
//设置条件变量,让线程在等待队列
pthread_cond_wait(&gcond, &gmutex);
cout << "线程在等待:" << name<<endl;
//解锁
pthread_mutex_unlock(&gmutex);
}
return nullptr;
}
void StartSlaver(vector<pthread_t> *tidptr, int num)
{
for (int i = 0; i < num; i++)
{
pthread_t tid;
char *name = new char[64];
snprintf(name, 64, "thread-%d", i + 1);
int n = pthread_create(&tid, nullptr, SlaverCore, name);
if (n == 0)
{
cout << "Slaver Thread create success" << endl;
}
tidptr->emplace_back(tid);
}
}
void StartMaster(vector<pthread_t> *tidptr)
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, MasterCore, (void *)"MasterThread");
if (n == 0)
{
cout << "Master Thread create success" << endl;
}
tidptr->emplace_back(tid);
}
void WaitThread(vector<pthread_t> tidptr)
{
for(auto& tid:tidptr)
{
pthread_join(tid, nullptr);
}
}
int main()
{
vector<pthread_t> tids;
StartMaster(&tids);
StartSlaver(&tids, 5);
WaitThread(tids);
return 0;
}