深刻理解互斥锁
对于上图中的加锁解锁汇编代码,是谁在执行呢?答案是调用的线程。
这里圈出来的汇编代码的意思是:将共享数据交换到自己的私有上下文当中。这是什么意思呢下面我们详细的讲解一下:
首先我们的线程进来后加锁然后向自己的上下文写入0就是上面图中这一局代码,在这里要记住未来我们切换线程的时候会将这个上下文中的0带走的,因为我们定义的锁在内存中存放,既然是内存那么注定了这个锁是共享的。然后我们执行下一条指令就是倒数第二张图中红色部分,这里就是将寄存器里的值和mutex变量里的内容做交换,以前我们的寄存器是0内存是1,现在变成了1,0如下图:
这也就证明了我们刚刚说的将共享数据交换到自己的私有上下文当中 ,这就是加锁的原理。
这个时候进入if语句,因为我们刚刚和寄存器做了交换,所以这次直接加锁成功返回0.在这里我们提一句,如果还没进入if判断语句线程就被切换了会怎么样呢?这个时候第一个线程会带走自己的上下文,也就是说寄存器里的1没有了被线程带走了,如下图:
这个时候第二个线程进来了,第二个进程也要申请锁,所以先和内存中的mutex做交换,因为刚刚mutex的1已经被第一个线程拿走了,所以交换完寄存器和mutex还是0,这个时候进入if判断语句发现不大于0只能挂起等待,后续来申请锁的线程都是如此,因为1(钥匙)已经被第一个线程拿走了!!这个时候操作系统将第一个线程拿过来,然后发现寄存器中的内容1大于0然后就申请锁成功了。所以上面的mutex的1只能进行流转,不会新增任何的1。解锁也很简单,直接将执行流中的mutex改为1即可,因为解锁只有一条指令,所以相当于原子性的解锁了。
class Thread
{
public:
typedef enum
{
NEW = 0,
RUNNING,
EXITED
} ThreadStatus;
typedef void (*func_t)(void*);
private:
pthread_t _tid;
std::string _name;
void*_args;
func_t _func; //线程未来要执行的回调
ThreadStatus _status;
};
首先我们把线程内部的内容写出来,线程中需要有线程的状态,和函数指针完成回调函数,以及线程的id,名称,可变参数列表等,我们对于函数指针的设计完全是和库里面的一样的,下面我们把需要的函数写出来:
Thread(int num, func_t func, void* args)
:_tid(0)
,_status(NEW)
,_func(func)
,_args(args)
{
char name[128];
snprintf(name,sizeof(name),"thread-%d",num);
_name = name;
}
对于线程内部的初始化我们直接将线程id初始化为0(注意一旦我们创建新线程后新线程的id会返回给tid),状态为new,然后将外面的函数指针传过来还有可变参数列表,在函数体内完成线程名称的打印。
int status()
{
return _status;
}
std::string threadname()
{
return _name;
}
线程状态和线程名称都可以直接返回给用户,下面我们实现一下run接口:
void run()
{
int n = pthread_create(&_tid,nullptr,runHelper,this);
if (n!=0)
{
exit(1);
}
_status = RUNNING;
}
run接口就是创建线程了,我们这里的参数就是线程内部的tid,创建成功后tid会变成新线程的id,如果没有创建成功就退出,让其执行run函数,执行run函数还需要传我们的线程对象,因为下面的run函数是static类型无法访问类内私有成员。我们还需要将状态设为运行。
static void *runHelper(void* args) //static后无this指针,满足create接口的第三个参数的要求
{
Thread* ts = (Thread*)args;
(*ts)();
return nullptr;
}
void operator ()()
{
_func(_args);
}
run函数为了完成回调工作首先必须是static函数,因为类内成员函数会默认多一个参数,这个参数是this指针,而我们的回调函数只有一个参数是void*的,所以用static,然后我们将args强转为thread*,下面实现一个仿函数,仿函数是可以直接调用func函数,所以我们的线程对象使用()就会调用func函数。
void join()
{
int n = pthread_join(_tid,nullptr);
if (n!=0)
{
std::cerr<<"main thread join thread"<<_name<<"error"<
等待线程也很简单,如果等待不成功就打印错误码,将状态改为退出状态。
pthread_t threadid()
{
if (_status==RUNNING)
{
return _tid;
}
else
{
std::cout<<"thread is not running,no tid"<
返回线程id前需要先判断该线程是否是运行状态,只有运行状态我们才返回其id值,否则就打印错误。
下面我们测试一下我们的线程:
#include "mythread.hpp"
#include
using namespace std;
void threadRun(void* args)
{
std::string message = static_cast(args);
while (true)
{
cout<<"我是一个线程,"<
通过运行我们可以看到封装的线程并没有问题,下面我们把锁也自己封装一下然后用我们自己的的线程试一下
二、demo版的锁的封装
#include
#include
class Mutex //自己不维护锁,有外部传入
{
public:
Mutex(pthread_mutex_t *mutex)
:_pmutex(mutex)
{
}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{
}
private:
pthread_mutex_t *_pmutex;
};
对于锁的封装就非常简单了,首先有一个锁的指针,初始化的时候把外部那个锁传给我们的指针,然后通过这个指针去进行加锁解锁操作。
class LockGuard //自己不维护锁,由外部传入
{
public:
LockGuard(pthread_mutex_t *mutex)
:_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
然后我们在用一个类,里面有一个锁对象,当这个对象创建的时候会自动加锁,销毁的时候自动解锁,下面我们演示一下:
int main()
{
Thread t1(1, threadRoutine, (void*)"hello world1");
Thread t2(2, threadRoutine,(void*)"hello world2");
Thread t3(3, threadRoutine,(void*)"hello world3");
Thread t4(4, threadRoutine,(void*)"hello world4");
t1.run();
t2.run();
t3.run();
t4.run();
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadRoutine(void* args)
{
std::string message = static_cast(args);
while (true)
{
pthread_mutex_lock(&mutex); //所有线程都要遵守这个规则
if (tickets>0)
{
usleep(2000); //模拟抢票花费的时间
cout<
运行后抢票逻辑也是没有问题的,下面我们引入我们自己封装的锁:
我们自己的锁只要在这个作用域就是加锁状态,出了作用域就销毁了用起来非常的方便:
运行后和刚刚库里的锁一模一样。