一、锁的定义
线程加锁是在多线程编程环境中,为了确保在同一时刻只有一个线程能够访问特定的共享资源或执行特定的代码段,而采取的一种同步手段,通过在需要保护的资源或代码段前获取锁,在访问完成后释放锁,来实现对共享资源的互斥访问
二、库函数
1、初始化互斥锁
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 返回值:成功返回0,失败返回非零错误码
mutex
:表示要初始化的互斥锁,pthread_mutex_t
是posix线程库中定义的互斥锁类型attr
:包含互斥锁的属性,设置为null
表示使用默认属性
2、销毁互斥锁
#include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 返回值:成功返回0,失败返回非零错误码
mutex
:表示要销毁的互斥锁
3、加锁
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex);
- 返回值:成功返回0,失败返回非零错误码
mutex
:表示要加锁的互斥锁
4、解锁
#include <pthread.h> int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 返回值:成功返回0,失败返回非零错误码
mutex
:表示要解锁的互斥锁
5、示例
#include <iostream> #include <pthread.h> #include <vector> #include <cstdio> #include <unistd.h> using namespace std; //定义一个全局锁就可以不需要初始化和销毁锁的函数了 //pthread_mutex_t lock = pthread_mutex_initializer; #define num 4 //共500张票 int tickets = 500; class threadinfo { public: threadinfo(const string &threadname, pthread_mutex_t *lock) :threadname_(threadname) ,lock_(lock) {} public: string threadname_; pthread_mutex_t *lock_; }; void *grabtickets(void *args) { threadinfo *ti = static_cast<threadinfo*>(args); string name(ti->threadname_); while(true) { pthread_mutex_lock(ti->lock_); // 加锁 if(tickets > 0) { usleep(10000); printf("%s get a ticket: %d\n", name.c_str(), tickets); tickets--; pthread_mutex_unlock(ti->lock_); // 解锁 } else { pthread_mutex_unlock(ti->lock_); // 解锁 break; } //这里上面的代码 usleep(13); // 用休眠来模拟抢到票的后续动作 } printf("%s quit...\n", name.c_str()); } int main() { pthread_mutex_t lock; // 定义互斥锁 pthread_mutex_init(&lock, nullptr); // 初始化互斥锁 vector<pthread_t> tids; vector<threadinfo*> tis; for(int i = 1; i <= num; i++) { pthread_t tid; threadinfo *ti = new threadinfo("thread-"+to_string(i), &lock); pthread_create(&tid, nullptr, grabtickets, ti); tids.push_back(tid); tis.push_back(ti); } // 等待所有线程 for(auto tid : tids) { pthread_join(tid, nullptr); } // 释放资源 for(auto ti : tis) { delete ti; } // 销毁互斥锁 pthread_mutex_destroy(&lock); return 0; }
这样就不会出现好多线程抢到一张票或者抢到不存在的票的问题了
三、深入理解锁
1、解读锁的机制
(一)先入为主原则
我们将上方代码中表示抢到票后续动作的休眠代码注释掉再次执行程序我们会发现,都是线程1抢的票,多次执行代码之后发现这是概率性问题,但是在抢票的时候,有一段时间的票都是一个线程抢到的,我们预想的应该是几乎平均分配的样子
这说明了几个问题:
- 第一,线程对于锁的竞争能力不同,一定有一个首先抢到锁的线程
- 第二,一般来说,刚解锁再去抢锁的更容易一些,类似于上面的结果,一直是线程1在抢票
(二)锁和线程
- 对于上面第二个问题来说,我们有处理方法,这种方法就是同步,同步可以让所有的线程按照一定的顺序获取锁
- 对于其他线程来讲,一个线程要么获取到了锁,要么释放了锁,当前进程访问临界区的过程对于其他线程是原子的
在加锁期间,即解锁之前,是可以发生线程切换的,线程切换的时候是拿着锁走的,被锁起来的内容其他线程也是访问不到临界区的的,在该线程再次切换回来的时候,恢复线程上下文继续访问临界区代码
(三)锁的特点
加锁的本质就是用时间来换取安全,我们知道在加锁后,临界区的代码只能由一个线程执行,如果是并发执行,至少时间要缩短5倍,但是锁给我们消除了安全隐患,即可能出现的++
、--
的隐患
加锁的表现就是线程对于临界区代码串行执行,一条线从上到下
我们加锁的原则就是尽量保证临界区的代码要少一些,可以使单线程执行的代码量更小,多线程综合处理的代码量更大,提高效率
锁的本身是共享资源,所以加锁和解锁本身就被设计成为了原子性操作(加锁和解锁通过硬件提供的原子指令,结合操作系统内核态的底层同步原语支持以及库层面的合理封装,来确保操作的原子性),这样可以确保在多线程环境下对共享资源加锁和解锁操作的完整性与一致性,避免因多线程并发干扰导致锁状态异常,进而保障线程安全和数据的正确性
2、锁的原理
下面来看一下加锁解锁对应的汇编指令,我们说,一条汇编指令就是原子性的
首先al寄存器中的数字为0时,代表锁已被拿走,为非零(一般为1)时,代表锁当前空闲,可以上锁
加锁机制:
- movb $0, %al:将值 0 移动到 al 寄存器
- xchgb %al, mutex:这是一个原子交换指令,将 al 寄存器中的值(即 0)与 mutex 变量的值交换
- if (al寄存器的内容 > 0):检查 al 寄存器中的内容(此时它保存的是原来 mutex 的值),如果值大于 0,说明互斥锁之前没有被锁定,锁定成功,返回 0
- else:如果 al 中的值是 0,说明互斥锁已经被锁定,程序会等待
- goto lock:程序跳转回 lock 标签,重新尝试获取锁
解锁机制:
- movb $1, mutex:将值 1 移动到 mutex
- xchgb %al, mutex:通过交换 al 中的值和 mutex,实现解锁
- return 0:解锁后,函数返回
四、锁的封装
1、lockguard.hpp
#pragma once #include <pthread.h> //简单的封装了一下函数,用的时候方便一些 class mutex { public: mutex(pthread_mutex_t *lock) :lock_(lock) {} void lock() { pthread_mutex_lock(lock_); } void unlock() { pthread_mutex_unlock(lock_); } private: pthread_mutex_t *lock_; }; class lockguard { public: lockguard(pthread_mutex_t *lock) :mutex_(lock) { mutex_.lock(); // 对象创建的时候加锁 } ~lockguard() { mutex_.unlock(); // 对象销毁的时候解锁 } private: mutex mutex_; };
#include <iostream> #include <pthread.h> #include <vector> #include <cstdio> #include <unistd.h> #include "lockguard.hpp" using namespace std; #define num 4 int tickets = 500; //全局变量定义锁 pthread_mutex_t lock = pthread_mutex_initializer; class threadinfo { public: threadinfo(const string &threadname) : threadname_(threadname) public: string threadname_; }; void *grabtickets(void *args) { threadinfo *ti = static_cast<threadinfo *>(args); string name(ti->threadname_); while (true) { { lockguard lockguard(&lock); // raii 风格的锁 if (tickets > 0) { usleep(10000); printf("%s get a ticket: %d\n", name.c_str(), tickets); tickets--; } else { break; } } usleep(13); // 用休眠来模拟抢到票的后续动作 } printf("%s quit...\n", name.c_str()); } int main() { vector<pthread_t> tids; vector<threadinfo *> tis; for (int i = 1; i <= num; i++) { pthread_t tid; threadinfo *ti = new threadinfo("thread-" + to_string(i)); pthread_create(&tid, nullptr, grabtickets, ti); tids.push_back(tid); tis.push_back(ti); } // 等待所有线程 for (auto tid : tids) { pthread_join(tid, nullptr); } // 释放资源 for (auto ti : tis) { delete ti; } pthread_mutex_destroy(&lock); return 0; }
这里封装的锁是raii风格的锁,raii风格是一种在 c++ 等编程语言中利用对象的构造和析构函数来自动管理资源的技术,确保资源在对象创建时获取,在对象生命周期结束时自动释放,以防止资源泄漏并简化资源管理
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论