【Linux的线程篇章 - 生产消费模型知识储备】

Linux之生产消费模型知识储备

前言:
前篇开始进行了解学习Linux线程基本知识等相关内容,接下来学习关于Linux线程生产消费模型知识,深入地了解这个强大的开源操作系统。
/知识点汇总/

1、线程同步

同步:可以是严格的顺序性,也可使宏观的顺序。旨在保证多个线程按照一定的顺序执行,以避免数据不一致和竞争条件等问题。

1.1、定义

线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对该内存地址进行操作,直到该线程完成操作。其他线程处于等待状态,直到获得对共享资源的访问权限。这种方式确保了线程之间的有序执行和资源的安全访问。

1.2、目的

线程同步的主要目的是解决多线程间的共享资源访问问题,确保多个线程在访问共享资源时的正确性和可靠性。通过同步机制,可以避免数据损坏、程序崩溃等不可预期的结果。

1.3、应用

互斥锁(Mutex):
互斥锁是最基本的同步机制之一。它保证在同一时间只有一个线程可以访问共享资源。当一个线程获得了互斥锁后,其他线程需要等待该线程释放锁才能继续访问共享资源。
信号量(Semaphore)
信号量是一种计数器,用来控制同时访问某个共享资源的线程数量。当计数器大于0时,线程可以访问资源并将计数器减1;当计数器等于0时,线程需要等待其他线程释放资源后才能继续访问。
临界区(Critical Section):
临界区是一段代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图访问公共资源,则在有一个线程进入后,其他试图访问公共资源的线程将被挂起,直到进入临界区的线程离开。

1.4、意义

线程同步的意义:
线程同步对于保证多线程环境下程序的正确性和可靠性至关重要。它避免了数据竞争、死锁等问题,确保了共享资源的安全访问。同时,合理的线程同步还可以提高程序的执行效率,减少资源等待和线程竞争带来的性能损耗。

2、条件变量(基于pthread库)

在POSIX线程(pthread)库中,条件变量是一种同步机制,它允许线程等待某个条件为真时才继续执行。条件变量通常与互斥锁(mutex)一起使用,以避免出现竞态条件(race condition)和确保线程安全。

2.1、条件变量的基本概念

条件变量
一个同步原语,用于阻塞一个或多个线程,直到另一个线程修改了某些条件并通知它们。
互斥锁
一个同步机制,用于保护共享数据不被多个线程同时访问。

2.2、条件变量的基本操作

初始化:
使用pthread_cond_init()函数初始化条件变量。
等待(阻塞):
线程在调用pthread_cond_wait()时,会释放互斥锁并进入等待状态,直到另一个线程调用pthread_cond_signal()或pthread_cond_broadcast()来唤醒它。在pthread_cond_wait()返回时,线程会重新获得互斥锁。
信号(唤醒):
pthread_cond_signal():唤醒等待该条件变量的一个线程。
pthread_cond_broadcast():唤醒等待该条件变量的所有线程。
销毁:
使用pthread_cond_destroy()函数销毁条件变量。

2.3、示例代码

以下是一个简单的示例,展示了如何使用条件变量和互斥锁来同步两个线程:

a、快速认识接口
man pthread_cond_init

#include <pthread.h>
int pthread_cond_destory(pthread_cond_t * cond);//释放

int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_cond_t* restrict attr);//如果是栈、堆上开辟的就使用init,定义条件变量,attr设置为nullptr

pthread_cond_t cond = PTHREAD_COND_INITIALIZER(initializer);//全局或静态的条件变量

int pthread_cond_wait(pthread_cond_t* testrict cond, pthread_mutex_t* restrict mutex);//等待条件变量

通知 / 唤醒
int pthread_cond_boradcast(pthread_cond_t * cond);//唤醒所有线程
int pthread_cond_signal(pthread_cond_t* cond);//唤醒一个线程

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
  
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;  
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  
int ready = 0;  
  
