线程互斥与同步
线程互斥
临界资源:多线程执⾏流共享的资源就叫做临界资源
• 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
• 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起
保护作⽤
• 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成要么未完成
互斥量mutex
• ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。
• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完
成线程之间的交互。
• 多个线程并发的操作共享变量,会带来⼀些问题
示例代码:
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
using namespace std;int ticket = 100;
void *route(void *args)
{char *id = (char *)args;while (true){if (ticket > 0){sleep(1);cout << id << "sells ticket:" <<ticket<< endl;ticket--;}else{break;}}return nullptr;
}
int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, route, (void *)"thread 1");pthread_create(&t2, nullptr, route, (void *)"thread 2");pthread_create(&t3, nullptr, route, (void *)"thread 3");pthread_create(&t4, nullptr, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}
结果:
if 语句判断条件为真以后,代码可以并发的切换到其他线程
usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段
--ticket 操作本⾝就不是⼀个原⼦操作
要解决以上问题,需要做到三点:
• 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
• 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程
进⼊该临界区。
• 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。
互斥量的接口
初始化互斥量
静态分配
动态分配
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量
使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁⼀个已经加锁的互斥量
已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
互斥量加锁和解锁
返回值:成功返回0,失败返回错误号
调用pthread_ lock 时,可能会遇到以下情况:
• 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
• 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到
互斥量,那么pthread_ lock调用会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。
代码:
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
using namespace std;int ticket = 100;
pthread_mutex_t mutex;
void *route(void *args)
{char *id = (char *)args;while (true){ pthread_mutex_lock(&mutex);if (ticket > 0){sleep(1);cout << id << "sells ticket:" <<ticket<< endl;ticket--;pthread_mutex_unlock(&mutex);}else{ pthread_mutex_unlock(&mutex);break;}}return nullptr;
}
int main()
{pthread_mutex_init(&mutex,nullptr);pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, route, (void *)"thread 1");pthread_create(&t2, nullptr, route, (void *)"thread 2");pthread_create(&t3, nullptr, route, (void *)"thread 3");pthread_create(&t4, nullptr, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);return 0;
}
结果:
互斥量实现原理探究
为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和
内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的 总线周
期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。
互斥量的封装
#include<iostream>
#include<cstring>
#include<pthread.h>
using namespace std;namespace LockMoudle{class Mutex{public:Mutex(){int n=pthread_mutex_init(&_mutex,nullptr);(void)n;}void Lock(){int n=pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n=pthread_mutex_unlock(&_mutex);(void)n;}
~Mutex(){}private:pthread_mutex_t _mutex;};class LockGuard{public :LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}
线程同步
同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从⽽有效避免
饥饿问题,叫做同步
• 竞态条件:因为时序问题,⽽导致程序异常,我们称之为竞态条件。
条件变量函数
初始化
参数:
cond:要初始化的条件变
attr:NULL
销毁
等待条件满足
参数:
cond:要在这个条件变量上等待
mutex:互斥量
唤醒等待
代码:
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;void *active(void *arg){
string name=static_cast<const char*>(arg);
while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);cout<<name<<"活动"<<endl;pthread_mutex_unlock(&mutex);
}
}
int main(){pthread_t t1,t2;pthread_create(&t1,nullptr,active,(void*)"thread-1");pthread_create(&t2,nullptr,active,(void*)"thread-2");while(true){// pthread_cond_signal(&cond);//唤醒一个线程pthread_cond_broadcast(&cond);//唤醒所有线程sleep(1);}
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
}
生产者消费者模型
生产者消费者模式就是通过⼀个容器来解决生产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,而通过阻塞队列来进⾏通讯,所以生产者生产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于⼀个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给⽣产者和消费者解耦的
生产者消费者模型优点
解耦
⽀持并发
⽀持忙闲不均
基于BlockingQueue的⽣产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是⼀种常⽤于实现⽣产者和消费者模型的数据结构。其与 普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元 素;当队列满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是 基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
代码:
#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__#include <iostream>
#include <cstring>
#include <queue>
#include <pthread.h>
using namespace std;template <typename T>
class BlockQueue
{
private:bool isFull() const{return _block_queue.size() == _cap;}bool isEmpty() const{return _block_queue.empty();}public:BlockQueue(int cap) : _cap(cap){_productor_wait_num = 0;_consumer_wait_num = 0;pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_product_cond, nullptr);pthread_cond_init(&_consum_cond, nullptr);}// 入队操作,返回void,参数使用const引用void Enqueue(const T &in){pthread_mutex_lock(&_mutex);while (isFull()){_productor_wait_num++;pthread_cond_wait(&_product_cond, &_mutex);_productor_wait_num--;}_block_queue.push(in);// 唤醒等待的消费者if (_consumer_wait_num > 0){pthread_cond_signal(&_consum_cond);}pthread_mutex_unlock(&_mutex); // 确保解锁}// 出队操作,返回取出的元素T Pop(){pthread_mutex_lock(&_mutex);while (isEmpty()){_consumer_wait_num++;pthread_cond_wait(&_consum_cond, &_mutex);_consumer_wait_num--;}T data = _block_queue.front(); // 获取队首元素_block_queue.pop(); // 移除队首元素// 唤醒等待的生产者if (_productor_wait_num > 0){pthread_cond_signal(&_product_cond);}pthread_mutex_unlock(&_mutex);return data; // 返回取出的元素}// 获取当前队列大小size_t size() const{pthread_mutex_lock(&_mutex);size_t s = _block_queue.size();pthread_mutex_unlock(&_mutex);return s;}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_product_cond);pthread_cond_destroy(&_consum_cond);}private:queue<T> _block_queue; // 阻塞队列int _cap; // 队列容量pthread_mutex_t _mutex; // 互斥锁pthread_cond_t _product_cond; // 生产者条件变量pthread_cond_t _consum_cond; // 消费者条件变量int _productor_wait_num; // 等待的生产者数量int _consumer_wait_num; // 等待的消费者数量
};#endif
为什么 pthread_cond_wait 需要互斥量?
• 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜,
所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好
的通知等待在条件变量上的线程。
• 条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保
护。没有互斥锁就⽆法安全的获取和修改共享数据。
由于解锁和等待不是原⼦操作。调⽤解锁之后, pthread_cond_wait 之前,如果已经有其他
线程获取到互斥量,摒弃条件满⾜,发送了信号,那么 pthread_cond_wait 将错过这个信
号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是⼀个原⼦
操作
POSIX信号量
POSIX信号量和SystemV信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源⽬的。但POSIX可以⽤于线程间同步。
初始化信号量
销毁信号量
等待信号量
发布信号量
基于环形队列的生产消费模型
代码:
线程池
线程安全的单例模式
单例模式的特点
某些类,只应该具有⼀个对象(实例),就称之为单例.
在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中.此时往往要⽤⼀个单例 的类来管理这些数据.
饿汉实现⽅式和懒汉实现⽅式
饿汉式
概念:在类加载时就主动创建并初始化单例对象,无论后续是否会被使用。
- 类加载阶段完成实例化,天生线程安全(类加载过程由 JVM 保证线程安全)
- 不存在多线程同步问题可能提前占用系统资源(如果实例始终未被使用则造成浪费)
- 实现简单直接
懒汉式
概念:延迟实例化,只有在第一次被使用时(即第一次调用获取实例的方法时)才创建对象。
特点:
- 实现了 "按需加载",避免资源浪费
- 基础版本线程不安全(多线程环境下可能创建多个实例)
- 需要通过同步机制(如
synchronized
关键字)保证线程安全 - 实现相对复杂,需要处理多线程并发问题
线程安全和重⼊问题
线程安全:就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结 果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量 或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。
重⼊其实可以分为两种情况:
多线程重⼊函数
信号导致⼀个执⾏流重复进⼊函数
常⻅的线程不安全的情况
不保护共享变量的函数
函数状态随着被调⽤,状态发⽣变化的函数
返回指向静态变量指针的函数
调⽤线程不安全函数的函数
常⻅的线程安全的情况
每个线程对全局变量或者静态变量只有读取 的权限,⽽没有写⼊的权限,⼀般来说这些 线程是安全的
类或者接⼝对于线程来说都是原⼦操作
多个线程之间的切换不会导致该接⼝的执⾏ 结果存在⼆义性
常⻅不可重⼊的情况
调⽤了malloc/free函数,因为malloc函数 是⽤全局链表来管理堆的
调⽤了标准I/O库函数,标准I/O库的很多实 现都以不可重⼊的⽅式使⽤全局数据结构
可重⼊函数体内使⽤了静态的数据结构
常⻅可重⼊的情况
不使⽤全局变量或静态变量
不使⽤ malloc或者new开辟出的空间
不调⽤不可重⼊函数
不返回静态或全局数据,所有数据都有函数 的调⽤者提供
使⽤本地数据,或者通过制作全局数据的本 地拷⻉来保护全局数据
可重⼊与线程安全联系
函数是可重⼊的,那就是线程安全的(其实知道这⼀句话就够了)
函数是不可重⼊的,那就不能由多个线程使⽤,有可能引发线程安全问题
如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重⼊的。
可重⼊与线程安全区别
可重⼊函数是线程安全函数的⼀种 线程安全不⼀定是可重⼊的,⽽可重⼊函数则⼀定是线程安全的。 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重⼊函数若锁还 未释放则会产⽣死锁,因此是不可重⼊的。
常⻅锁概念
死锁
死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站⽤不会 释放的资源⽽处于的⼀种永久等待状态。
为了⽅便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进⾏后续资源的访问
死锁四个必要条件
互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤
请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放
不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺
循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
破坏循环等待条件问题:资源⼀次性分配,使⽤超时机制、加锁顺序⼀致