
因为线程是共享地址空间的,就会共享大部分资源,这种共享资源就是公共资源,当多执行流访问公共资源的时候,就会出现各种情况的数据不一致问题。为了解决这种问题,我们就需要学习线程的同步与互斥,本篇将介绍线程的互斥。
1.相关概念
- 临界资源:多线程执⾏流被保护的共享资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤
- 原⼦性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1.1 数据不一致
⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
下面是一个模拟抢票的简单代码。
1#include <stdio.h> 2#include <stdlib.h> 3#include <string.h> 4#include <unistd.h> 5#include <pthread.h> 6 7int ticket = 100; 8void *route(void *arg) 9{ 10 char *id = (char *)arg; 11 while (1) 12 { 13 if (ticket > 0) // 判断票的数量 14 { 15 usleep(1000); // 模拟抢票的时候花费的时间 16 printf("%s sells ticket:%d\n", id, ticket); // 假设这里就是抢到票了 17 ticket--; // 更新票的数量 18 } 19 else 20 { 21 break; 22 } 23 } 24 return nullptr; 25} 26 27int main() 28{ 29 pthread_t t1, t2, t3, t4; 30 pthread_create(&t1, NULL, route, (void *)"thread 1"); 31 pthread_create(&t2, NULL, route, (void *)"thread 2"); 32 pthread_create(&t3, NULL, route, (void *)"thread 3"); 33 pthread_create(&t4, NULL, route, (void *)"thread 4"); 34 35 pthread_join(t1, NULL); 36 pthread_join(t2, NULL); 37 pthread_join(t3, NULL); 38 pthread_join(t4, NULL); 39 return 0; 40}
运行后会发现,这个票数居然还减到了负数。

主要是因为usleep让所有线程在判断tickets>0时,全部进到判断里,但是usleep却不让线程往后执行,大大提升了线程被同时进到临界区的机会,tickets就会被减到负数。(不一致原因详情:课42)
全局资源没有加保护就可能会有并发问题,这也是线程安全问题。

1.2 见一见锁
解决上面出现的问题我们可以给临界区代码加锁。

