上篇文章:Linux操作系统6- 线程4(POSIX线程的简单封装)-CSDN博客
本篇代码仓库:myLerningCode/l30 · 橘子真甜/Linux操作系统与网络编程学习 - 码云 - 开源中国 (gitee.com)
目录
一. 线程不互斥造成的结果
二. pthread_mutex_t 互斥锁操作
2.1 互斥锁的定义与初始化
a 动态分配初始化
b 静态分配
2.2 加解锁操作
三. 互斥锁的使用
四. 互斥锁总结
4.1 谁来保护锁?
4.2 加解锁的原子性问题
一. 线程不互斥造成的结果
线程是进程内的执行流,是CPU调度的基本单位,线程之间有共享资源。比如代码区,常量区,数据段(.data和.bss)以及堆区,进程栈区的资源。
加入一个线程A正要修改某一个全局变量的时候,因为CPU的调度离开了。然后另一个线程B来访问这个变量并且修改,线程A再次访问这个变量就会导致资源错误。
代码举例:
#include <iostream>
#include <memory>#include <unistd.h>
#include "Thread.hpp"
using namespace std;int tickets = 1000;void *get_tickets(void *args)
{string name = static_cast<const char *>(args);while (true){if (tickets > 0){usleep(1000); // 模拟抢票cout << "线程" << name << "正在获取票,票号为" << tickets-- << endl;}else{cout << "无票可抢" << endl;break;}}return nullptr;
}int main()
{std::unique_ptr<Thread> t1(new Thread(get_tickets, (void *)"thread 1", 0));std::unique_ptr<Thread> t2(new Thread(get_tickets, (void *)"thread 2", 0));std::unique_ptr<Thread> t3(new Thread(get_tickets, (void *)"thread 3", 0));std::unique_ptr<Thread> t4(new Thread(get_tickets, (void *)"thread 4", 0));t1->start();t2->start();t3->start();t4->start();t1->join();t2->join();t3->join();t4->join();return 0;
}
运作结果如下:
可以看到,线程取到了负数的票。
这是因为--操作在汇编是三条指令
1 从内存读取到cpu中
2 cpu对根据命令执行计算和逻辑操作
3 将结果写回内存中
如果线程A执行完命令1后就被切换走了,线程B执行了一个完整--操作。然后切换回线程A的时候,线程A根据上下文操作继续执行计算命令。这样就会导致一个数据被多次--了。从而导致数据错误
保护共享资源最简单的方式就是对临界资源加锁
二. pthread_mutex_t 互斥锁操作
2.1 互斥锁的定义与初始化
a 动态分配初始化
//头文件
#include <pthread.h>//定义
pthread_mutex_t mutex;//动态分配
pthread_mutex_t mutex = INITIALIZER
动态分配一般用于定义全局变量和静态变量,动态分配初始化的互斥锁不需要销毁。
b 静态分配
//头文件
#include <pthread.h>//定义
pthread_mutex_t mutex;//初始化函数
pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)//参数
mutex:输入输出型参数,调用该接口之后对mutex进行初始化
attr:用于控制锁的属性,一般设置为nulpptr//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *restrict mutex)
//与初始化一样,用于销毁一个锁
注意静态分配的时候:
1 不要销毁已经加锁的锁
2 不要销毁了锁之后,继续对这个锁加锁
2.2 加解锁操作
使用pthread_mutex_lock进行加锁 pthread_mutex_unlock进行解锁。
//所需头文件
#include <pthread.h>//用于对一个已经初始化的锁加锁
pthread_mutex_lock(pthread_mutex_t *mutex);
//所需头文件
#include <pthread.h>//对加锁的mutex进行解锁
pthread_mutex_unlock(pthread_mutex_t *mutex);
三. 互斥锁的使用
我们对线程获取票这个操作进行加锁。如果我们定义的锁不是全局锁,需要通过传参的方式将锁传递给线程进行加锁解锁。当然全局锁就不需要了
这里以非全局锁举例
#include <iostream>
#include <vector>
#include <memory>#include <unistd.h>
#include "Thread.hpp"
#include <bits/unique_ptr.h>
using namespace std;const int thread_num = 5;
int tickets = 1000;struct ThreadData
{ThreadData(const string &name = string(), pthread_mutex_t *lock = nullptr): _name(name), _lock(lock) {}string _name;pthread_mutex_t *_lock;
};// 通过参数的方式传递锁
void *get_tickets(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string name = td->_name;pthread_mutex_t *mtx = td->_lock;while (true){// 加锁pthread_mutex_lock(mtx);if (tickets > 0){usleep(1000); // 模拟抢票cout << "线程" << name << "正在获取票,票号为" << tickets-- << endl;// 解锁pthread_mutex_unlock(mtx);}else{cout << "无票可抢" << endl;// 由于break会导致无法解锁,这里需要解锁pthread_mutex_unlock(mtx);break;}}return nullptr;
}int main()
{// 定义域初始化锁pthread_mutex_t lock;pthread_mutex_init(&lock, 0);vector<Thread> tds;for (int i = 0; i < thread_num; i++){string name = "thread-";name += to_string(i);ThreadData *td = new ThreadData(name, &lock);tds.push_back(Thread(get_tickets, td, 0));}for (int i = 0; i < tds.size(); i++)tds[i].start();for (int i = 0; i < tds.size(); i++)tds[i].join();// 销毁锁pthread_mutex_destroy(&lock);return 0;
}
运行结果如下:
可以看到,通过加锁操作可以保护共享资源的访问
加锁解锁之间的区域称为临界区,线程在临界区需要访问的资源称为临界资源。加解锁虽然能保护线程访问资源,但是会加解锁操作和线程竞争锁会导致效率降低
四. 互斥锁总结
4.1 谁来保护锁?
锁可以保护我们访问共享资源,但是锁本身也是一个共享资源,为什么我们加锁解锁不需要保护呢?因为加锁解锁操作是原子的,只需执行必定执行完
申请锁成功的线程会继续执行代码,申请锁失败的线程在干什么?此时线程会阻塞等待加锁的线程释放锁。
就像下图,只有一个线程在临界区中执行
4.2 加解锁的原子性问题
一个线程申请锁成功后,在临界区中能否被切换走?(可以切换走,并且其他线程仍无法访问临界区,因为我是带着锁走的,锁没有被释放)
那么这样会不会导致程序的效率过低(其他线程都在阻塞等待)?会导致效率降低,所以在编码的时候尽量保证临界区比较小,将无关的代码放到临界区外部。
即尽量保证锁的粒度比较小。