void* thread_func(void* arg) {  
    pthread_mutex_lock(&lock);  
    while (!ready) {  
        // 等待条件满足  
        pthread_cond_wait(&cond, &lock);  
    }  
    printf("Thread is ready to work\n");  
    pthread_mutex_unlock(&lock);  
    return NULL;  
}  
  
int main() {  
    pthread_t t;  
  
    // 创建线程  
    pthread_create(&t, NULL, thread_func, NULL);  
  
    // 假设这里有一些初始化代码  
    sleep(1); // 模拟初始化过程  
  
    pthread_mutex_lock(&lock);  
    ready = 1; // 设置条件变量  
    pthread_cond_signal(&cond); // 唤醒线程  
    pthread_mutex_unlock(&lock);  
  
    pthread_join(t, NULL);  
  
    return 0;  
}

b、注意事项

1.避免虚假唤醒:
即使没有线程调用pthread_cond_signal()或pthread_cond_broadcast(),等待的线程也可能会醒来(虚假唤醒)。因此,通常需要将条件变量的等待放在一个循环中,并检查条件是否真正满足。
2.持有锁时等待
在调用pthread_cond_wait()之前,必须持有与条件变量相关联的互斥锁。在pthread_cond_wait()返回时,线程将重新获得该锁。
3.避免死锁
确保在适当的时候释放和重新获取互斥锁,以避免死锁。
4.使用PTHREAD_MUTEX_INITIALIZER和PTHREAD_COND_INITIALIZER
对于静态分配的条件变量和互斥锁,可以使用这些宏进行初始化。对于动态分配的对象,应使用pthread_mutex_init()和pthread_cond_init()函数。

3、生产消费模型

生产消费模型是一种常见的多线程设计模式,它描述了一种将生产者与消费者分离的设计方法。在这种模型中,生产者负责生成数据并将其放入共享缓冲区,而消费者则从该缓冲区中取出数据进行处理。

3.1、基本概念

1.生产者(Producer):
负责生成数据的线程或进程。它不断地向共享缓冲区中写入数据。
2.消费者(Consumer):
负责处理数据的线程或进程。它从共享缓冲区中读取数据并进行相应的处理。
3.共享缓冲区(Shared Buffer):
生产者和消费者之间的数据交换区域。它充当了生产者和消费者之间的媒介,用于存储生产者生成的数据,供消费者取用。

3.2、工作原理

1.生产者操作:

a、生产者生成数据。
b、生产者将数据写入共享缓冲区。
c、如果缓冲区已满,生产者将等待直到缓冲区中有空间可用。
d、生产者可能通过某种机制(如信号量、互斥锁等)通知消费者数据已准备好。

2.消费者操作:

a、消费者从共享缓冲区中读取数据。
b、消费者处理读取到的数据。
c、如果缓冲区为空,消费者将等待直到缓冲区中有数据可读。
d、消费者可能通过某种机制(如信号量、互斥锁等)通知生产者缓冲区已空,可以继续写入数据。

3.3、特点与优势

1.解耦合:
生产者和消费者之间通过共享缓冲区进行通信,实现了生产者与消费者之间的解耦合。这样,生产者和消费者可以独立地设计、修改和扩展,而不会相互影响。
2.提高系统吞吐量
通过生产者和消费者的并行处理,可以显著提高系统的数据处理能力和吞吐量。
3.平衡负载:
在生产消费模型中,可以根据系统的实际需求动态地调整生产者和消费者的数量,以实现负载的均衡分配。
4.支持并发:
生产消费模型是一种典型的并发模型,它允许生产者和消费者并发地执行,从而提高了系统的并发处理能力。

3.4、实现方式

生产消费模型可以通过多种方式实现,包括但不限于:

**1.使用阻塞/环形队列:**可以使用阻塞队列(如BlockingQueue)或环形队列(ringqueue)来实现生产消费模型。
阻塞队列(BlockingQueue)
支持在队列为空时阻塞消费者,在队列满时阻塞生产者。
环形队列(RingQueue)
是一种使用固定大小的数组来模拟队列的数据结构,其中数组的末尾与开头相连,形成一个环形。这种结构在需要固定大小缓冲区的场景下非常有用,因为它可以高效地利用空间,避免在队列满时还需要进行额外的内存分配。
2.使用信号量和互斥锁:
通过信号量(Semaphore)和互斥锁(Mutex)等同步机制来实现生产者和消费者之间的同步和互斥。
3.使用条件变量:
在C/C++等语言中,可以使用条件变量(Condition Variable)与互斥锁结合来实现生产消费模型。条件变量允许线程在特定条件未满足时挂起,并在条件满足时被唤醒。

3.5、应用场景

生产消费模型广泛应用于各种需要并发处理数据的场景,如:

1.多任务处理系统
在操作系统中,可以使用生产消费模型来管理多个任务的执行和调度。
2.数据处理系统:
在大数据处理、实时数据分析等领域,生产消费模型可以用于实现数据的生成、处理和消费。
3.网络通信:
在网络通信中,可以使用生产消费模型来管理网络数据的发送和接收。

总之,生产消费模型是一种高效、灵活的多线程设计模式,它通过解耦合生产者和消费者、提高系统吞吐量和支持并发处理等方式,为各种并发数据处理场景提供了有力的支持。

6、生产消费BlockingQueue模型介绍

基于BlockingQueue的生产者消费者模型:
是在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构,与其普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入元素;当队列满时,往队列存放元素的操作也会被阻塞,直到有元素被从队列中取出;
(以上的操作都是基于不同的线程来说的,线程在对阻塞从队列操作时会被阻塞)

6.1、BlockingQueue模型实现

实现方式:单生产,单消费

BlockQueue.hpp

#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include <ctime>

const static int defaultcap = 5;

template<typename T>
class BlockQueue
{
private:
	bool IsFull()
	{
		return _block_queue.size() == _max_cap;
	}
	bool IsEmpty()
	{
		return _block_queue.empty();
	}
public:
	BlockQueue(int cap = defaultcap)
		:_max_cap(cap)
	{
		pthread_mutex_init(&_mutex, nullptr);//初始化锁
		pthread_cond_init(&_p_cond, nullptr);//初始化条件变量
		pthread_cond_init(&_c_cond, nullptr);//初始化条件变量
	}
	void Pop(T* out)//消费者消费 -- 也涉及加锁解锁
	{
		pthread_mutex_lock(&_nutex);
		//if (IsEmpty())//生产队列空了
		while (IsEmpty())//生产队列空了
		{
			pthread_cond_wait(&_c_cond, &_mutex);//当生产者使用broadcast方式唤醒时,若两个消费者,都被唤醒都在这里等,尽管其中一个被唤醒成功拿到锁去消费操作,另一个尽管也被唤醒,也会在这里等待锁
			//这样可以避免,发生,两个消费者去抢仅剩余一件生产物品的情况;
			//添加尚未满足,但是线程被异常唤醒的情况,叫做位唤醒。因此,这里不建议用if,而是用while保证代码健壮性
		}
		//程序走到这,1.没有空 2.被唤醒;
		//说明有数据,则消费
		*out = _block_queue.front();
		_block_queue.pop();
		pthread_mutex_unlock(&_mutex);
		//唤醒生产者
		pthread_cond_signal(&_p_cond);
	}
	//生产者与消费者,相互唤醒。即,生产一个消费一个消费一个生产一个。
	void Equeue(const T& in)
	{
		pthread_mutex_lock(&_mutex);
		//if (IsFull())//生产队列满了,-》执行阻塞等待,并不是死等所以,还需要设置唤醒
		while (IsFull())
		{
			//等待
			//由于在临界区,针对pop拿不到锁的情况,所以加第二个参数&_mutex
			//被调用时,除了让自己继续排队等待,还会自己释放传入的锁
			//函数返回时,不就还在临界区吗?
			//所以返回时,必须先参与锁的竞争,重新加上锁,该函数才会返回。
			pthread_cond_wait(&_p_cond,&_mutex);
		}
		//程序走到这,1.没有满 2.被唤醒;
		_block_queue.push(in);//生产到阻塞队列 --- 说明,保证阻塞队列里至少有一个数据,不为空
		pthread_mutex_unlock(&_mutex);
		//唤醒消费者
		pthread_cond_signal(&_c_cond);

		//有数据,消费者消费
	}
	~BlockQueue()
	{
		pthread_mutex_destory(&_mutex);//销毁/释放锁
		pthread_cond_destory(&_p_cond);//销毁条件变量/释放
		pthread_cond_destory(&_c_cond);//销毁条件变量/释放
	}
private:
	std::queue<T> _block_queue;//设置为临济资源,保护数据
	int _max_cap;
	pthread_mutex_t _mutex;
	pthread_cond_t _p_cond;//生产者的条件变量
	pthread_cond_t _c_cond;//消费者的条件变量
};

