线程

线程基础知识

一.线程相关概念

1.线程的定义

进程中的一条执行流,是一个进程内的控制序列。
线程在linux下通过pcb实现,并且这些pcb共享了进程的大部分资源,相较于传统pcb更加轻量化,也称为轻量级进程

2.线程的独立和共享

独有:
标识符(唯一标识一个线程);
栈空间;
寄存器(存储上下文信息,状态信息,并不是物理上的硬件);
信号屏蔽字(信号的阻塞集合),线程信号的阻塞接口:pthread_sigmask;
errno(一个全局变量,保存上一次系统调用出错的原因编号),因为这是一个全局变量,所有线程都可以修改,多个线程对于错误原因使用同一个全局变量,那么错误原因就会不准确,所以为了让每个线程对自己的错误原因进行清晰的认识,每个线程有一个自己独立的errno;
优先级;

共享:
虚拟地址空间(代码段/数据段),
文件描述符表,
信号处理方式,
用户id/用户组id/工作路径

3.线程与进程之间的区别和联系

(1)联系

线程是进程中的一条执行流,同一进程的所属线程共享进程的资源。

(2)区别

进程是操作系统资源分配的最小单位,而线程是系统调度的最小单位;
进程有独立的空间地址,而线程没有独立的空间地址,多个线程共享所属进程的空间地址;
进程之间相互不影响,而同一进程的线程之间有同步和互斥关系,不同进程的线程间可以并发执行;
一个进程含有一至多个线程,而线程只能属于一个进程;
创建进程消耗时间较大,创建线程消耗时间较小;

4.多线程和多进程进行多任务处理的优缺点

多进程的优点:
稳定性更高,一个进程崩溃不会影响其他进程

多线程的优点:
(1)线程间通信更加灵活。除了进程间通信方式外,还可以通过全局变量和函数传参实现通信(线程间公用同一个进程的虚拟地址空间);
(2) 线程的创建和销毁成本更低。创建一个线程只需要创建一个pcb,而共有的数据通过一个指针指向同一个地址就可以实现共享;
(3)同一个进程中的线程切换调度成本更低。调度切换需要切换页表,多个线程间使用的是同一个页表,就不需要切换;

多线程的缺点:
稳定性差,一个线程崩溃,线程所在进程就会崩溃;

多进程的应用场景:
适用于对主进程安全性要求比较高的程序,比如shell,网络服务器
多线程的应用场景:
适用于多任务处理的场景

5.CPU密集型程序和IO密集型程序

(1)CPU密集型程序
当进行大量的数据运算时,如果CPU资源足够就可以同时处理,提高效率。通常执行流的个数是cpu核心数+1。
如果创建的线程很多,而cpu资源不够多,会造成线程切换调度成本的提高。

(2)IO密集型程序
主要有等待和数据拷贝两个过程,
多任务并行处理时,单磁盘可以并行压缩等待时间,多磁盘可以实现同时处理

6.线程使用起来有很多好处,是不是创建线程越多越好?

不是,因为CPU资源是固定的,如果线程多了,就会增加调度切换成本

二.线程控制

线程控制包括四个方面:
线程创建,线程终止,线程等待,线程分离

(1)线程创建

pthread_create函数

接口定义:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(start_routine)(void), void *arg);
参数:
thread:返回线程ID,一个输入输出型参数,用来获取线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:
成功返回0;失败返回一个非零值,也就是错误码

线程id是什么?

每个线程在进程的PCB中会有自己的一块相对独立的地址空间,线程id就是这块空间的首地址

查看当前线程id的命令

ps -ef -L | head -n 1 && ps -ef -L | grep 程序名

(2)线程退出

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 在线程入口函数return退出。这种方法对主线程不适用,从main函数return相当于退出进程
  2. 线程可以调用pthread_ exit终止自身;
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程;

疑难点

进程退出,会退出所有的线程.
主线程中退出,并不会导致进程退出,除非使用return;

接口介绍:

pthread_exit函数

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:线程退出的返回值
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

pthread_cancel函数

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

(3)线程等待

为什么需要线程等待?

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。

pthread_join函数

功能:一个阻塞接口,等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

(4)线程分离

默认情况下,新创建的线程是处于joinable状态的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

pthread_detach函数

功能:分离一个线程
原型:
int pthread_detach(pthread_t thread)
参数:线程id
返回值:成功返回0;失败返回错误码

三.线程安全

1.基本概念

1).什么是线程安全?

