Linux——线程(2)线程互斥(锁)
知识回顾
在学习本篇内容前,我们需要先回顾一下几个概念。
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有⼀个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量 归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。此时如果多个线程并发操作共享变量,可能会有一些问题。因此,我们需要理解互斥与同步的原理并对共享资源在合适的时候“加锁”。
我们所保护的,并不是所谓的共享资源,而是称为临界区的代码。在临界区内,我们通过加锁只允许一个线程执行。
加锁一定不能大块代码地进行加锁,只要把临界区的一部分加锁即可。
一、不加锁会出现什么问题
这里我们写了一个利用线程进行抢票功能的代码(线程封装是作者自己写的这里直接拿来用,实际中直接调用pthread_create即可)
#include <iostream>
#include "mythread.hpp"
#include <vector>
using namespace ThreadModule;#define NUM 4
int ticket = 10000; // 共享资源
void Ticket()
{while (true){if (ticket > 0){usleep(1000);// 抢票printf("get ticket success ! ticketnum:%d\n", ticket--);}else{break;}}
}int main()
{// 构建线程对象std::vector<Thread> threads;for (int i = 0; i < NUM; i++){threads.emplace_back(Ticket);}// 启动线程for (auto &thread : threads){thread.Start();}// 等待for (auto &thread : threads){thread.Join();}
}
按道理来说票数为正就会一直抢,直到0所有进程就会退出抢票,但我们运行时发现,票数居然出现负数了?!这是为什么呢?
这就需要我们最上面的概念——原子性,在ticket--的过程中一共有三步:把ticket从内存放在CPU,CPU进行--,再放回内存,这是对于每一个线程的过程,但是在这个过程中,如果我们把数据放进CPU那么这个数据就对于线程是私有的了(本身的ticket是全局变量所以共享资源),他们完成第一步时,这时线程突然切换了,结果到下一个线程本来应该拿到比上个线程少一的ticket,但因为上一个还没有--所以拿到的值是一样的,然后当他们都进行--,就会出现为负数的原因。所以,--操作并不是原子性的,(if也不是)我们要加锁本质就是不让线程在完成这些操作的过程中被切换。
二、关于锁
1.锁长什么样
我们可以定义一个pthread_mutex_t类型(pthread库中提供的类型)的变量,这就是一把锁,第二个接口就是对锁进行初始化。但如果我们定义的锁是全局或静态的,就可以用最下面的宏进行初始化(这种锁就不需要destroy)。
2.怎么加锁与解锁
我们只需要把锁的地址传进来就行了。
所以现在上面的情况就可以解决了。
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
int ticket = 10000; // 共享资源
void Ticket()
{while (true){pthread_mutex_lock(&lock);if (ticket > 0){usleep(1000);// 抢票printf("get ticket success ! ticketnum:%d\n", ticket--);pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}
}
只要线程启动就要保证原子性,完成后进行解锁。
锁作为全局变量,保护着全局变量的资源(临界资源),可是谁来保证锁的安全,锁只需要保证原子性即可。
线程在申请锁的时候,其他线程要进行阻塞等待!线程在访问临界区代码时,是可以切换的,但在加锁的线程被切走的时候,其他线程是无法进来访问临界区的,该线程要么不做,要么做完!这对于其他线程就保证了原子性。
其实,锁这个概念就是线程互斥,就是为了让线程在完成任务的过程中不让其他线程“加入”等自己完成后再让他们执行。
三、锁的封装
这里无非就是利用lock和unlock所以直接上代码
#pragma once
#include <iostream>namespace LockModule
{class Mutex{public:Mutex(const Mutex&)=delete;//需要保证多线程用一把锁,不允许赋值和拷贝const Mutex& operator =(const Mutex&)=delete;Mutex(){int n=::pthread_mutex_init(&_lock,nullptr);(void)n;}~Mutex(){int n=::pthread_mutex_destroy(&_lock);(void)n;}void Lock(){int n=::pthread_mutex_lock(&_lock);(void)n;}void Unlock(){int n=::pthread_mutex_unlock(&_lock);(void)n;}private:pthread_mutex_t _lock;};class LockGuard{public:LockGuard(Mutex&mtx):_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex& _mtx;};
}
封装lockguard类是通过构造和析构直接实现锁的初始化和回收