main.cc

#include "BlockQueue.hpp"

void* Consumer(void* args)
{
	BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
	while (true)
	{
		sleep(2);
		//1.获取数据
		int data = 0;
		bq->Pop(&data);
		//2.处理数据
		std::cout << "Consumer -> " << data << std::endl;
	}
}

void* Productor(void* args)
{
	srand((unsigned int)time(nullptr) ^ getpid());//随机数种子
	BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
	while (true)
	{
		sleep(2);
		//1.构建数据
		int data = rand() % 10 + 1;//[1 10]
		//2.生产数据
		bq->Equeue(data);
		std::cout << "Productor-> " << data << std::endl;
	}
}

int main()
{
	BlockQueue<int>* bp = new BlockQueue<int>();
	pthread_t c, p;
	pthread_create(&c, nullptr, Consumer, bq);
	pthread_create(&p, nullptr, Productor, bq);

	pthread_join(c, nullptr);
	pthread_join(p, nullptr);
	return 0;
}

6.2、同步机制

在生产消费BlockingQueue模型中,阻塞队列提供了同步机制,确保生产者和消费者之间的协调和同步,避免竞态条件和数据不一致性等问题。生产者通过put或offer方法将元素放入队列,如果队列满,则生产者会阻塞等待;消费者通过take或poll方法从队列中获取元素,如果队列空,则消费者会阻塞等待。
如何理解效率的问题?

高效利用CPU:BlockingQueue通过阻塞机制,确保生产者在队列满时不会进行无用的计算或数据生成,消费者在队列空时也不会进行无意义的轮询或等待,从而提高了CPU的利用率。
减少线程冲突:BlockingQueue通过内部锁机制,确保在同一时间内只有一个线程可以修改队列的状态(添加或移除元素),从而减少了线程间的冲突和竞争。

为什么都是在临界区等待呢?

1.资源竞争:
多个线程可能同时尝试访问同一个BlockingQueue,如果队列状态不满足操作条件(如满或空),则线程需要等待。
2.同步需求
BlockingQueue内部实现了必要的同步机制(如锁、条件变量等),以确保线程在访问队列时的安全性和一致性。线程在临界区等待是同步机制的一部分。
3.避免死锁和活锁
合理的等待机制可以避免死锁和活锁的发生。死锁是指两个或多个线程相互等待对方释放资源而无法继续执行的情况;活锁是指线程在相互等待对方改变状态而无法继续执行的情况。BlockingQueue的阻塞机制通过确保线程在满足条件时被唤醒,从而避免了这些问题。

7、生产消费RingQueue模型介绍

生产消费RingQueue模型是一种在多线程或并发环境下常用的设计模式,主要用于解决生产者和消费者之间的同步与互斥问题。在这个模型中,生产者负责生成数据并将其放入队列中,而消费者则从队列中取出数据进行处理。RingQueue,即环形队列,是一种特殊的队列实现方式,它通过循环利用数组空间来避免在队列满时因扩容而导致的性能开销。