多个线程访问临界资源时,因为线程间的操作相互独立,对临界资源的访问是不安全的,这就是线程安全问题
线程安全需要通过同步和互斥实现

2).重入

同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入

3).可重入函数

一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

4).可重入和线程安全的区别和联系

区别

可重入函数是线程安全函数的一种;
线程安全不一定是可重入的,而可重入函数则一定是线程安全的;
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的;

联系

函数是可重入的,那就是线程安全的;
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题;
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的;


2.线程互斥

1)互斥锁

本质上是一个只有0和1两种状态的计数器,用于标记临界资源的访问状态,0表示不能访问,1表示可以访问。

在进入临界区之前加锁,判断资源是否可访问,
在退出临界区之后解锁,将资源状态置为可访问状态
互斥锁自身的计数操作是一个原子操作,它是线程安全的

2)互斥锁常见操作接口

初始化pthread_mutex_init

函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex:互斥量
attr:互斥量的属性,通常设置为NULL
返回值:成功返回0,失败返回错误编号

销毁pthread_mutex_destroy

函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:互斥量
返回值:成功返回0,失败返回错误编号

加锁pthread_mutex_lock

函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
作用:阻塞加锁
参数:mutex:互斥量
返回值:成功返回0,失败返回错误编号

下面的接口也可以加锁:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
作用:非阻塞加锁
这个接口返回值成功返回0,失败返回错误编号

解锁pthread_mutex_unlock

函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:mutex:互斥量
返回值:成功返回0,失败返回错误编号

3)互斥锁的使用流程

  1. 定义互斥锁变量
pthread_mutex_t mtx;
  1. 初始化互斥锁变量,有两种方式
    第一种:在定义时使用宏PTHREAD_MUTEX_INITIALIZER初始化;
    第二种:使用pthread_mutex_init接口

  2. 在临界资源访问之前加锁

  3. 在临界资源访问之后解锁

  4. 释放互斥锁

4)互斥锁相关知识

互斥锁是一种不可重入锁,也就是说只能加锁一次,加锁之后必须解锁才能继续使用。

5)死锁

程序运行因为某种原因卡死,无法继续运行,常见的比如资源不足;

死锁产生的四个必要条件

互斥条件;
资源在同一时刻只能有一个进程或线程访问

不可剥夺条件;
对于本进程或线程拥有的资源,只能自己主动释放,不允许别的进程或线程抢占当前拥有的资源;

请求和保持条件;
请求资源请求不到,不会释放自身已有的资源

环路等待条件
当前进程组/线程组中的每个进程/线程都在等到该组的其它进程/线程释放资源

死锁如何处理?

预防死锁:
破坏死锁的四个必要条件中的请求和保持条件或者环路等待条件,而互斥条件和不可剥夺条件是无法人为控制的。
在程序中可以提前计算好资源的请求和释放顺序;
请求不到新资源就释放已有资源;

避免死锁:
银行家算法和死锁检测算法


3.线程同步

通过条件判断实现对资源获取的合理性
在Linux下可以通过条件变量和信号量两种方式来实现

条件变量实现同步

条件变量的概念

条件变量是一种“事件通知机制”,它本身不提供、也不能够实现“互斥”的功能。因此,条件变量通常(也必须)配合互斥量来一起使用,其中互斥量实现对“共享数据”的互斥(即同步),而条件变量则去执行 “通知共享数据状态信息的变化”的任务。
条件变量本身并不确定资源什么时候获取合理,资源是否获取合理需要自行判断

条件变量函数
初始化

有两种初始化方式:
第一种:
通过pthread_cond_init接口
函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
返回值:成功返回0,失败返回错误编号

第二种:通过PTHREAD_COND_INITIALIZER这个宏在定义时初始化

pthread_cond_t con = PTHREAD_COND_INITIALIZER;
销毁pthread_cond_destroy

函数原型:
int pthread_cond_destroy(pthread_cond_t *cond)
参数: cond:要初始化的条件变量
返回值:成功返回0,失败返回错误编号

阻塞等待条件满足pthread_cond_wait

函数原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:等待的条件变量
mutex:互斥量
返回值:成功返回0,失败返回错误编号

为什么pthread_ cond_ wait 需要互斥量?

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

唤醒等待pthread_cond_signal

函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
作用:至少唤醒一个线程
参数:cond:要唤醒的条件变量
返回值:成功返回0,失败返回错误编号