mutex互斥锁,也叫互斥量,它的类型就叫pthread_mutex_t,使用锁需要头文件pthread.h。
使用的时候先对锁初始化,直接用PTHREAD_MUTEX_INITIALIZER这个宏初始化就行。
1int ticket = 100; 2pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义一把锁并初始化 3void *route(void *arg) 4{ 5 char *id = (char *)arg; 6 while (1) 7 { 8 pthread_mutex_lock(&lock); // 加锁 9 if (ticket > 0) // 判断票的数量 10 { 11 usleep(1000); // 模拟抢票的时候花费的时间 12 printf("%s sells ticket:%d\n", id, ticket); // 假设这里就是抢到票了 13 ticket--; // 更新票的数量 14 pthread_mutex_unlock(&lock); // 解锁 15 } 16 else 17 { 18 pthread_mutex_unlock(&lock); // 防止走到else时锁没解 19 break; 20 } 21 } 22 return nullptr; 23}

可以看到加锁之后就没有出现数据被减到负数了,而且还能感受到这个代码的运行速度变慢了。
2.认识mutex
- 全局的锁:这种方式定义的锁不用被释放,程序运行结束会自动释放。

- 局部的锁:就要用到相关的函数,初始化锁的函数第二个参数就是锁的一些属性,不用管。局部的锁要调用destroy释放。

- 不管是全局的还是局部的锁,线程在访问公共资源之前都要申请锁,lock加锁,unlock解锁。线程申请锁成功,继续向后运行,申请失败会阻塞挂起申请执行流。trylock是非阻塞版本,不考虑。

所有线程都要竞争申请锁,所以首先所有线程都要看到锁,所以锁本身就是临界资源;锁是用来保护临界区资源的,但是谁来保护锁?所以要求锁的申请和解除必须是原子的。
锁提供的能力本质就是:执行临界区代码的执行流由并行转为串行。
2.1 接口使用
前面我们已经使用过全局的锁了,现在就用一下局部锁。
1#include <stdio.h> 2#include <stdlib.h> 3#include <string.h> 4#include <unistd.h> 5#include <pthread.h> 6#include <string> 7#include <iostream> 8 9int ticket = 100; 10struct Data 11{ 12 Data(const std::string &name, pthread_mutex_t *plock) 13 : _name(name), 14 _plock(plock) 15 {} 16 17 std::string _name; 18 pthread_mutex_t *_plock; 19}; 20 21void *route(void *arg) 22{ 23 Data* d = static_cast<Data *>(arg); 24 while (1) 25 { 26 pthread_mutex_lock(d->_plock); // 加锁 27 if (ticket > 0) 28 { 29 usleep(1000); 30 printf("%s sells ticket:%d\n", d->_name.c_str(), ticket); 31 ticket--; 32 pthread_mutex_unlock(d->_plock); // 解锁 33 } 34 else 35 { 36 pthread_mutex_unlock(d->_plock); // 防止走到else时锁没解 37 break; 38 } 39 } 40 return nullptr; 41} 42 43int main() 44{ 45 pthread_mutex_t lock; // 局部锁 46 pthread_mutex_init(&lock, nullptr); // 对锁初始化 47 48 Data d1("thread 1", &lock); 49 Data d2("thread 2", &lock); 50 Data d3("thread 3", &lock); 51 Data d4("thread 4", &lock); 52 53 pthread_t t1, t2, t3, t4; 54 pthread_create(&t1, NULL, route, &d1); 55 pthread_create(&t2, NULL, route, &d2); 56 pthread_create(&t3, NULL, route, &d3); 57 pthread_create(&t4, NULL, route, &d4); 58 59 pthread_join(t1, NULL); 60 pthread_join(t2, NULL); 61 pthread_join(t3, NULL); 62 pthread_join(t4, NULL); 63 64 pthread_mutex_destroy(&lock); // 销毁锁 65 66 return 0; 67}
操作还是比较简单的。
对临界区资源进行保护,本质就是用锁对临界区代码进行保护。

- 加锁之后,在临界区内部,依旧允许线程切换,因为当前线程并没有释放锁,依旧持有锁,带着锁被切换的,其他的线程必须等我回来执行完代码,将锁释放后,他们才可以展开对锁的竞争从而进入临界区。
- 这把锁要么没被使用,要么已经被使用完了,这两种状态才对其他线程有意义,这就体现了原子性。
- 在线程访问临界区资源时不会被其他线程打扰,也是一种变相的原子性的表现。
2.2 mutex的原理
硬件实现:关闭时钟中断(了解即可)。
软件实现:为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange汇编指令,该指令的作⽤是把寄存器和内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,下面有段伪代码。

申请锁
进程/线程切换:CPU内部的寄存器硬件只有一套,但是CPU寄存器的数据可以有多份,每份就是当前执行流的上下文数据。
把一个变量的内容换到CPU内部,其实就是把变量的内容获取到当前执行流的硬件上下文中,CPU寄存器的硬件上下文属于进程/线程私有的。
我们用swap或exchange将内存中的变量交换到寄存器中,其实就是当前进程/线程在获取锁,是交换,而不是拷贝,所以锁只有一份,谁申请谁持有。

当后面来的执行流想申请锁,首先会把寄存器清0,然后在交换的这一步时,就只会用0换0,因为这个1已经被之前的线程申请走了,此时申请锁失败,线程就会阻塞挂起。

解锁
解锁的时候,只需要往内存里的mutex写1。

2.3 C++里的mutex
1#include <mutex> //需要包含的头文件 2 3std::mutex cpp_mutex; //定义锁 4 5cpp_mutex.lock(); //加锁 6cpp_mutex.unlock(); //解锁 7

3.封装mutex
1//Mutex.hpp文件 2#include <iostream> 3#include <pthread.h> 4#include <cstring> 5#include <cstdio> 6 7namespace MyMutex 8{ 9 class Mutex 10 { 11 public: 12 Mutex() 13 { 14 pthread_mutex_init(_plock, nullptr); // 锁初始化 15 } 16 void Lock() // 加锁 17 { 18 int n = pthread_mutex_lock(_plock); 19 if (n != 0) 20 std::cerr << "pthread_mutex_lock fail: " << strerror(n) << std::endl; 21 } 22 void UnLock() // 解锁 23 { 24 int n = pthread_mutex_unlock(_plock); 25 if (n != 0) 26 std::cerr << "pthread_mutex_unlock fail: " << strerror(n) << std::endl; 27 } 28 29 ~Mutex() 30 { 31 pthread_mutex_destroy(_plock); // 锁释放 32 } 33 34 private: 35 pthread_mutex_t *_plock; 36 }; 37}
1//测试 2#include <stdio.h> 3#include <stdlib.h> 4#include <string.h> 5#include <unistd.h> 6#include <string> 7#include <iostream> 8#include "Mutex.hpp" 9 10using namespace MyMutex; 11 12int ticket = 100; 13struct Data 14{ 15 Data(const std::string &name, Mutex *plock) 16 : _name(name), 17 _plock(plock) 18 { 19 } 20 21 std::string _name; 22 Mutex *_plock; 23}; 24 25void *route(void *arg) 26{ 27 Data *d = static_cast<Data *>(arg); 28 while (1) 29 { 30 d->_plock->Lock(); // 加锁 31 if (ticket > 0) 32 { 33 usleep(1000); 34 printf("%s sells ticket:%d\n", d->_name.c_str(), ticket); 35 ticket--; 36 d->_plock->UnLock(); // 解锁 37 } 38 else 39 { 40 d->_plock->UnLock(); // 解锁 41 break; 42 } 43 } 44 return nullptr; 45} 46 47int main() 48{ 49 Mutex lock; //用自己实现的锁 50 Data d1("thread 1", &lock); 51 Data d2("thread 2", &lock); 52 Data d3("thread 3", &lock); 53 Data d4("thread 4", &lock); 54 55 pthread_t t1, t2, t3, t4; 56 pthread_create(&t1, NULL, route, &d1); 57 pthread_create(&t2, NULL, route, &d2); 58 pthread_create(&t3, NULL, route, &d3); 59 pthread_create(&t4, NULL, route, &d4); 60 61 pthread_join(t1, NULL); 62 pthread_join(t2, NULL); 63 pthread_join(t3, NULL); 64 pthread_join(t4, NULL); 65 66 return 0; 67}
我们还可以进一步封装这个锁,让他可以自动的加锁解锁。需要在实现一个LockGuard类。
1#include <iostream> 2#include <pthread.h> 3#include <cstring> 4#include <cstdio> 5 6namespace MyMutex 7{ 8 class Mutex 9 { 10 public: 11 Mutex() 12 { 13 pthread_mutex_init(_plock, nullptr); // 锁初始化 14 } 15 void Lock() // 加锁 16 { 17 int n = pthread_mutex_lock(_plock); 18 if (n != 0) 19 std::cerr << "pthread_mutex_lock fail: " << strerror(n) << std::endl; 20 } 21 void UnLock() // 解锁 22 { 23 int n = pthread_mutex_unlock(_plock); 24 if (n != 0) 25 std::cerr << "pthread_mutex_unlock fail: " << strerror(n) << std::endl; 26 } 27 28 ~Mutex() 29 { 30 pthread_mutex_destroy(_plock); // 锁释放 31 } 32 33 private: 34 pthread_mutex_t *_plock; 35 }; 36 37 class LockGuard 38 { 39 public: 40 LockGuard(Mutex *mutex) 41 : _mutex(mutex) 42 { 43 _mutex->Lock(); // 构造时加锁 44 } 45 46 ~LockGuard() 47 { 48 _mutex->UnLock(); // 析构时解锁 49 } 50 51 private: 52 Mutex *_mutex; 53 }; 54}
1void *route(void *arg) 2{ 3 Data *d = static_cast<Data *>(arg); 4 while (1) 5 { 6 { 7 LockGuard lock_guard(d->_plock); 8 if (ticket > 0) 9 { 10 usleep(1000); 11 printf("%s sells ticket:%d\n", d->_name.c_str(), ticket); 12 ticket--; 13 } 14 else 15 { 16 break; 17 } 18 } 19 } 20 return nullptr; 21}
这个就叫做RAII风格的互斥锁实现。
本篇分享就到这里,我们下篇见~

《【Linux】线程的互斥》 是转载文章,点击查看原文。