7.1、POSIX信号量

信号量的本质是一个计数器,实现对资源的预定机制。(涉及的操作就是P/V原子操作)
所以本质就是对公共资源的预定机制。

信号量的使用:

在生产消费RingQueue模型中,信号量(Semaphore)被用来控制生产者和消费者之间的同步与互斥。信号量本质上是一个计数器,用于管理对共享资源的访问。
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。
但是POSIX可以用于线程间同步。

快速认识信号量的接口

man sem_init
#include <semaphore.h>
int sem_init(sem_t* sem,int pshared, unsigned int value);
int dem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_post(sem_t* sem);

思考:多线程如何在环形队列中进行生产和消费?
当队列为空 || 为满的时候,head == end
引入:计数器/牺牲一个空位置head==end+1;
极端情况:
1.队列为空时,让谁先访问?生产者end先生产
2.当队列满了,该让谁访问呢?消费者head消费,生产者停止end
验证了:具备顺序性(同步)&& 互斥
正常情况下(理性情况):队列不为空&&队列不满,head和end–》head!=end同时可访问。

所以结论:

1.不能让生产者把消费者套一个圈;
2.不能让消费者,超过生产者;
3…队列为空时,让谁先访问?生产者先生产
4.当队列满了,该让谁访问呢?消费者来消费,生产者暂停

那么就能使用信号量完成互斥与同步同时使用。
信号量:本质就是对公共资源的预定机制。

7.2、RingQueue模型实现

RingQueue

#pragma once

#include <iostream>
#include <string>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
#include <ctime>
#include <unistd.h>

template<typename T>
class RingQueue
{
private:
	void P(sem_t& s)
	{
		sem_wait(&s);
	}
	void V(sem_t& s)
	{
		sem_post(&s);
	}
public:
	RingQueue(int max_cap)
		:_max_cap(max_cap)
		,_ringqueue(max_cap)
		,_c_step(0)
		,_p_step(0)
	{
		//信号量初始化
		sem_init(&_data_sem, 0, 0);
		sem_init(&_space_sem, 0, max_cap);
		//互斥锁初始化
		pthread_mutex_init(&_c_mutex, nullptr);
		pthread_mutex_init(&_p_mutex, nullptr);
	}
	void Push(const T& in)//生产者
	{
		//?思考先申请信号量还是先抢锁 -- 先申请再抢锁,防止重复申请信号量的操作
		//形象的比喻:如果是先抢锁再申请,就是到电影院后再买票,先申请再抢锁,就是先预定资源直接排队拿锁即可
		//效率高,最根本的还是解决数据的处理效率

		//思考2,这里的申请信号量为何不想之前BlockingQueue阻塞队列那样进行判断条件满足再申请?
		//因为信号量本身就是判断条件,信号量本身就是描述内部资源的多少的,有则申请,没有就不会申请
		//所以:本质,信号量就是一个计数器,是资源的预定机制,预定:在外部,可以不判断资源是否满足,就可以知道内部资源的情况。

		//信号量实现同时同步与互斥---封装P/V操作,原子操作不会出错
		P(_space_sem);//P操作
		
		//多生产者抢生产锁
		pthread_mutex_lock(&_p_mutex);
		
		_ringqueue[_p_step] = in;
		_p_step++;
		_p_step %= _max_cap;
		V(_data_sem);//V操作
		pthread_mutex_unlock(&_p_mutex);
	}
	void Pop(T* out)//消费者
	{
		//?思考先申请信号量还是先抢锁 -- 先申请再抢锁,防止重复申请信号量的操作
		
		//P操作 -- 申请数据信号量
		P(_data_sem);
		
		//多消费者抢消费锁
		pthread_mutex_lock(&_c_mutex);

		*out = _ringqueue[_c_step];
		_c_step++;
		_c_step %= _max_cap;
		V(_space_sem);//V操作 
		pthread_mutex_unlock(&_c_mutex);
	}