唤醒所有PCB等待队列的线程pthread_cond_broadcast

int pthread_cond_broadcast(pthread_cond_t *cond);

条件变量使用中的注意事项
  1. 条件变量使用过程中,条件的判断应该采用循环操作
  2. 使用过程中,如果有多种角色就需要使用多个条件变量,不同角色的分开等待和唤醒;
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

//模拟顾客在餐馆就餐问题

#define MAX_COOKER 4	//厨师数
#define MAX_CUSTOMER 6	//顾客数

int bowl = 0;	//规定每次只能有一个人在吃饭,因为这是一个VIP餐厅,提供一对一服务,bowl为0表示没有饭,bowl为1表示有饭
pthread_mutex_t mtx;	//互斥锁
pthread_cond_t cond_cooker;	//厨师相关条件变量
pthread_cond_t cond_customer;	//顾客相关条件变量

//厨师线程入口函数
void* cookerEntry(void* arg)
{
	//厨师一直在不停的做饭
	while(1)
	{
		//1.加锁
		pthread_mutex_lock(&mtx);

		//2.判断是否有饭
		//有饭就阻塞等待
		//没有饭就做饭
		//做好饭之后唤醒等待的顾客
		//为什么是循环进行判断,这是因为当线程被唤醒时,有可能其他线程也被唤醒了,这时候资源被使用了,继续
		//使用资源是不合理的
		//在这个应用场景中,也就是现在没饭了,唤醒了厨师,但是其他厨师已经快人一步,将饭做好了,
		//这时候继续做饭就是不合理的,应该继续判断当前的状态
		while(bowl == 1)
		{
			//有饭就阻塞自己,阻塞操作需要循环进行
			pthread_cond_wait(&cond_cooker, &mtx);
		}
		//没有饭开始做饭
		printf("厨师正在做饭~\n");
		//厨师需要时间去做饭
		sleep(1);
		++bowl;
		//做好饭之后,唤醒顾客去吃饭
		pthread_cond_signal(&cond_customer);

		//3.解锁
		pthread_mutex_unlock(&mtx);
	}
	return NULL;
}

//顾客线程入口函数
void* customerEntry(void* arg)
{
	while(1)
	{
		//1.加锁
		pthread_mutex_lock(&mtx);

		//2.判断是否有饭
		//有饭就吃饭
		//吃完饭后唤醒等待的厨师
		//没有饭就阻塞等待
		//为什么是循环进行判断,这是因为当线程被唤醒时,有可能其他线程也被唤醒了,这时候资源被使用了,继续
		//使用资源是不合理的
		//在这个例子中,也就是现在有饭了,顾客被唤醒,但是其他顾客先人一步,将饭吃了,那么这时候继续吃饭就没饭了
		//需要继续判断当前到底有没有饭
		while(bowl == 0)
		{
			//没有饭阻塞等待
			pthread_cond_wait(&cond_customer, &mtx);
		}
		//有饭
		printf("好吃~\n");
		//顾客需要时间去吃饭
		sleep(1);
		--bowl;
		//唤醒厨师
		pthread_cond_signal(&cond_cooker);

		//3.解锁
		pthread_mutex_unlock(&mtx);
	}
	return NULL;
}

int main()
{
	//初始化互斥锁和条件变量
	pthread_mutex_init(&mtx, NULL);
	pthread_cond_init(&cond_cooker, NULL);
	pthread_cond_init(&cond_customer, NULL);

	pthread_t cookers[MAX_COOKER];		//厨师数组
	pthread_t customers[MAX_CUSTOMER];	//顾客数组

	//创建厨师线程
	for(int i = 0; i < MAX_COOKER; ++i)
	{
		int ret = pthread_create(&cookers[i], NULL, cookerEntry, NULL);
		if(ret != 0)
		{
			printf("create cooker thread failed!\n");
			continue;
		}
	}

	//创建顾客线程
	for(int i = 0; i < MAX_CUSTOMER; ++i)
	{
		int ret = pthread_create(&customers[i], NULL, customerEntry, NULL);
		if(ret != 0)
		{
			printf("create customer thread failed!\n");
			continue;
		}
	}

	//主线程阻塞等待线程退出
	for(int i = 0; i < MAX_COOKER; ++i)
	{
		pthread_join(cookers[i], NULL);
	}
	for(int i = 0; i < MAX_CUSTOMER; ++i)
	{
		pthread_join(customers[i], NULL);
	}

	//销毁互斥锁和条件变量
	pthread_mutex_destroy(&mtx);
	pthread_cond_destroy(&cond_cooker);
	pthread_cond_destroy(&cond_customer);

	return 0;
}

