一. 概念
1.1 简述
首先我们来回顾一下进程的概念,进程是程序和操作系统维护进程的相关数据结构的总称。程序 就是我们写的代码,维护进程的相关数据结构 就是PCB(task_struct),虚拟内存(mm_struct),files_struct等等数据结构。一个线程就是进程内部的一条执行流,多个线程就有多个执行流,线程是包含在进程里面的,进程跟线程是1:N关系。 进程有对应的PCB(进程控制块),线程也是有对应的TCB(线程控制块)。这是理论上的说法,实际上不同系统有不同的实现方法,我们常用的Windows就是一个进程里面有一个PCB和多个TCB,如果让PCB和TCB之间建立映射关系。而Linux系统下,没有真正意义上的TCB,而是用多个PCB来实现,一个PCB代表一个线程,所以Linux系统下的PCB比传统PCB更加轻量化了。
1.2 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.3 线程的缺点
- 性能损失,增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低,一个线程异常,崩溃会导致进程收到进程信号,终止进程,从而影响整个进程。
1.4 线程和进程的关系
进程是资源分配的基本单位,而线程是调度的基本单位。常见的进程线程关系是,单进程单线程,单进程多线程这两种。 线程共享进程数据,也有自己的一部分数据(比如说线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级)。
1.5 线程id及进程地址空间布局
如图,我们创建线程时,会链接pthread动态库,这个库会从硬盘加载到内存,然后通过页面映射到mmap区域。前面我们说了线程也有自己的部分数据,这些数据就放在mmap区域,pthread动态库会有描述线程的属性和数据结构。线程id有两个,一个是CPU调度用的LWP用命令行
ps -aL
可以查看。另外一个是给我们用户操作线程用的,我们平时关心的就是这个id,这个id的类型为pthread_t,实际上它是线程在虚拟内存的第一个元素单元的地址,如下图的id1和idn
二. 线程控制
2.1 POSIX线程库
- 头文件<pthread.h>
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 链接这些线程函数库时要在gcc/g++命令后面加“-lpthread”选项
2.2 创建线程
2.2.1 pthread_create函数
// 1. 头文件
#include <pthread.h>
// 2. 函数声明
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void *arg);
// 3. 功能:创建一个新的线程
// 4. thread:输出型参数,用来接收新线程的ID
// 5. attr:设置线程的属性,传NULL表示使用默认属性
// 6. start_routine:一个函数指针,指向的是线程启动后要执行的函数
// 7. arg:传给线程启动函数的参数
// 8. 返回值:成功返回0,失败返回错误码
2.2.2 代码演示
#include <iostream>
#include <pthread.h>
#include <unistd.h> // for sleep
#include <string.h> // for strerror
using namespace std;
// 新线程执行的函数
void* pthread_run(void* arg)
{
printf("%s, I am a new thread\n", (char*)arg);
}
int main()
{
pthread_t id;
int ret = pthread_create(&id, NULL, pthread_run, (void*)"Hello");
if (ret == 0)
{
sleep(1);
printf("A thread was successfully created with id 0x%x\n");
}
else
{
fprintf(stderr, "pthread_create:%s\n", strerror(ret));
exit(EXIT_FAILURE); // #define EXIT_FAILURE 1 --> 0表示正常退出,1表示异常退出
}
return 0;
}
2.3 线程等待
2.3.1 pthread_exit函数
// 1. 头文件
#include <pthread.h>
// 2. 函数声明
void pthread_exit(void* retval);
// 3. 功能:终止线程
// 4. retval:不能指向一个局部变量
2.3.2 pthread_cancel函数
// 1. 头文件
#include <pthread.h>
// 2. 函数声明
int pthread_cancel(pthread_t pthread);
// 3. 功能:取消一个id为pthread的线程
// 4. 返回值: 成功返回0,错误返回错误码
2.3.3 pthread_join函数
// 1. 头文件
#include <pthread.h>
// 2. 函数声明
int pthread_join(pthread_t thread, void** retval);
// 3. 功能:等待id为thread的线程结束,并且获取退出信息
// 4. retval是一个二级指针,指向的是返回信息的地址
// 5. 返回值: 成功返回0,错误返回错误码
2.3.4 pthread_self函数
// 1. 头文件
#include <pthread.h>
// 2. 函数声明
pthread_t pthread_self(void);
// 3. 功能:获取当前线程的id
// 5. 返回值:永远会成功返回当前线程的id
2.3.5 代码演示
#include <iostream>
#include <pthread.h>
#include <unistd.h> // for sleep
#include <string.h> // for strerror
using namespace std;
// 全局变量(用于在新线程里面取消主线程)
pthread_t gthreadid;
void* pthread_run(void* arg)
{
cout << (char*)arg << endl;
pthread_cancel(gthreadid); // 非主线程取消主线程,主线程会出现类型僵尸进程那样的情况
int times = 10;
while (times)
{
sleep(1);
cout << times-- << endl;
}
// 线程退出
return (void*)"123"; // return终止线程,适用于非主线程,主线程用return会导致整个进程结束
//pthread_exit((void*)"I quit mormally!"); // 调用函数返回
}
int main()
{
gthreadid = pthread_self();
pthread_t threadid;
int ret = pthread_create(&threadid, NULL, pthread_run, (void*)"Hi");
if (ret == 0)
{
sleep(1);
printf("A thread was successfully created with id 0x%x\n");
}
else
{
fprintf(stderr, "pthread_create:%s\n", strerror(ret));
exit(EXIT_FAILURE);
}
// 等待线程退出,阻塞等待
void* status;
int waitret = pthread_join(threadid, &status);
if (waitret == 0)
{
cout << "wait for success! retval is : " << (char*)status << endl;
}
else
{
fprintf(stderr, "pthread_join:%s\n", strerror(waitret));
exit(EXIT_FAILURE);
}
while (1)
{
sleep(1);
cout << "I am main thread....." << endl;
}
return 0;
}
2.4 线程分离
2.4.1 简述
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候可以用线程分离,告诉系统,当线程退出时,自动释放线程资源。线程分离可以自己分离自己,也可以分离其他的线程。
2.4.2 pthread_detach函数
// 1. 头文件
#include <pthread.h>
// 2. 函数声明
int pthread_detach(pthread_t thread);
// 3. 功能:分离id为thread的进程
// 5. 返回值:成功返回0,失败返回错误码
2.4.3 代码演示
// 注意:join和分离是有冲突的,分离之后也没有必要join了
#include <iostream>
#include <pthread.h>
#include <unistd.h> // for sleep
using namespace std;
// 4
void* pthread_run(void* arg)
{
//pthread_detach(pthread_self()); // 自己分离自己
printf("%s, I am a new thread\n", (char*)arg);
}
int main()
{
pthread_t id;
int ret = pthread_create(&id, NULL, pthread_run, (void*)"Hello");
pthread_detach(id); // 分离其他进程
if (ret == 0)
{
sleep(1);
printf("A thread was successfully created with id 0x%x\n");
}
else
{
fprintf(stderr, "pthread_create:%s\n", strerror(ret));
exit(EXIT_FAILURE);
}
while (1);
return 0;
}
三. 线程互斥
3.1 相关概念
临界资源: 多线程执行流共享的资源就叫做临界资源
临界区: 每个线程内部,访问临界资源的代码区域,就叫做临界区
互斥: 任何时刻,只允许一个执行流(线程)进入临界区,访问临界资源,通常对临界资源起保护作用
原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。汇编下的一条代码指令可以看作是原子性的。
3.2 实现卖票系统
3.2.1 没有加互斥控制的代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
// 不加锁的情况
int ticket = 100; // 票数,也就是我们的临界资源
void* BuyTicket(void* arg)
{
size_t i = *(size_t*)arg;
while (true)
{
// 临界区开始
if (ticket > 0)
{
usleep(10000); // 1000微妙
printf("我是第%u号线程,我在抢%d号票\n", i, ticket);
ticket--;
// 临界区结束
}
else
{
std::cout << "票抢完了" << std::endl;
break;
}
}
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, BuyTicket, (void*)(tid + i));
}
for (int i = 0; i < 5; i++)
{
pthread_join(*(tid + i), nullptr);
}
return 0;
}
上面代码的运行结果
我们看到运行结果,有的票号被不同线程抢了多次,有的票号没有人抢,后面没票了还可以买。出现上面这些问题,说明了在同一时间有多个进程进入了临界区(上面代码标明的区域),这样他们会一起访问临界资源,从而出现了问题。为了解决上面问题,我们就要保证一个线程进入临界区之后,其他线程就不能进入了,直到该线程走出临界区。想要做到这一点我们只需要提供一把锁,当一个线程进入临界区就把锁给锁住,锁定之后其他线程就不能访问临界区了,当该线程出去临界区就解锁, 这个锁我们叫做互斥量。
3.2.2 互斥量mutex
// mutex相关函数接口(他们的头文件都是<pthead.h>)
// 定义一个mutex
// 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 不需要初始化和销毁
// 动态分配
pthread_mutex_t mutex; // 需要初始化和销毁
// 1. 初始化函数
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
// mutex:传的是想要初始化的mutex对象地址
// attr:是指定锁的属性,一般传NULL就好了
// 返回值:成功返回0,失败返回错误码
// 2. 加锁和解锁函数
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
// 返回值:成功返回0,失败返回错误码
// 发起函数调用时,其他线程已经锁定互斥量,pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
// 3. 销毁函数
int pthread_mutex_destroy(pthread_mutex_t* mutex);
// 返回值:成功返回0,失败返回错误码
// 不要销毁一个锁着的互斥量
// 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁
3.2.3 加了互斥量的代码
// 下面锁的使用已经标了顺序序号,锁的使用流程大概就是这样的
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
int ticket = 100;
// 1. 定义一个全局的锁(互斥量)
pthread_mutex_t mutex;
void* BuyTicket(void* arg)
{
size_t i = *(size_t*)arg;
while (true)
{
// 3. 上锁
pthread_mutex_lock(&mutex);
// 临界区开始
if (ticket > 0)
{
usleep(1000); // 1000微妙
printf("我是第%u号线程,我在抢%d号票\n", i, ticket);
ticket--;
// 临界区结束
// 4. 解锁
pthread_mutex_unlock(&mutex);
}
else
{
printf("我是第%u号线程,票已经抢完了\n", i);
// 4. 解锁
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main()
{
// 2. 初始化锁
pthread_mutex_init(&mutex, nullptr);
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, BuyTicket, (void*)(tid + i)));
}
for (int i = 0; i < 5; i++)
{
pthread_join(*(tid + i), nullptr);
}
// 5. 销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
运行结果
从运行结果上看我们加了互斥控制之后,就可以达到我们预期的结果了。
3.2.4 互斥量的实现原理
互斥锁保护临界区临界资源的前提是得保证自己是安全的,即上锁和解锁的过程是保持原子性的,上面我们说过,汇编下一条代码指令就是原子性的。在汇编下有一条swap或exchange指令,可以直接交换寄存器和内存单元的数据。如图,**al
是CPU内部的一个寄存器,用来存储当前线程的相关数据,所以不同线程在运行时al的数据是不一样的,线程切换时这个寄存器的数据会被存到线程的上下文中。比如说有两个线程A和B,一开始mutex
里面存的值是1。如果是A先执行了swap
语句,执行swap
之前A肯定执行了move
,所以A的al
在执行swap
之前是0,执行swap
之后,A的al
变成1,mutex
为0。mutex
为0,后面无论B怎么执行swap
最终它的al
里面存的都是0,所以就一直被goto
到lock
的开始,只要A没有unlock
,B就一直会在循环(阻塞)状态。**CPU运行哪个线程,CPU的al寄存器的数据就是那个线程上下文的al数据,下图是CPU在运行B线程的状态。
2.2.5 线程安全和重入
重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
线程安全: 多个线程并发同一段代码时,不会出现不同的结果。比如说抢票这个系统案例,如果有1000张票,那么我们上面的代码运行结果只有一种情况,就是票重1000到1逐张递减。如果多次运行会出现不同情况,那么就是线程不安全的,我们没有加互斥量之前就是这样的。
重入和线程安全的关系: 可重入函数一定是线程安全的,线程安全不一定是可重入函数。
3.3 死锁
3.3.1 简述
死锁是指在一组进程中的各个线程均占有不会释放的资源,但因互相申请被其他线程所站用不会释放的资源而处于的一种永久等待状态。
3.3.2 死锁的四个必要条件
- 互斥条件: 一个资源每次只能被一个线程使用
- 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件: 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系
3.3.3 避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
3.3.4 避免死锁算法
- 死锁检测算法
- 银行家算法
四. 线程同步
4.1 同步与竞态
同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
竞态条件(race condition): 两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。
4.2 条件变量
4.2.1 简述
生产者和消费者问题中,一个消费者线程往队列拿产品时,发现队列为空,此时它只能等待生产者线程往放产品。等待就要使用条件变量,当生产者往队列放了产品,就给条件变量发一个信号,这时消费者就可以拿了。
4.2.2 条件变量的函数接口
// 他们的头文件都是<pthead.h>
// 定义一个cond
pthread_cond_t cond;
// 1. 初始化函数
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// cond:传的是想要初始化的cond对象地址
// attr:是指条件变量的属性,一般传NULL就好了
// 返回值:成功返回0,失败返回错误码
// 2. 等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
// cond:要在这个条件变量上等待
// mutex:这个函数调用时会释放mutex锁,然后挂起自己,返回时,首先会竞争锁,竞争到锁后,才能返回。
// 返回值:成功返回0,失败返回错误码
// 3. 唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); // 【ˈbrɔːdkɑːst】广播,唤醒这个与cond关联的全部线程
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个线程
// 返回值:成功返回0,失败返回错误码
// 3. 销毁函数
int pthread_cond_destroy(pthread_cond_t *cond);
// 返回值:成功返回0,失败返回错误码
4.2.3 代码例子
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h> // for sleep
pthread_mutex_t mutex;
pthread_cond_t cond;
void* control(void* arg)
{
std::string name = (char*)arg;
while (true)
{
std::cout << "sen a signal....." << std::endl;
//pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
sleep(2);
}
}
void* work(void* arg)
{
int id = *(int*)arg;
delete (int*)arg;
while (true)
{
pthread_cond_wait(&cond, &mutex);
std::cout << id << "start to work...." << std::endl;
}
}
int main()
{
pthread_mutex_init(&mutex, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t controller;
pthread_t worker[5];
pthread_create(&controller, nullptr, control, (void*)"controller");
for (int i = 0; i < 5; i++)
{
int* id = new int(i);
pthread_create(worker + i, nullptr, work, (void*)id);
}
pthread_join(controller, nullptr);
for (int i = 0; i < 5; i++)
{
pthread_join(worker[i], nullptr);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
4.3 生产者与消费者
4.3.1 简述
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的,同时还支持并发,支持忙闲不均。
321模型: 3种关系,生产者和生产者有竞争、互斥关系,消费者和消费者有竞争、互斥关系,消费者和生产者有互斥、同步关系。2种角色,消费者和消费者。1个场所(内存空间,STL容器等)。
4.3.2 基于BlockkingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。
4.3.3 代码实现
block_queue.hpp文件代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>
#include <time.h>
namespace li
{
template<class T>
class block_queue
{
public:
block_queue(const int& capacity = 5)
:_capacity(capacity)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_full, nullptr);
pthread_cond_init(&_empty, nullptr);
}
~block_queue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_full);
}
void Push(const T& data)
{
LockQueue();
//if (IsFull())
while (IsFull())
{
pthread_cond_wait(&_full, &_mutex); // 防止挂起失败,或者被伪唤醒
}
_bq.push(data);
UnlockQueue();
WakeUpConsumer();
}
void Pop(T* data)
{
LockQueue();
//if (IsEmpty())
while (IsEmpty())
{
pthread_cond_wait(&_empty, &_mutex); // 防止挂起失败,或者被伪唤醒
}
*data = _bq.front();
_bq.pop();
UnlockQueue();
WakeUpProducter();
}
private:
bool IsFull() // 判断队列是否满了
{
return _bq.size() == _capacity;
}
bool IsEmpty() // 判断队列是否为空
{
return _bq.size() == 0;
}
void WakeUpConsumer() // 唤醒消费者
{
pthread_cond_signal(&_empty);
}
void WakeUpProducter() // 唤醒生产者
{
pthread_cond_signal(&_full);
}
void LockQueue() // 上锁
{
pthread_mutex_lock(&_mutex);
}
void UnlockQueue() // 解锁
{
pthread_mutex_unlock(&_mutex);
}
private:
std::queue<T> _bq; // 阻塞队列
pthread_mutex_t _mutex;
int _capacity; // 阻塞队列的容量
pthread_cond_t _full; // 阻塞队列满的时候用的条件变量
pthread_cond_t _empty; // 阻塞队列为空时用的条件变量
};
}
consumer_producter.cpp文件代码
#include "block_queue.hpp"
using namespace li;
void* ConsumerRun(void* arg)
{
block_queue<int>* bq = (block_queue<int>*)arg;
int data;
while (true)
{
bq->Pop(&data);
std::cout << "consumer get " << data << std::endl;
sleep(1);
}
}
void* ProducterRun(void* arg)
{
block_queue<int>* bq = (block_queue<int>*)arg;
int data;
while (true)
{
data = rand() % 100 + 1;
bq->Push(data);
std::cout << "producter put " << data << std::endl;
//sleep(1);
}
}
int main()
{
srand((size_t)time(nullptr));
block_queue<int> bq;
pthread_t consumer;
pthread_t producter;
pthread_create(&consumer, nullptr, ConsumerRun, (void*)&bq);
pthread_create(&producter, nullptr, ProducterRun, (void*)&bq);
pthread_join(consumer, nullptr);
pthread_join(producter, nullptr);
return 0;
}
五. POSIX信号量
5.1 简述
信号量的本质就是用来描述临界资源的数量大小,申请信号量可以理解为在预约临界资源。
5.2 函数接口
// 注意:以下所有接口都是成功返回0,失败返回-1
// 头文件
#include <semaphore.h>
// 定义信号量
sem_t sem;
// 1. 初始化
int sem_init(sem_t* sem, int pshared, unsiged int value);
// sem:需要初始化的信号量
// pshared:0表示线程间共享,非0表示进程间共享
// value:信号量初始值
// 2. 等待信号量
int sem_wait(sem_t* sem);
// 功能:等待信号量,会将信号量的值减1,就是我们课本上说的P操作
// 3. 发送信号
int sem_post(sem_t* sem);
// 功能:发送信号量,会将信号量的值加1,就是我们课本上说的V操作
// 4. 销毁信号量
int sem_destroy(sem_t* sem);
5.3 基于环形队列的生产消费模型
5.3.1 简述
如图所示,我们用vector来实现环形队列,c_step表示消费者的下标位置,p_step表示生产者的下标位置。我们定义了两个信号变量,blank表示空白的位置个数,data表示有数据的位置个数,一开始我们让blank为8,data为0。在生产者眼里blank是资源,在消费者眼里data是资源。我们需要保证c_step不大于p_step,同时p_step不能大于c_step一轮,即blank和data不能小于0,不能大于8。我们的信号量有自己的控制机制,当信号量等于0时就会挂起线程,P(wait)操作就会自动挂起线程,直到对方用V(post)操作唤醒。
5.3.2 代码实现
5.3.2.1 单生产者单消费者
ring_queue.hpp文件代码
#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
namespace li
{
template<class T>
class ring_queue
{
public:
ring_queue(const int& capacity = 8)
:_capacity(capacity)
{
_ring_queue.resize(capacity, T());
_c_step = _p_step = 0;
sem_init(&_blank, 0, capacity);
sem_init(&_data, 0, 0);
}
~ring_queue()
{
sem_destroy(&_blank);
sem_destroy(&_data);
}
void Push(const T& data)
{
sem_wait(&_blank);
_ring_queue[_p_step] = data;
sem_post(&_data);
_p_step++;
_p_step %= _capacity;
}
void Pop(T* data)
{
sem_wait(&_data);
*data = _ring_queue[_c_step];
sem_post(&_blank);
_c_step++;
_c_step %= _capacity;
}
private:
std::vector<T> _ring_queue;
int _capacity;
int _c_step;
int _p_step;
sem_t _blank;
sem_t _data;
};
}
ring_cp.cpp文件代码
#include "ring_queue.hpp"
#include <time.h>
#include <unistd.h>
using namespace li;
void* ProducerRun(void* arg)
{
ring_queue<int>* rq = (ring_queue<int>*)arg;
while (true)
{
int data = rand() % 100 + 1;
rq->Push(data);
std::cout << "put a number: " << data << std::endl;
}
}
void* ConsumerRun(void* arg)
{
ring_queue<int>* rq = (ring_queue<int>*)arg;
int data;
while (true)
{
sleep(1);
rq->Pop(&data);
std::cout << "get a number: " << data << std::endl;
}
}
int main()
{
srand((size_t)time(nullptr));
ring_queue<int> rq;
pthread_t producer;
pthread_t consumer;
pthread_create(&producer, nullptr, ProducerRun,(void*)&rq);
pthread_create(&consumer, nullptr, ConsumerRun,(void*)&rq);
pthread_join(producer, nullptr);
pthread_join(consumer, nullptr);
return 0;
}
5.3.2.2 多生产者多消费者
ring_queue.hpp文件代码
#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <pthread.h>
namespace li
{
template<class T>
class ring_queue
{
public:
ring_queue(const int& capacity = 8)
:_capacity(capacity)
{
_ring_queue.resize(capacity, T());
_c_step = _p_step = 0;
sem_init(&_blank, 0, capacity);
sem_init(&_data, 0, 0);
pthread_mutex_init(&_mutex, nullptr);
}
~ring_queue()
{
sem_destroy(&_blank);
sem_destroy(&_data);
pthread_mutex_destroy(&_mutex);
}
void Push(const T& data)
{
sem_wait(&_blank);
pthread_mutex_lock(&_mutex);
_ring_queue[_p_step] = data;
_p_step++;
_p_step %= _capacity;
pthread_mutex_unlock(&_mutex);
sem_post(&_data);
}
void Pop(T* data)
{
sem_wait(&_data);
pthread_mutex_lock(&_mutex);
*data = _ring_queue[_c_step];
_c_step++;
_c_step %= _capacity;
pthread_mutex_unlock(&_mutex);
sem_post(&_blank);
}
private:
std::vector<T> _ring_queue;
int _capacity;
int _c_step;
int _p_step;
pthread_mutex_t _mutex;
sem_t _blank;
sem_t _data;
};
}
ring_cp.cpp文件代码
#include "ring_queue.hpp"
#include <time.h>
#include <unistd.h>
using namespace li;
void* ProducerRun(void* arg)
{
ring_queue<int>* rq = (ring_queue<int>*)arg;
while (true)
{
int data = rand() % 100 + 1;
rq->Push(data);
//std::cout << "put a number: " << data << std::endl;
printf("我是%u号线程,我在队列里放了数字%d\n", pthread_self(), data);
}
}
void* ConsumerRun(void* arg)
{
ring_queue<int>* rq = (ring_queue<int>*)arg;
int data;
while (true)
{
sleep(1);
rq->Pop(&data);
//std::cout << "get a number: " << data << std::endl;
printf("我是%u号线程,我从队列里拿走了数字%d\n",pthread_self(), data);
}
}
int main()
{
srand((size_t)time(nullptr));
ring_queue<int> rq;
pthread_t producer1;
pthread_t producer2;
pthread_t producer3;
pthread_t consumer1;
pthread_t consumer2;
pthread_create(&producer1, nullptr, ProducerRun,(void*)&rq);
pthread_create(&producer2, nullptr, ProducerRun,(void*)&rq);
pthread_create(&producer3, nullptr, ProducerRun,(void*)&rq);
pthread_create(&consumer1, nullptr, ConsumerRun,(void*)&rq);
pthread_create(&consumer2, nullptr, ConsumerRun,(void*)&rq);
pthread_join(producer1, nullptr);
pthread_join(producer2, nullptr);
pthread_join(producer3, nullptr);
pthread_join(consumer1, nullptr);
pthread_join(consumer2, nullptr);
return 0;
}
六. 线程池
6.1 简述
提前准备好一定数量的线程,用来随时处理任务。 线程池适合应用于完成单个任务小,任务量巨大的任务。
6.2 代码实现
thread_pool.hpp文件代码
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
namespace li
{
template<class T>
class thread_pool
{
public:
thread_pool(const int& num = 10)
:_num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
ThreadPoolInit();
}
~thread_pool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
static void* Run(void* arg)
{
pthread_detach(pthread_self());
thread_pool<T>* tp = (thread_pool<T>*)arg;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
tp->Wait();
}
T t;
tp->PopTask(&t);
tp->UnLock();
// run
printf("我是%u号线程,执行任务完毕:%s%d\n", pthread_self(), t.ShowTask().c_str(), t.GetResult());
}
}
void ThreadPoolInit()
{
pthread_t id;
for (int i = 0; i < _num; i++)
{
pthread_create(&id, nullptr, Run, (void*)this);
}
}
void PushTask(const T& t)
{
Lock();
_q.push(t);
UnLock();
Signal();
}
private:
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void UnLock()
{
pthread_mutex_unlock(&_mutex);
}
bool IsEmpty()
{
return _q.size() == 0;
}
void Wait()
{
pthread_cond_wait(&_cond, &_mutex);
}
void Signal()
{
pthread_cond_signal(&_cond);
}
void PopTask(T* t)
{
*t = _q.front();
_q.pop();
}
private:
std::queue<T> _q;
int _num;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
}
task.hpp文件代码
#pragma once
#include <iostream>
#include <string>
namespace li
{
class task
{
public:
task(){}
task(const int& x, const int& y, const char& op)
:_x(x)
,_y(y)
,_op(op)
{}
std::string ShowTask() const
{
std::string s;
s += std::to_string(_x);
s += ' ';
s += _op;
s += ' ';
s += std::to_string(_y);
s += ' ';
s += '=';
s += ' ';
return s;
}
int GetResult() const
{
int ret = 0;
switch(_op)
{
case '+':
ret = _x + _y;
break;
case '-':
ret = _x - _y;
break;
case '*':
ret = _x * _y;
break;
case '/':
ret = _x / _y;
break;
case '%':
ret = _x % _y;
break;
default:
std::cout << "erro............" << std::endl;
break;
}
return ret;
}
private:
int _x;
int _y;
char _op;
};
}
main.cpp文件代码
#include "thread_pool.hpp"
#include "task.hpp"
#include <time.h>
#include <unistd.h>
using namespace li;
int main()
{
srand((size_t)time(nullptr));
thread_pool<task> tp;
while (true)
{
task t(rand() % 100 + 1,rand() % 100 + 1, "+-*/%"[rand() % 5]);
std::cout << "派发了一个任务" << t.ShowTask().c_str() << std::endl;
tp.PushTask(t);
sleep(1);
}
return 0;
}
七. 单例模式
7.1 简述
一个类只能实例化一个对象,称为单例。
7.2 饿汉和懒汉方式
饿汉: 吃完饭,立刻洗碗,下次吃饭立刻可以吃。在单例模式上就是,一开始就实例化对象,用的时候可以直接用。
懒汉: 吃完饭,没有立刻希望,下次吃饭再洗。在单例模式上就是,当你第一次需要对象的时候才实例化对象,后面的使用不用实例化。延时加载,从而能够优化服务器的启动速度。
7.3 懒汉方式实现单例模式
singleton_thread_pool.hpp文件代码
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
namespace li
{
template<class T>
class thread_pool
{
public:
~thread_pool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
static void* Run(void* arg)
{
pthread_detach(pthread_self());
thread_pool<T>* tp = (thread_pool<T>*)arg;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
tp->Wait();
}
T t;
tp->PopTask(&t);
tp->UnLock();
// run
printf("我是%u号线程,执行任务完毕:%s%d\n", pthread_self(), t.ShowTask().c_str(), t.GetResult());
}
}
void ThreadPoolInit()
{
pthread_t id;
for (int i = 0; i < _num; i++)
{
pthread_create(&id, nullptr, Run, (void*)this);
}
}
void PushTask(const T& t)
{
Lock();
_q.push(t);
UnLock();
Signal();
}
static thread_pool<T>* GetInstance(const int& num = 10)
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态的mutex不需要销毁
if (_ptr == nullptr) // 双重判断,防止创建之后还会上锁,影响效率
{
pthread_mutex_lock(&mutex);
if (_ptr == nullptr)
{
_ptr = new thread_pool<T>(num);
}
pthread_mutex_unlock(&mutex);
}
return _ptr;
}
private:
thread_pool(const thread_pool<T>& tp) = delete;
thread_pool<T>& operator=(thread_pool<T>& tp) = delete;
thread_pool(const int& num)
:_num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
ThreadPoolInit();
}
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void UnLock()
{
pthread_mutex_unlock(&_mutex);
}
bool IsEmpty()
{
return _q.size() == 0;
}
void Wait()
{
pthread_cond_wait(&_cond, &_mutex);
}
void Signal()
{
pthread_cond_signal(&_cond);
}
void PopTask(T* t)
{
*t = _q.front();
_q.pop();
}
private:
std::queue<T> _q;
int _num;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static thread_pool<T>* _ptr;
};
template<class T>
thread_pool<T>* thread_pool<T>::_ptr = nullptr; //静态成员需要在类外初始化
}
task.hpp文件代码 <-- 跟上面线程池的task文件一样的
main.cpp文件代码
#include "singleton_thread_pool.hpp"
#include "task.hpp"
#include <time.h>
#include <unistd.h>
using namespace li;
int main()
{
srand((size_t)time(nullptr));
int times = 10;
while (times)
{
std::cout << times-- << std::endl;
}
while (true)
{
task t(rand() % 100 + 1,rand() % 100 + 1, "+-*/%"[rand() % 5]);
std::cout << "派发了一个任务" << t.ShowTask().c_str() << std::endl;
thread_pool<task>::GetInstance()->PushTask(t);
// tp.PushTask(t);
sleep(1);
}
return 0;
}
八. STL、智能指针的线程安全分析
8.1 STL
STL不是线程安全的, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。
8.2 智能指针
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数,所以智能指针是线程安全的。
九. 常见的锁
9.1 锁的分类
悲观锁: 在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁: 每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作: 当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁: 不断地检测锁的状态,适用于访问临界区比较短的场景。
9.2 spin自旋锁
// spin相关函数接口(他们的头文件都是<pthead.h>)
// 定义一个spin
pthread_spinlock_t spin;
// 1. 初始化函数
int pthread_spin_init(pthread_spinlock* lock, int pshared);
// lock:传的是想要初始化的lock对象地址
// pshared:有下面两个宏可以传
// (1)PTHREAD_PROCESS_PRIVATE
// 自旋锁只能由与调用pthread_spin_init()的线程处于同一进程的线程操作。 (尝试在进程之间共享自旋锁会导致未定义的行为。)
// (2)PTHREAD_PROCESS_SHARED
// 自旋锁可以由有权访问包含该锁的内存的任何进程中的任何线程操作(即,该锁可以在多个进程之间共享的共享内存对象中)。
// 返回值:成功返回0,失败返回错误码
// 2. 加锁和解锁函数
int pthread_spin_lock(pthread_spinlock_t* lock);
int pthread_spin_unlock(pthread_spinlock_t* lock);
// 返回值:成功返回0,失败返回错误码
// 发起函数调用时,其他线程已经锁定互斥量,pthread_spin_lock调用会不断检测锁的状态。
// 3. 销毁函数
int pthread_spin_destroy(pthread_spinlock_t* lock);
// 返回值:成功返回0,失败返回错误码
// 总结:使用上跟mutex差不多,差别就是锁被占用时,mutex会挂起当前线程(适用于时间比较长的场景),而spin会不停检测锁的状态(适用于时间比较短的场景)
十. 读者与写着问题
10.1 简述
读的机会比较多,写的机会比较少。如图,与生产者消费者的主要区别是,消费者与消费者的关系为竞争和互斥,而读者与读者的关系是没有关系,本质是读者读的时候不会拿走数据,而消费者是拿走数据的。
10.2 rwlock锁
// rwlock相关函数接口 头文件<pthread.h>
// 定义一把锁
pthread_rwlock_t rwlock;
// 1. 初始化函数
int pthread_rwlock_init(pthread_rwlock_t* restric rwlock, const pthread_rwlockattr_t* restrict attr);
// rwlock:传的是想要初始化的rwlock对象地址
// attr:是指定锁的属性,一般传NULL就好了
// 返回值:成功返回0,失败返回错误码
// 2. 加锁和解锁函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 以读者身份加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 以写者身份加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 返回值:成功返回0,失败返回错误码
// 3. 销毁函数
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
// 返回值:成功返回0,失败返回错误码