加锁和解锁的原理
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
现在我们把lock和unlock的伪代码改一下:
假设现在是线程A运行,线程A进行了申请加锁,内存中的int当中的1就是锁。
首先让0放进CPU中的寄存器%al当中,然后将内存中的1%al中的0交换。
这里交换是一条汇编完成的。
这个时候,如果突然时间片到了,线程B换了上来,线程A就要带着自己的上下文走。
然后线程B从头开始,先将0放入%al,然后交换:
这里继续向下执行语句,发现寄存器%al中的内容并不大于0,走第二条语句,线程B就被挂起等待了。
然后线程A又切换回来继续向下执行:
这就是为什么当前线程申请锁之后其他线程无法申请锁!
解锁的过程就是将%al的1移动到内存中:
锁的封装
因为C语言很多接口是不兼容C++的,所以我们要想办法设计让锁的接口兼容C++。
#pragma once #include <iostream> #include <pthread.h> #include <cassert> #include <string> #include <cstring> #include <vector> #include <unistd.h> #include <cstdlib> #include <memory> using namespace std; class Mutex { public: Mutex(pthread_mutex_t *lock_p = nullptr):_lock_p(lock_p) {} void lock() { if(_lock_p) pthread_mutex_lock(_lock_p); } void unlock() { if(_lock_p) pthread_mutex_unlock(_lock_p); } ~Mutex() {} private: pthread_mutex_t *_lock_p; }; class LockGuard//这里像智能指针一样,自动解锁 { public: LockGuard(pthread_mutex_t *mutex):_mutex(mutex) { _mutex.lock(); } ~LockGuard() { _mutex.unlock(); } private: Mutex _mutex; };
#pragma once #include <iostream> #include <pthread.h> #include <cassert> #include <string> #include <cstring> #include <vector> #include <unistd.h> #include <cstdlib> #include <memory> using namespace std; class Thread;//声明 class Context//上下文,相当于一个大号的结构体 { public: Thread *this_; void* args_; public: Context():this_(nullptr),args_(nullptr) {} ~Context() {} }; class Thread { typedef function<void* (void*)> func_t; public: //这里需要加一个静态,因为不加静态就是类成员函数,还有一个隐藏的this指针,也就说明这等于前面有一个缺省参数 //所以在类内创建线程,想让对应的线程执行方法需要在方法前面加一个static static void* start_routine(void* args) { //但是静态方法不能调用成员方法或者成员变量,这里可以设置一个上下文 Context* ctx = static_cast<Context*>(args); void* ret = ctx->this_->run(ctx->args_);//这里让自身去调用这个方法 delete ctx; return ret; } void* run(void* args) { return _func(args);//调用该函数 } Thread(func_t func,void* args,int num):_func(func),_args(args) { char buffer[1024]; snprintf(buffer, sizeof(buffer), "thread_%d", num); _name = buffer; Context* ctx = new Context(); ctx->this_ = this; ctx->args_ = _args;//这里是将自身的部分数据传给ctx int n = pthread_create(&_tid, nullptr, start_routine, ctx);//这里要通过调用函数来转化,直接传func是不行的,因为类型是C++的类,不是C语言的类 assert(n==0); (void)n; } void join() { int n = pthread_join(_tid,nullptr); assert(n==0); (void)n; } ~Thread() {} private: string _name;//线程名字 pthread_t _tid;//线程id func_t _func;//线程调用的函数 void* _args;//传给函数的参数 };
#include "Thread.hpp" #include "MUtex.hpp" int tickets = 1000;//票数 class ThreadData { public: ThreadData(const string& threadname, pthread_mutex_t *mutex_p):_threadname(threadname),_mutex_p(mutex_p) {} ~ThreadData() {} public: string _threadname; pthread_mutex_t *_mutex_p; }; void* thread_run(void* args) { ThreadData* p = static_cast<ThreadData*>(args); LockGuard lockGuard(p->_mutex_p);//这里会自动加锁解锁 while(true) { {//这里的域是为了避免对下面的usleep进行加锁 if(tickets > 0) { usleep(1234);//1秒=1000毫秒=1000000微秒 cout << p->_threadname << "用户正在抢票:"<< tickets-- <<endl; } else { break; } } usleep(1234);//模拟抢完票形成一个订单,这里也就等于阻止了竞争力强的线程,让竞争力强的到后面排队去 } } int main() { pthread_mutex_t lock; pthread_mutex_init(&lock, nullptr);//初始化锁,第二个参数设为nullptr就可以 vector<pthread_t> arr(4); for(int i = 0;i < 4; i++) { char buffer[64]; snprintf(buffer,sizeof(buffer),"thread %d",i+1); ThreadData* p = new ThreadData(buffer, &lock); pthread_create(&arr[i], nullptr, thread_run, p); } for(const auto& e:arr) { pthread_join(e,nullptr); } pthread_mutex_destroy(&lock);//解锁 return 0; }
这种风格叫做RAII加锁。
可重入与线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
类或者接口对于线程来说都是原子操作。
多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
可重入函数体内使用了静态的数据结构。
可重入与线程安全联系
函数是可重入的,那就是线程安全的。
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种。
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生。
死锁,因此是不可重入的。
死锁
死锁的概念与条件
概念:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用。
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
上面代码有个例子,申请了两次锁,就等于自己本来有锁,又申请一次之后等于将自己挂起,这样谁也申请不到锁了。
避免死锁
破坏死锁的四个必要条件。
加锁顺序一致。
避免锁未释放的场景。(也就是用完锁一定要释放)
资源一次性分配。(不要到处给锁分配资源,不然看起来很乱,就容易造成死锁)
这里要注意一下,当前线程的锁可以被别的线程释放,上面的汇编语言释放锁的逻辑就说明了这一点。
避免死锁算法
死锁检测算法
银行家算法
注意:平时尽量不要用锁。