4.posix信号量实现同步和互斥

1)信号量回顾

信号量是程序中的一个计数器,加上一个PCB等待队列,用于实现进程和线程间的同步与互斥

2) 使用流程及接口

  1. 定义信号量
    sem_t sem;
  2. 初始化信号量
    int sem_init(sem_t *sem, int pshared, unsigned int value);
  3. 进行P操作
    int sem_wait(sem_t *sem);
    int sem_trywait(sem_t *sem);
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  4. 进行V操作
    int sem_post(sem_t *sem);
  5. 销毁信号量
    int sem_destroy(sem_t *sem);

3) 信号量实现生产者消费者模型

注意事项:
加锁和判断的顺序应该是先判断后加锁,有以下两点原因:

  1. 如果先加锁,判断时发生阻塞,那么锁就无法释放;
  2. 加锁会降低效率,加锁永远只保护需要保护的操作

RingQueue.h:

#pragma once

#include <vector>
#include <semaphore.h>

#define MAX_CAPACITY 6		//队列的容量

//使用信号量实现一个线程安全的阻塞队列
//信号量不仅仅是一个计数器,还有一个等待队列
//当数据不满足条件时,将请求进程放到等待队列中
class RingQueue
{
private:
	int _capacity;		//当前队列所能存储的容量,有一个空间不能使用,用来标记队列满的情况
	int _read;		//读指针,指向当前可以读取的位置
	int _write;		//写指针,指向当前可以写的位置
	std::vector<int> _data;	//数据域,存储数据
	sem_t _sem_mutex;	//用于实现互斥的信号量
	sem_t _sem_idle;	//用于实现生产者同步的信号量
	sem_t _sem_data;	//用于实现消费者同步的信号量

public:
	RingQueue(int cap = MAX_CAPACITY)
		:_capacity(cap + 1)
		,_read(0)
		,_write(0)
	{
		//多开辟一个大小的空间,是为了区分当前队列是否已满,队列中满的时候的容量是_capacity,有一个空间不放置数据
		_data.resize(cap + 1);
		//实现互斥的信号量初始化为1
		//int sem_init(sem_t *sem, int pshared, unsigned int value);
		//pshared为0表示初始化用于线程间的信号量,非0表示用于进程间
		sem_init(&_sem_mutex, 0, 1);
		//实现生产者同步的信号量初始化为cap,表示现在可以写入cap个数据
		sem_init(&_sem_idle, 0, cap);
		//实现消费者同步的信号量初始化为0,表示没有可以使用的数据
		sem_init(&_sem_data, 0, 0);
	}

	~RingQueue()
	{
		sem_destroy(&_sem_mutex);
		sem_destroy(&_sem_idle);
		sem_destroy(&_sem_data);
	}

	//关于Push和Pop为什么是先判断后加锁呢?
	//1.如果先加锁,判断时发生阻塞,那么锁就无法释放;
	//2.加锁会降低效率,加锁永远只保护需要保护的操作
	bool Push(int val)
	{
		//判断能否入队数据
		//空闲节点数-1,如果小于0阻塞
		//这个操作内部本身是有实现线程安全的
		sem_wait(&_sem_idle);
		
		//互斥访问保护数据操作
		//加锁
		sem_wait(&_sem_mutex);

		//插入数据
		_data[_write] = val;
		//更新写指针
		_write = (_write + 1) % _capacity;

		//解锁
		sem_post(&_sem_mutex);

		//数据节点数+1,唤醒消费者
		sem_post(&_sem_data);

		return true;
	}

	//出队的数据存放到data这个输入输出型参数中
	bool Pop(int* data)
	{
		//判断能否出队数据
		//数据节点-1,小于0进行阻塞,这个操作内部是线程安全的
		sem_wait(&_sem_data);

		//互斥信号量保护数据读取操作
		//加锁
		sem_wait(&_sem_mutex);

		//将要读取的数据写入data这个输入输出型参数中
		*data = _data[_read];
		//更新读指针
		_read = (_read + 1) % _capacity;
		//唤醒生产者

		//解锁
		sem_post(&_sem_mutex);
		
		//空闲节点+1,唤醒生产者线程
		sem_post(&_sem_idle);

		return true;
	}

