线程同步与互斥(互斥)
目录
线程互斥
进程线程间的互斥相关背景概念
互斥量mutex
互斥量的接⼝
初始化互斥量
初始化互斥量有两种⽅法
销毁互斥量
互斥量加锁和解锁
加锁的使用
互斥量实现原理探究
互斥量的封装
Mutex.hpp
Main.cc
线程互斥
进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起 保护作⽤
- 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成
互斥量mutex
- ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量 归属单个线程,其他线程⽆法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完 成线程之间的交互。
- 多个线程并发的操作共享变量,会带来⼀些问题。
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;void *route(void *arg)
{char *id = (char*)arg;while ( 1 ) {if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;} else {break;}}
}
int main( void )
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
⼀次执⾏结果:thread 4 sells ticket:100...thread 4 sells ticket:1thread 2 sells ticket:0thread 1 sells ticket:-1thread 3 sells ticket:-
要解决以上问题,需要做到三点:
- 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
- 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程 进⼊该临界区。
- 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量
互斥量的接⼝
初始化互斥量
初始化互斥量有两种⽅法
⽅法1,静态分配: PTHREAD_MUTEX_INITIALIZER
是一个宏,用于静态初始化互斥锁。它只能用于静态存储期的对象(如全局变量或静态变量),不能用于动态分配的互斥锁。全局使用不需要销毁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
⽅法2,动态分配: pthread_mutex_init
是一个函数,用于动态初始化互斥锁。它可以用于任何类型的互斥锁,包括动态分配的互斥锁 ,局部使用需要销毁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);参数:mutex:要初始化的互斥量attr:NULL
销毁互斥量
销毁互斥量需要注意:
使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁⼀个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);返回值:成功返回0,失败返回错误号
调⽤ pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到 互斥量,那么pthread_lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁
加锁的使用
注释掉的是使用互斥量的接⼝的方法 ,保留的是C++11的加锁
1. 锁本身是全局的,那么锁也是共享资源!谁保证锁的安全??
pthread_mutex:加锁和解锁被设计成为原子的了 --- TODO??
2. 如何看待锁呢?二元信号量就是锁!
2.1 加锁本质就是对资源展开预订!
2.2 整体使用资源!!
3. 如果申请锁的时候,锁被别人已经拿走了,怎么办?其他线程要进行阻塞等待
4. 线程在访问临界区代码的时候,可以不可以切换??可以切换!!
4.1 我被切走的时候,别人能进来吗??不能!我是抱着锁,被切换的!!不就是串行吗!效率低的原因!原子性!
5. 不遵守这个约定??bug!
使用C++11的锁
#include <iostream>
#include <vector>
#include "Thread.hpp"
#include <mutex> //c++11的锁using namespace ThreadModule;#define NUM 4// 1. 锁本身是全局的,那么锁也是共享资源!谁保证锁的安全??
// pthread_mutex:加锁和解锁被设计成为原子的了 --- TODO??
// 2. 如何看待锁呢?二元信号量就是锁!
// 2.1 加锁本质就是对资源展开预订!
// 2.2 整体使用资源!!
// 3. 如果申请锁的时候,锁被别人已经拿走了,怎么办?其他线程要进行阻塞等待
// 4. 线程在访问临界区代码的时候,可以不可以切换??可以切换!!
// 4.1 我被切走的时候,别人能进来吗??不能!我是抱着锁,被切换的!!不就是串行吗!效率低的原因!原子性!
// 5. 不遵守这个约定??bug!// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int ticketnum = 10000; // 共享资源,临界资源
std::mutex gmtx; //定义c++11的锁 ,定义一个全局互斥量class ThreadData
{
public:std::string name;//pthread_mutex_t *lock_ptr;
};void Ticket(ThreadData &td)
{while (true){gmtx.lock();//加锁//pthread_mutex_lock(td.lock_ptr); // 加锁if (ticketnum > 0){usleep(1000);// 1. 抢票printf("get a new ticket, who get it: %s, id: %d\n", td.name.c_str(), ticketnum--);gmtx.unlock(); //解锁//pthread_mutex_unlock(td.lock_ptr); // ??// 2. 入库模拟usleep(50);}else{//pthread_mutex_unlock(td.lock_ptr); // ??gmtx.unlock(); //解锁break;}}
}int main()
{// pthread_mutex_t lock;// pthread_mutex_init(&lock, nullptr);// 1. 构建线程对象std::vector<Thread<ThreadData>> threads;for (int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();//td->lock_ptr = &lock;// td->lock_ptr =nullptr;threads.emplace_back(Ticket, *td);td->name = threads.back().Name();}// 2. 启动线程for (auto &thread : threads){thread.Start();}// 3. 等待线程for (auto &thread : threads){thread.Join();}// pthread_mutex_destroy(&lock);return 0;
}
使用全局的锁
#include <iostream>
#include <vector>
#include "Thread.hpp"
#include <mutex> //c++11的锁using namespace ThreadModule;#define NUM 4pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int ticketnum = 10000; // 共享资源,临界资源
// std::mutex gmtx; //定义c++11的锁class ThreadData
{
public:std::string name;pthread_mutex_t *lock_ptr;
};void Ticket(ThreadData &td)
{while (true){// gmtx.lock();//加锁pthread_mutex_lock(td.lock_ptr); // 加锁if (ticketnum > 0){usleep(1000);// 1. 抢票printf("get a new ticket, who get it: %s, id: %d\n", td.name.c_str(), ticketnum--);// gmtx.unlock(); //解锁pthread_mutex_unlock(td.lock_ptr); // ??// 2. 入库模拟usleep(50);}else{pthread_mutex_unlock(td.lock_ptr); // ??// gmtx.unlock(); //解锁break;}}
}int main()
{// pthread_mutex_t lock;// pthread_mutex_init(&lock, nullptr);// 1. 构建线程对象std::vector<Thread<ThreadData>> threads;for (int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->lock_ptr = &lock;threads.emplace_back(Ticket, *td);td->name = threads.back().Name();}// 2. 启动线程for (auto &thread : threads){thread.Start();}// 3. 等待线程for (auto &thread : threads){thread.Join();}// pthread_mutex_destroy(&lock);return 0;
}
互斥量实现原理探究
经过上⾯的例⼦,⼤家已经意识到单纯的 i++ 或者 ++i 都不是原⼦的,有可能会有数据⼀致性 问题
为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和 内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的总线周 期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。现在 我们把lock和unlock的伪代码改⼀下
互斥量的封装
Mutex.hpp
#pragma once
#include<iostream>
#include<pthread.h>
#include <unistd.h>namespace LockModule
{class Mutex{public://防止锁被拷贝 Mutex(const Mutex&) = delete;const Mutex& operator = (const Mutex&) = delete;Mutex(){int n = ::pthread_mutex_init(&_lock ,nullptr);if(n!= 0) std::cout<<"pthread_mutex_init error"<<std::endl;}void Lock(){int n = ::pthread_mutex_lock(&_lock);if(n!= 0) std::cout<<"lock error"<<std::endl;}void Unlock(){int n =::pthread_mutex_unlock(&_lock);if(n!= 0) std::cout<<"unlock error"<<std::endl;}~Mutex(){int n = pthread_mutex_destroy(&_lock);if(n!= 0) std::cout<<"destroy error"<<std::endl;}private:pthread_mutex_t _lock;};class LockGuard{public:LockGuard(Mutex &mtx):_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex &_mtx;};}
使用
Main.cc
#include "Mutex.hpp"using namespace LockModule;
int ticket=1000;Mutex mtx;void *route(void *arg)
{char *id = (char *)arg;while (1){//RAII 风格的加锁LockGuard lockguard(mtx);//在代码块中定义的临时对象 ,处了这个块 就自己析构了 ,这个块就是临界区if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else{break;}}return nullptr;
}// void *route(void *arg)
// {
// char *id = (char *)arg;
// while (1)
// {
// mtx.Lock();
// if (ticket > 0)
// {
// usleep(1000);
// printf("%s sells ticket:%d\n", id, ticket);
// ticket--;
// mtx.Unlock();
// }
// else
// {
// mtx.Unlock();
// break;
// }
// }
// return nullptr;
// }int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}