	~RingQueue()
	{
		sem_destory(&_data_sem);
		sem_destory(&_space_sem);

		pthread_mutex_destory(&_c_mutex);
		pthread_mutex_destory(&_p_mutex);
	}
private:
	std::vector<T> _ringqueue;
	int _max_cap;
	//下标
	int _c_step;
	int _p_step;
	//信号量
	sem_t _data_sem;
	sem_t _space_sem;
	//加锁,生产互斥锁,消费互斥锁
	pthread_mutex_t _c_mutex;
	pthread_mutex_t _p_mutex;
};

main

#include "RingQueue.hpp"
#include "Task.hpp"

void* consumer(void* args)
{
	RingQueue<Task>* rq = static_cast<RingQueue<Task>*>(args);
	while (true)
	{
		Task t;
		//1.消费者消费
		rq->Pop(&t);
		//2.处理数据
		t();
		std::cout << "Consumer --> " << t.result() << std::endl;
	}
}

void* Productor(void* args)
{
	RingQueue<Task>* rq = static_cast<RingQueue<Task>*>(args);
	while (true)
	{
		//sleep(1);
		//1.生产者,构造数据
		int x = rand() % 10 + 1;//[1,10]
		usleep(x * 1000);
		int y = rand() % 10 + 1;
		Task t(x, y);
		//2.生产数据
		rq->Push(t);

		std::cout << "Productor --> " << t.debug() << std::endl;
	}
}

int main()
{
	srand((unsigned int)time(nullptr) ^ getpid());
	RingQueue<Task>* rq = new RingQueue<Task>(5);//访问同一资源
	//单生产,单消费
	pthread_t c, p;
	pthread_create(&c, nullptr, Consumer, rq);
	pthread_create(&p, nullptr, Productor, rq);

	pthread_join(c, nullptr);
	pthread_join(p, nullptr);
	return 0;
}

Task.hpp

#pragma once
#include<iostream>

// 要做加法
class Task
{
public:
	Task()
	{
	}
	Task(int x, int y) : _x(x), _y(y)
	{
	}
	void Excute()
	{
		_result = _x + _y;
	}
	void operator ()()
	{
		Excute();
	}
	std::string debug()
	{
		std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";
		return msg;
	}
	std::string result()
	{
		std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);
		return msg;
	}

private:
	int _x;
	int _y;
	int _result;
};

小结:
思考先申请信号量还是先抢锁 ?

答:先申请再抢锁,防止重复申请信号量的操作
形象的比喻:如果是先抢锁再申请,就是到电影院后再买票,先申请再抢锁,就是先预定资源直接排队拿锁即可,要效率高,最根本的还是解决数据的处理效率

思考,这里的申请信号量为何不想之前BlockingQueue阻塞队列那样进行判断再申请?

因为信号量本身就是判断条件,信号量本身就是描述内部资源的多少的,有则申请,没有就不会申请,
所以:本质,信号量就是一个计数器,是资源的预定机制,
预定:在外部,可以不判断资源是否满足,就可以知道内部资源的情况。

7.3、同步与互斥

同步:
信号量的使用确保了生产者和消费者之间的同步。当队列为空时,消费者线程会等待数据个数信号量变得可用;当队列满时,生产者线程会等待空白位置信号量变得可用。
互斥:
虽然环形队列本身并不直接提供互斥机制,但通常会在访问队列的关键代码段(即临界区)时加锁,以确保同一时间只有一个线程可以访问队列。

7.4、优点

1.高效:
环形队列的空间复用避免了频繁的内存分配和释放,提高了效率。
2.灵活:
通过信号量控制生产者和消费者之间的同步与互斥,使得模型更加灵活和可控。
3.可扩展:
可以根据实际需要调整环形队列的容量,以适应不同的生产消费场景。

综上所述,生产消费RingQueue模型是一种高效、灵活且可扩展的生产者消费者同步机制,在多线程或并发编程中具有广泛的应用前景。

  • 29
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值