	//判断当前队列是否为空
	bool empty()
	{
		//当读指针和写指针在同一个位置,说明还没有写入数据
		return _read == _write;
	}

	//判断当前队列是否已满
	bool full()
	{
		//当写指针的下一个位置是读指针,说明已经写入了_capacity个数据,数据域满了
		return _read == (_write + 1) % _capacity;
	}
};

pro_cus.cpp:

#include <iostream>
#include "RingQueue.h"
#include <unistd.h>

#define MAX_PRODUCER 5		//生产者的最大数目
#define MAX_CUSTOMER 6		//消费者的最大数目

using std::cout;
using std::endl;

//生产者线程
void* producerEntry(void* arg)
{
	RingQueue* q = reinterpret_cast<RingQueue*>(arg);
	while(1)
	{
		static int i = 1;
		bool ret = q->Push(i++);
		if(!ret)
		{
			cout << "thread " << pthread_self() << " Push data " << i << " is failed" << endl;
		}
		else
		{
			cout << "thread " << pthread_self() << "Push data " << i << " is successful" << endl;
		}
	}
	return NULL;
}

//消费者线程
void* customerEntry(void* arg)
{
	RingQueue* q = reinterpret_cast<RingQueue*>(arg);
	int data;
	while(1)
	{
		bool ret = q->Pop(&data);
		if(!ret)
		{
			cout << "thread " << pthread_self() << " Pop data failed" << endl;	
		}
		else
		{
			cout << "thread " << pthread_self() << " Pop data successfully , data is " << data << endl;
		}
	}
	return NULL;
}

int main()
{
	//生产者和消费者县城要使用同一个环形队列存储数据,这样数据才能进行通信
	RingQueue q;
	pthread_t pro_tid[MAX_PRODUCER];
	pthread_t cus_tid[MAX_CUSTOMER];	
	int ret;

	//创建生产者线程
	for(int i = 0; i < MAX_PRODUCER; ++i)
	{
		ret = pthread_create(&pro_tid[i], NULL, producerEntry, &q);
		if(ret != 0)
		{
			cout << "创建生产者线程失败" << endl;
			continue;
		}
		else
		{
			cout << "创建生产者线程成功,线程id是" << pro_tid[i] << endl;
		}
	}

	//创建消费者线程
	for(int i = 0; i < MAX_CUSTOMER; ++i)
	{
		ret = pthread_create(&cus_tid[i], NULL, customerEntry, &q);
		if(ret != 0)
		{
			cout << "创建消费者线程失败" << endl;
			continue;
		}
		else
		{
			cout << "创建消费者线程成功,线程id是" << cus_tid[i] << endl;
		}
	}

	//线程等待,让主进程不退出
	for(int i = 0; i < MAX_PRODUCER; ++i)
	{
		ret = pthread_join(pro_tid[i], NULL);
		if(ret != 0)
		{
			cout << "阻塞等待线程 " << pro_tid[i] << " 失败" << endl;
			continue;
		}
		else
		{
			cout << "阻塞等待线程 " << pro_tid[i] << " 成功" << endl;
		}
	}
	for(int i = 0; i < MAX_CUSTOMER; ++i)
	{
		ret = pthread_join(cus_tid[i], NULL);
		if(ret != 0)
		{
			cout << "阻塞等待线程 " << cus_tid[i] << " 失败" << endl;
			continue;
		}
		else
		{
			cout << "阻塞等待线程 " << cus_tid[i] << " 成功" << endl;
		}

	}

	return 0;
}

5.信号量和条件变量的区别

  1. 信号量本质是一个程序中的计数器+一个PCB等待队列,而条件变量的本质是一个PCB等待队列;
  2. 条件变量需要搭配互斥锁一起使用,信号量不需要,信号量内部是线程安全的;
  3. 条件变量的资源获取条件需要自己判断,信号量通过自身计数完成;

四.线程应用

1.生产者与消费者模型

一种进程负责产生数据,另一方负责处理数据,双方将数据放到公共的缓冲区中,这个公共缓冲区的数据放入和拿出必须是线程安全的

优点:解耦合,支持忙闲不均,支持并发

代码实现:
BlockQueue.h:

#pragma once

#include <queue>
#include <pthread.h>

//线程安全的阻塞队列

#define MAX_CAPACITY 6		//阻塞队列中的最大容量

