Linux_线程互斥
互斥的相关概念
- 共享资源:指多个进程或线程可以共同访问和操作的资源
- 临界资源:被保护的共享资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进⼊临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
一个不加保护的Demo
这里使用多个线程共同执行上面的方法,代码很简单。但是运行结果怎么出现了负数?等于0不就直接break了吗?
原因有以下两点:
- 代码if(XXXX)不是原子操作,ticketnum--也不是原子操作
- 所有的线程在尽可能多的进行调度切换执行 --- 线程或者进程什么时候会切换?
- a.时间片耗尽
- b.更高优先级的进程要调度
- c.通过sleep,从内核返回用户时,会进行时间片是否到达的检测,进而导致切换
当我们执行上述代码时,每个线程都要这样执行上面的逻辑,但cpu的寄存器只有一套,但是寄存器中的数据有多套,且数据为线程私有。由于ticketnum--操作不是原子的(即,将ticketnum的值移动到CPU,CPU做运算,再将结果写回内存。共三步)。当一个线程正走到以上逻辑的第二步时,正准备判断,此时这个线程被切换了,一旦被切换,当前线程在寄存器中数据都会保存下来,等在被切回来的时候,再恢复!
当票数为1时,a线程会做判断,符合逻辑进入if,走到usleep语句;此时b线程也进来了,a将寄存器中的数据带走,此时b线程见到的票数也是1,b线程也符合逻辑,进入if,也会走到usleep;同样的c和d线程都会做以上线程的动作,都会进入if。当a过了usleep时间,会执行--操作(1.重读数据2.--数据3.写回数据),此时票数为0了,同样的b,c,d线程也会做--,因为它们已经进入了if中。最后就导致票数为-2的情况了。
互斥量mutex
在Linux中互斥量就是锁。
要解决上述多线程并发引起的安全问题,我们只需在进入临界区之前加上一把锁,就可以完美解决。
互斥量(锁)的相关接口
pthread_mutex_init
: 初始化互斥锁。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 使用宏值初始化(全局)
mutex
:指向要初始化的互斥锁对象的指针。
attr
:指定互斥锁属性的对象,如果传递NULL,则使用默认的互斥锁属性。
pthread_mutex_init
函数若调用成功,会返回0
。若发生错误,会返回一个非零的错误码。
pthread_mutex_destroy
: 销毁互斥锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock
: 锁定互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock
: 解锁互斥锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
锁接口的使用
全局锁
// 定义一个全局锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int ticketnum = 10000; // 共享资源,临界资源
void Ticket()
{
while (true)
{
pthread_mutex_lock(&lock); // 加锁
if (ticketnum > 0)
{
usleep(1000);
printf("get a new ticket, id: %d\n", ticketnum--);
pthread_mutex_unlock(&lock); // 解锁
}
else{
pthread_mutex_unlock(&lock); // 解锁
break;
}
}
}
int main()
{
// 创建多线程的逻辑,调用Tichet
return 0;
}
局部锁
使用ThreadData接收参数,包括锁的接收,保证每一个线程都能看到同一把锁
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>
namespace ThreadModule
{
// 要传递的参数
struct ThreadData
{
ThreadData(const std::string &name, pthread_mutex_t *lock_ptr)
: _name(name)
, _lock_ptr(lock_ptr)
{}
std::string _name;
pthread_mutex_t *_lock_ptr;
};
// 执行任务的方法
using func_t = std::function<void(ThreadData*)>;
// 线程状态-枚举
enum class TSTATUS
{
NEW,
RUNNING,
STOP
};
class Thread
{
private:
// 成员方法,具备this指针,置为static之后就不具备this指针了
static void *Routine(void *args)
{
// t就拿到了this指针
Thread *t = static_cast<Thread *>(args);
t->_status = TSTATUS::RUNNING;
t->_func(t->_td); // 就可以执行相应的类内方法了
return nullptr;
}
public:
// 线程要执行的方法直接传进来
Thread(const std::string &name, func_t func, ThreadData* td)
: _name(name)
, _func(func)
, _td(td)
, _status(TSTATUS::NEW)
, _joinable(true)
{}
bool Start()
{
if (_status != TSTATUS::RUNNING)
{
int n = ::pthread_create(&_tid, nullptr, Routine, this); // 将this指针通过参数传过去
if (n != 0)
return false;
return true;
}
return false;
}
bool Stop()
{
if (_status == TSTATUS::RUNNING)
{
int n = ::pthread_cancel(_tid);
if (n != 0)
return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}
bool Join()
{
if (_joinable)
{
int n = ::pthread_join(_tid, nullptr);
if (n != 0)
return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}
std::string Name() { return _name; }
~Thread() {}
private:
std::string _name; // 线程名字
pthread_t _tid; // 线程id
bool _joinable; // 是否是分离状态,默认不是
func_t _func; // 线程未来要执行的方法
TSTATUS _status; // 线程状态
ThreadData* _td; // 要传递的参数
};
}
让每个线程都获取局部锁的地址,在每个线程在执行抢票逻辑的时候,将锁的地址传给加锁函数,就能实现局部加锁了。
#include "Thread.hpp"
#include <vector>
int ticketnum = 10000;
void Ticket(ThreadModule::ThreadData *td)
{
while(true)
{
pthread_mutex_lock(td->_lock_ptr); // 加锁
if(ticketnum > 0)
{
// 抢票
printf("get a new ticket, who get it: %s, id: %d\n", td->_name.c_str(), ticketnum--);
pthread_mutex_unlock(td->_lock_ptr);// 解锁
}
else
{
pthread_mutex_unlock(td->_lock_ptr);// 解锁
break;
}
}
}
#define NUM 4
int main()
{
// 创建局部锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
// 创建线程对象
std::vector<ThreadModule::Thread> threads;
for(int i = 0;i < NUM; i++)
{
std::string name = "thread-" + std::to_string(i+1);
// 把锁的地址给到td对象
ThreadModule::ThreadData *td = new ThreadModule::ThreadData(name, &mutex);
// 之后在将td给到Thread
threads.emplace_back(name, Ticket, td);
}
// 启动线程
for(int i = 0; i< NUM;i++)
threads[i].Start();
// 等待线程
for(int i = 0; i< NUM;i++)
threads[i].Join();
// 释放锁
pthread_mutex_destroy(&mutex);
return 0;
}
锁的相关问题
1. 锁本身是全局的,那么锁也是共享资源!谁保证锁的安全?
pthread_mutex:加锁和解锁被设计成为原子的了
2. 如何看待锁呢?二元信号量就是锁!
2.1 加锁本质就是对资源展开预订!
2.2 整体使用资源!!
3. 如果申请锁的时候,锁被别人已经拿走了,怎么办?
其他线程要进行阻塞等待
4. 线程在访问临界区代码的时候,可以不可以切换?可以切换!!
4.1 我被切走的时候,别人能进来吗?不能!因为我是抱着锁,被切换的!临界区的代码就是被串行的!这也是加锁效率低的原因!也体现了原子性(要么不做,要么做完)!
锁是如何实现的
现在大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
在内核中,为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。
将%al看成一个寄存器,把 0 movb到 %al中,xchgb将内存中的变量与寄存器中的做了直接的交换,不需要中间变量。(我们假定mutex一开始的数据是:1表示锁没有被申请;0表示锁被申请了)。线程执行判断,如果%al中的内容>0,则申请锁成功然后返回,否则挂起等待,等待完成被唤醒,goto lock重新申请锁。
- CPU的寄存器只有一套,被所有的线程共享。但是寄存器中的数据,属于执行流上下文,属于执行流私有的数据!
- CUP在执行代码的时候,一定要有对应的执行载体 -- 线程&&进程。
- 数据在内存中,是被所有线程所共享的
结论:把数据从内存移动到寄存器,本质是把数据从共享,变成线程私有!
重新理解加锁
当线程A执行第一行代码时,此时%al寄存器中为0,内存mutex中为1(图1);执行第二条代码时内存中mutex中的数据与%al进行交换,变为%al中值为1,mutex的值为0(图2);我们假设线程A执行第三行代码的时候被切换走,线程A会保存自身的上下文,带走%al中的数据,此时线程A处在第三行。
这时线程B来了,并且开始走第一行和第二行代码,由于内存中mutex的值为0(还是处于图2的状态),交换之后%al的值还是0。所以当线程B执行到第3行代码的时候只能跳到第6行,进行挂起等待。
线程B被挂起,线程A被重新切回,并恢复上文数据,从第三行开始执行,进入if,调用接口pthread_mutex_lock,return 0表示加锁成功,进入临界区。所以此时线程A被称为:申请锁成功。在上面代码中,加锁就是执行第二行代码:xchgb,只有一条汇编代码,交换不是拷贝,只有一个“1”,持有1的,就表示持有锁!
当线程A执行完临界区的代码后,进行解锁,执行第八行代码,将自身持有的“1”movb到内存中(这样就回到了图1的状态),接着唤醒正在等待mutex的线程B,线程B被唤醒后,执行第七行代码,继续goto lock重新申请锁。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/524275.html 如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!