目录
问题引入
我们的多线程利用在我们的各个场景,最常见的就是在我们的买票的过程中,当多线程没有被我们进行保护,我们可以随便的访问我们的票数,导致出现购买的票号既然是负数,这显然是不可能的,这就是我们多线程中的共享数据的不一致问题,所以我们就需要有一个工具来保护我们的多线程,从而防止下图这种场景发生。

线程代码如下:该例以tickes为全局变量模拟票数。
void *getTicket(void *args)
{
threadData *td = static_cast<threadData *>(args);
string name = td->thread_name_;
while (true)
{
if (tickes > 0)
{
usleep(10000);
cout << name << " get a ticket: " << tickes << endl;
tickes--;
}
else
{
break;
}
}
return nullptr;
}
当我们知道这个现象是由共享数的不一致问题导致的,接下来我们来分析一下,为什么会出现这个现象,知道原因之后,就可以找方法来解决我们的问题。
在上述代码中,由于tickes是共享数据,假设我们创建四个多线程来模拟四个用户抢票,当我们的tickes>0的时候,就有票可以选,但是我们的多线程是并发的,有可能存在当只有最后一张票的时候,我们的四个线程都执行到了if(tickes>0)这个判断处,当执行usleep时,我们的线程就被切换了(带走了我们的上下文,同时带走了tickes的数值为1),导致我们的四个线程都进入了if语句之内,这就表示着,最后一种票同时有四个用户能够购买。由于我们的tickes--操作并不是原子的(他会被编译成多条汇编语句),他会被分为三步:1、先将tickes的值读入到cpu中,2、CPU内进行运行,3、将运算结果写会内存。当我们的某一个线程已经将我们tickes--操作执行到第三步的时候,我们的tickes在内存中的值就变为了0,当另一个线程(虽然被切换时上下文中的tickes是1,但是输出和其他操作需要重新读取内存)来读取tickes时,就变为了0,然后有进行--操作,导致我们出现上述的结果。
此时我们就知道了,这是说到底还是因为我们的对共享数据的保护不足,假设我们的多线程,在访问共享资源的时候,永远只有一个执行流进行访问,那是不是就可以保证我们共享数据的安全了呢?是的,我们将这种能够安全访问的共享数据叫做我们的临界资源
接下来我们便来说一说该怎么解决上述的问题,那就是我们的线程锁
Linux线程互斥
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互,例如我们上面的tickes就是共享变量
- 多个线程并发的操作共享变量,会带来一些问题,我们一开始的问题就是其中之一。
需要解决我们上面的这个问题需要做到三点:
-
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
-
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
-
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
互斥量的接口
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数:mutex :要初始化的互斥量attr : NULL(目前不需要了解设为空即可)
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 ,也就是全局的不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数
mutex:需要销毁的锁的地址
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);(阻塞加锁)
int pthread_mutex_trylock(pthread_mutex_t *mutex);(非阻塞加锁)
int pthread_mutex_unlock(pthread_mutex_t *mutex);(解锁)参数
mutex:锁的地址
返回值
成功返回0,失败返回错误号
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁,后再去申请锁,知道申请到锁,才可以进入临界区执行,这样就保证了我们当前线程在访问临界区的时候,对于其他的线程来说是原子的。
- 如果在纯互斥环境中,锁如果分配的不合理,就容易导致某些线程一直无法得到锁,一直在等待,导致出现了线程饥饿问题
此时我们就思考,既然我们的所以线程都可以访问锁,那我们的锁,是不是共享资源呢?是的!既然我们的共享锁是共享资源,那我们共享锁是怎么做到安全的,也就是原子操作的呢?

解决问题
当我们已经知道了加锁的方法之后,我们就可以来修改一下我们的代码了,当访问我们的共享资源之前我们就进行加锁处理,当出了临界区之后我们就将锁给释放掉。
修改后的现象如下:此时我们的票数的结果就和我们预期的现象一样了

源码如下:
#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
#define NUM 4
int tickes = 1000;
struct threadData//定义我们的线程数据结构体,存放线程姓名和锁
{
threadData(int num)
{
thread_name_ = "thread-" + to_string(num);
}
pthread_mutex_t *lock;
string thread_name_;
};
void *getTicket(void *args)
{
threadData *td = static_cast<threadData *>(args);
string name = td->thread_name_;
while (true)
{
pthread_mutex_lock(td->lock);//在访问tickes临界资源之前进行加锁
if (tickes > 0)
{
usleep(10000);
cout << name << " get a ticket: " << tickes << endl;
tickes--;
pthread_mutex_unlock(td->lock);//访问完成之后,解锁
}
else
{
pthread_mutex_unlock(td->lock);//如果没有票了也需要解锁
break;
}
usleep(133);
}
return nullptr;
}
int main()
{
vector<pthread_t> tids;
vector<threadData *> thread_datas;
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData *td = new threadData(i);
td->lock = &lock;
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i]);
tids.push_back(tid);
}
for (auto &e : tids)//进行线程等待
{
pthread_join(e, nullptr);
}
pthread_mutex_destroy(&lock);//摧毁锁
for (auto &e : thread_datas)
{
delete e;
}
return 0;
}

5002

被折叠的 条评论
为什么被折叠?