class BlockQueue
{
private:
	size_t _capacity = MAX_CAPACITY;	//队列的最大容量
	pthread_mutex_t _mtx;			//互斥锁
	pthread_cond_t _cond_producer;		//生产者相关的条件变量
	pthread_cond_t _cond_consumer;		//消费者相关的条件变量
	std::queue<int> _queue;
public:
	BlockQueue()
	{
		//在构造函数中初始化互斥锁和条件变量
		pthread_mutex_init(&_mtx, NULL);
		pthread_cond_init(&_cond_producer, NULL);
		pthread_cond_init(&_cond_consumer, NULL);
	}
	~BlockQueue()
	{
		//在析构函数中销毁互斥锁和环境变量
		pthread_mutex_destroy(&_mtx);
		pthread_cond_destroy(&_cond_producer);
		pthread_cond_destroy(&_cond_consumer);
	}

	//生产者向队列中写数据
	bool Push(int data)
	{
		//1.加锁
		pthread_mutex_lock(&_mtx);

		//2.判断队列中是否已经满了
		while(_queue.size() == _capacity)
		{
			//队列中数据满了,就阻塞等待,直到队列中可以写数据
			pthread_cond_wait(&_cond_producer, &_mtx);
		}
		//队列中可以写数据,将数据写入
		_queue.push(data);
		//唤醒等待的消费者线程
		pthread_cond_signal(&_cond_consumer);

		//3.解锁
		pthread_mutex_unlock(&_mtx);
		
		return true;
	}

	//消费者队列出对数据
	bool Pop(int* data)
	{
		//1.加锁
		pthread_mutex_lock(&_mtx);

		//2.判断队列中是否为空
		while(_queue.empty())
		{
			//队列为空,消费者阻塞等待
			pthread_cond_wait(&_cond_consumer, &_mtx);
		}
		//队列中有数据就出队
		*data = _queue.front();
		_queue.pop();
		//唤醒等待的生产者进程
		pthread_cond_signal(&_cond_producer);

		//3.解锁
		pthread_mutex_unlock(&_mtx);

		return true;
	}
};

pro_cus.cpp:

#include <iostream>
#include <unistd.h>
#include "BlockQueue.h"

using std::cout;
using std::endl;

#define MAX_PRODUCER 3
#define MAX_CONSUMER 5


//生产者线程入口函数
void* producerEntry(void* arg)
{
	//之前在这里类型强转时使用普通变量来接收,这样是有问题的
	//BlockQueue q = *(BlockQueue*)arg;
	BlockQueue* q = (BlockQueue*)arg;
	static int i = 1;
	while(1)
	{
		q->Push(i);
		cout << pthread_self() << " write data : " << i << endl;
		++i;
	}
	return NULL;
}

//消费者线程入口函数
void* consumerEntry(void* arg)
{
	BlockQueue* q = (BlockQueue*)arg;
	int buf;
	while(1)
	{
		q->Pop(&buf);
		cout << pthread_self() << " read from queue is " << buf << endl; 
	}
	return NULL;
}

int main()
{
	pthread_t pro_tid[MAX_PRODUCER];
	pthread_t con_tid[MAX_CONSUMER];
	
	//创建同一个阻塞队列作为缓冲区
	BlockQueue q;

	//创建生产者线程
	for(int i = 0; i < MAX_PRODUCER; ++i)
	{
		int ret = pthread_create(&pro_tid[i], NULL, producerEntry, (void*)&q);
		if(ret == 0)
		{
			cout << "create thread successfully!" << endl;
			continue;
		}
	}
	
	//创建消费者线程
	for(int i = 0; i < MAX_CONSUMER; ++i)
	{
		int ret = pthread_create(&con_tid[i], NULL, consumerEntry, (void*)&q);
		if(ret == 0)
		{
			cout << "create thread successfully!" << endl;
			continue;
		}
	}

	//等待线程退出
	for(int i = 0; i < MAX_PRODUCER; ++i)
	{
		pthread_join(pro_tid[i], NULL);
	}
	for(int i = 0; i < MAX_CONSUMER; ++i)
	{
		pthread_join(con_tid[i], NULL);
	}

	return 0;
}

2.线程池

1) 线程池的工作原理

创建好一堆线程(有最大数量上限)和一个任务队列,当有新任务到来时,将新任务放到任务队列中,线程池中的线程从任务队列中获取任务进行处理

2)使用场景

针对大量请求进行处理

3)优势

  1. 避免了因为大量创建和销毁线程而带来的时间成本;
  2. 线程池中的线程数量和任务队列中存储的任务有最大上限,避免了峰值压力下资源耗尽,系统崩溃的风险;

4)实现一个线程池

  1. 定义一个任务节点类,通过实例化一个任务节点对象,调用对象的方法进行任务处理
  2. 实现一个线程安全的任务队列;
  3. 定义一个线程池类,
    这个类有三个成员:最大线程数、最大任务节点数、线程安全队列;
    提供可以指向线程数和任务节点数目的构造函数,提供创建线程的初始化接口,提供任务节点入队接口;

代码实现:
ThreadPool.h:

#pragma once

#include <queue>
#include <pthread.h>
#include <cstdio>
#include <errno.h>
#include <unistd.h>

typedef void(*handler_t)(int);	//定义一个函数指针类型

//任务节点
class TaskNode
{
	private:
		handler_t _handler;	//任务节点对应的处理函数 
		int _data;		//任务节点处理的数据

	public:
		//空的构造函数保证能实例化一个无参对象
		TaskNode(){}

		//任务节点在构造时将处理的任务函数和参数传入
		TaskNode(handler_t handler, int data)
			:_handler(handler)
			,_data(data)
	{}

		//设置任务和参数
		void SetTask(handler_t handler, int data)
		{
			_handler = handler;	
			_data = data;
		}

		void Run()
		{
			_handler(_data);
		}
};

//***********************线程安全的阻塞队列******************************

#define MAX_CAPACITY 6		//阻塞队列中的最大容量

class BlockQueue
{
	private:
		size_t _capacity;	//队列的最大容量
		pthread_mutex_t _mtx;			//互斥锁
		pthread_cond_t _cond_producer;		//生产者相关的条件变量
		pthread_cond_t _cond_consumer;		//消费者相关的条件变量
		std::queue<TaskNode> _queue;
	public:
		BlockQueue(int cap = MAX_CAPACITY)
			:_capacity(cap)
		{
			//在构造函数中初始化互斥锁和条件变量
			pthread_mutex_init(&_mtx, NULL);
			pthread_cond_init(&_cond_producer, NULL);
			pthread_cond_init(&_cond_consumer, NULL);
		}
		~BlockQueue()
		{
			//在析构函数中销毁互斥锁和环境变量
			pthread_mutex_destroy(&_mtx);
			pthread_cond_destroy(&_cond_producer);
			pthread_cond_destroy(&_cond_consumer);
		}

		//生产者向队列中写数据
		bool Push(const TaskNode& data)
		{
			//1.加锁
			pthread_mutex_lock(&_mtx);

			//2.判断队列中是否已经满了
			while(_queue.size() == _capacity)
			{
				//队列中数据满了,就阻塞等待,直到队列中可以写数据
				pthread_cond_wait(&_cond_producer, &_mtx);
			}
			//队列中可以写数据,将数据写入
			_queue.push(data);
			//唤醒等待的消费者线程
			pthread_cond_signal(&_cond_consumer);

			//3.解锁
			pthread_mutex_unlock(&_mtx);

			return true;
		}

		//消费者队列出队数据
		bool Pop(TaskNode* data)
		{
			//1.加锁
			pthread_mutex_lock(&_mtx);
			/*	
			进行非阻塞等待也是可以的
			while(pthread_mutex_trylock(&_mtx) == EBUSY)
			{
				printf("Inside BlockQueue Pop, trylock failed!\n");
				usleep(500000);
			}
			*/
			
			//2.判断队列中是否为空
			while(_queue.empty())
			{
				//队列为空,消费者阻塞等待
				pthread_cond_wait(&_cond_consumer, &_mtx);
			}
			//队列中有数据就出队
			*data = _queue.front();
			_queue.pop();
			//唤醒等待的生产者进程
			pthread_cond_signal(&_cond_producer);

			//3.解锁
			pthread_mutex_unlock(&_mtx);

			return true;
		}
};

#define MAX_THREAD_NUM 6	//线程池中能创建的最大线程数
#define MAX_QUEUE_NODE_NUM 10	//线程池中的任务队列最多存储的节点数

//*******************************线程池类*************************************
class ThreadPool
{
private:
	int _max_thread_num;		//最大线程数
	int _max_queue_node_num;	//任务队列中最大节点数
	BlockQueue _queue;		//线程安全的任务队列
private:
	//线程入口函数,负责将任务节点的处理函数转换为创建线程要求的格式,并执行任务处理动作
	//设置为静态的成员函数,避免this指针造成的影响
	static void* Entry(void* arg)
	{
		//这里在类型转换的时候太粗心,将arg转换为了BlockQueue*!!!!!!!!!!!!!!!
		ThreadPool* tp = (ThreadPool*)arg;
		while(1)
		{
			TaskNode task;
			//拿到任务处理队列中的任务,并让其出队
			tp->_queue.Pop(&task);
			task.Run();
		}

		return NULL;
	}
public:
	//构造函数中不能进行初始化,防止异常产生
	ThreadPool(int threadNum = MAX_THREAD_NUM, int queueNodeNum = MAX_QUEUE_NODE_NUM)
		:_max_thread_num(threadNum)
		,_max_queue_node_num(queueNodeNum)
		,_queue(BlockQueue(queueNodeNum))
	{}
	
	//创建线程
	bool Init()
	{
		pthread_t tid;
		int ret;
		for(int i = 0; i < _max_thread_num; ++i)
		{
			ret = pthread_create(&tid, NULL, Entry, (void*)this);
			if(ret != 0)
			{
				printf("创建线程失败!\n");
				return false;
			}
			//线程分离,让线程退出时自动释放资源
			pthread_detach(tid);
		}
		return true;
	}

	//任务节点入队
	bool Push(const TaskNode& task)
	{
		_queue.Push(task);
		return true;
	}
};

test.cpp:

#include <cstdio>
#include <unistd.h>
#include "ThreadPool.h"


void taskFun(int data)
{
	printf("我是线程 %p , 我拿到了数据 %d, 我要休眠1s\n", pthread_self(), data);
	sleep(1);
}

void test_threadpool()
{
	ThreadPool threadPool;
	TaskNode task;
	threadPool.Init();

	for(int i = 0; i < 10; ++i)
	{
		task.SetTask(taskFun, i);	
		bool ret = threadPool.Push(task);
		if(ret)
		{
			printf("push task successful!\n");
		}
	}


	while(1)
	{
		printf("我是主线程!\n");
		sleep(1);
	}
}

int main()
{
	test_threadpool();
	return 0;
}

3.线程安全的单例模式

1)什么是单例模式?

一种创建类型的设计模式,规定一个类只能实例化一个对象

2 )两种类型的单例模式

饿汉模式

提前创建好一份对象实例,资源在启动时就加载

代码实现:

#pragma once

template<class T>
class SingleTon
{
public:
	static SingleTon<T>* getInstance()
	{
		return _instance;
	}
private:
	static SingleTon<T>* _instance;		//这个类实例化后的唯一的一份资源
private:
	SingleTon(){}
	SingleTon(const SingleTon<T>& obj) = delete;
	SingleTon<T>& operator=(const SingleTon<T>& obj) = delete;
};

//静态成员类外实例化
template<class T>
SingleTon<T>* SingleTon<T>::_instance = new SingleTon<T>(); 

懒汉模式

资源并不会一开始就加载,而是在第一次使用的时候加载,是一种延时加载的思想

代码实现:

#pragma once

#include <pthread.h>

template<class T>
class SingleTon
{
private:
	static SingleTon<T>* _instance;		//对应的实例化对象
	static pthread_mutex_t* _mtx;		//用于保证线程安全的互斥锁
private:
	SingleTon()
	{
		//构造函数中初始化互斥锁
		pthread_mutex_init(_mtx, NULL);
	}
	//删除拷贝构造和赋值运算符重载
	SingleTon(const SingleTon<T>& obj) = delete;
	SingleTon<T>& operator=(const SingleTon<T>& obj) = delete;
public:
	//提供销毁单例对象的接口
	void Destroy()
	{
		delete _instance;
		pthread_mutex_destroy(_mtx);
	}
	static SingleTon<T>* getInstance()
	{
		//第一层判断,提高效率
		if(_instance == nullptr)
		{
			pthread_mutex_lock(_mtx);
			//第二层判断,在资源不存在时实例化资源
			if(_instance == nullptr)
			{
				_instance = new SingleTon<T>;
			}
			pthread_mutex_unlock(_mtx);
		}
		return _instance;
	}
};

template<class T>
SingleTon<T>* SingleTon<T>::_instance = nullptr;	//将资源初始化为空

template<class T>
pthread_mutex_t* SingleTon<T>::_mtx = new pthread_mutex_t();

对于懒汉模式的实现,因为对泛型不熟悉,个人觉得有一些小问题,但是暂时找不出来,希望各位指点!!!!!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值