【Linux初阶】多线程3 | 线程同步,生产消费者模型(普通版、BlockingQueue版)

news2024/11/19 0:26:34

在这里插入图片描述

文章目录

  • ☀️一、线程同步
    • 🌻1.条件变量
    • 🌻2.同步概念与竞态条件
    • 🌻3.条件变量函数
    • 🌻4.条件变量使用规范
    • 🌻5.代码案例
  • ☀️二、生产者消费者模型
    • 🌻1.为何要使用生产者消费者模型
    • 🌻2.生产者消费者模型优点
    • 🌻3.生产消费的关系
  • ☀️三、基于BlockingQueue的生产者消费者模型
    • 🌻1.概念
    • 🌻2.代码示例
      • ⚡(1)生产者给消费者派发整形数据(简单版)
      • ⚡(2)生产者给消费者派发任务(复杂版)
    • 🌻3.探究:生产消费模型高校在哪里


☀️一、线程同步

🌻1.条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
  • 通过条件变量,我们可以实现线程同步,即可让线程顺序进行
  • 我们访问临界资源的模式一般是这样的:对临界资源加锁,判断(是否满足条件变量/生产消费条件),解锁

🌻2.同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  • 竞态条件:因为线程运行的时序问题,而导致程序异常,我们称之为竞态条件。

🌻3.条件变量函数

  • (1)初始化

动态设置

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
	cond:要初始化的条件变量
	attr:NULL

静态设置

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  //静态设置条件变量(不用初始化、销毁)
  • (2)销毁
int pthread_cond_destroy(pthread_cond_t *cond)
  • (3)等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
	cond:要在这个条件变量上等待
	mutex:互斥量,后面详细解释
  • 在新线程内部调用pthread_cond_wait,可让线程加入等待队列中。

  • (4)唤醒等待

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 在新线程内部调用pthread_cond_signal,就是把线程从等待队列中拿出来,放到CPU中运行。

🌻4.条件变量使用规范

  • 等待条件代码
	pthread_mutex_lock(&mutex);
	while (条件为假)
		pthread_cond_wait(cond, mutex);
	修改条件
	pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
	pthread_mutex_lock(&mutex);
	设置条件为真
	pthread_cond_signal(cond);
	pthread_mutex_unlock(&mutex);

🌻5.代码案例

  • makefile
testCond:testCond.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f testCond
  • testCond.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  //静态设置条件变量(不用初始化、销毁)

void* start_routine(void* args)
{
    std::string name = static_cast<const char*>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); //将新线程放入等待队列
        // wait函数参数为什么要有mutex?为了后续释放和再次获取mutex(锁)
        //线程进入阻塞队列时要释放锁,为了能让别的线程能访问该临界资源
        //线程被唤醒之后需要重新把锁申请回来
		
        //判断暂时省略
        std::cout << name << " -> " << tickets << std::endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    // 通过条件变量控制线程的执行
    pthread_t t[5];
    for (int i = 0; i < 5; i++)
    {
        char* name = new char[64];
        snprintf(name, 64, "thread %d", i + 1);
        pthread_create(t + i, nullptr, start_routine, name);
    }

    while (true)
    {
        sleep(1); //#include <unistd.h>
        // pthread_cond_signal(&cond);  //每次循环唤醒一个新线程
        pthread_cond_broadcast(&cond);  //唤醒一批线程(所有线程都会被唤醒)
        std::cout << "main thread wakeup one thread..." << std::endl;
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_join(t[i], nullptr);
    }

    return 0;
}
  • 运行结果

在这里插入图片描述


☀️二、生产者消费者模型

🌻1.为何要使用生产者消费者模型

  • 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
  • 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
  • 这个阻塞队列就是用来给生产者和消费者解耦的。
  • 阻塞队列(临时保存的数据场所)在缓冲区中。

🌻2.生产者消费者模型优点

  • 解耦(将生产和消费过程进行分离)
  • 支持并发
  • 支持忙闲不均
    在这里插入图片描述

🌻3.生产消费的关系

  • 生产者和生产者之间 - 互斥关系(竞争关系)。
  • 消费者和消费者之间 - 互斥关系。
  • 生产者和消费者之间 - 互斥 && 同步(生产消费需要访问同一份资源时为互斥,生产消费协同进行为同步)。

生产消费模型巧记 - 321原则:

  • 3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥(保证共享资源安全性) && 同步) 。
  • 2种角色:生产者线程,消费者线程。
  • 1个交易场所:一段特定结构的缓冲区。
  • 生产消费的产品就是数据。

☀️三、基于BlockingQueue的生产者消费者模型

🌻1.概念

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

在这里插入图片描述

🌻2.代码示例

⚡(1)生产者给消费者派发整形数据(简单版)

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

#define NUM 8

class BlockQueue {
private:
	std::queue<int> q;
	int cap;
	pthread_mutex_t lock;
	pthread_cond_t full;
	pthread_cond_t empty;

private:
	void LockQueue()
	{
		pthread_mutex_lock(&lock);
	}
	void UnLockQueue()
	{
		pthread_mutex_unlock(&lock);
	}
	void ProductWait()
	{
		pthread_cond_wait(&full, &lock);
	}
	void ConsumeWait()
	{
		pthread_cond_wait(&empty, &lock);
	}
	void NotifyProduct()
	{
		pthread_cond_signal(&full);
	}
	void NotifyConsume()
	{
		pthread_cond_signal(&empty);
	}
	bool IsEmpty()
	{
		return (q.size() == 0 ? true : false);
	}
	bool IsFull()
	{
		return (q.size() == cap ? true : false);
	}

public:
	BlockQueue(int _cap = NUM) :cap(_cap)
	{
		pthread_mutex_init(&lock, NULL);
		pthread_cond_init(&full, NULL);
		pthread_cond_init(&empty, NULL);
	}
	void PushData(const int& data)
	{
		LockQueue();
		while (IsFull()) {
			NotifyConsume();
			std::cout << "queue full, notify consume data, product stop." << std::endl;
			ProductWait();
		}
		q.push(data);
		// NotifyConsume();
		UnLockQueue();
	}
	void PopData(int& data)
	{
		LockQueue();
		while (IsEmpty()) {
			NotifyProduct();
			std::cout << "queue empty, notify product data, consume stop." << std::endl;
			ConsumeWait();
		}
		data = q.front();
		q.pop();
		// NotifyProduct();
		UnLockQueue();
	}
	~BlockQueue()
	{
		pthread_mutex_destroy(&lock);
		pthread_cond_destroy(&full);
		pthread_cond_destroy(&empty);
	}
};

void* consumer(void* arg)
{
	BlockQueue* bqp = (BlockQueue*)arg;
	int data;
	for (; ; ) {
		bqp->PopData(data);
		std::cout << "Consume data done : " << data << std::endl;
	}
}

//more faster
void* producter(void* arg)
{
	BlockQueue* bqp = (BlockQueue*)arg;
	srand((unsigned long)time(NULL));
	for (; ; ) {
		int data = rand() % 1024;
		bqp->PushData(data);
		std::cout << "Prodoct data done: " << data << std::endl;
		// sleep(1);
	}
}

int main()
{
	BlockQueue bq;
	pthread_t c, p;
	pthread_create(&c, NULL, consumer, (void*)&bq);
	pthread_create(&p, NULL, producter, (void*)&bq);

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

⚡(2)生产者给消费者派发任务(复杂版)

生产者派发任务(计算任务) -> 放入阻塞队列1 -> 消费处理任务 -> 放入阻塞队列2(将结果储存) -> 记录任务结果(保存在文件中)

  • BlockQueue.hpp
#include <iostream>
#include <queue>
#include <pthread.h>

const int gmaxcap = 500;

template <class T>
class BlockQueue
{
public:
    BlockQueue(const int& maxcap = gmaxcap) :_maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }
    
    void push(const T& in) // 输入型参数,const &
    {
        pthread_mutex_lock(&_mutex);
        // 1. 判断
        // 细节2: 充当条件判断的语法必须是while,不能用if
        while (is_full()) //bug?
        {
            // 细节1:pthread_cond_wait这个函数的第二个参数,必须是我们正在使用的互斥锁!
            // a. pthread_cond_wait: 该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起
            // b. pthread_cond_wait: 该函数在被唤醒返回的时候,会自动的重新获取你传入的锁
            pthread_cond_wait(&_pcond, &_mutex); //因为生产条件不满足,无法生产,此时我们的生产者进行等待
        }
        
        // 2. 走到这里一定是没有满
        _q.push(in);
        
        // 3. 绝对能保证,阻塞队列里面一定有数据
        // 细节3:pthread_cond_signal:这个函数,可以放在临界区内部,也可以放在外部
        pthread_cond_signal(&_ccond); // 这里可以有一定的策略
        pthread_mutex_unlock(&_mutex);
        //pthread_cond_signal(&_ccond); // 这里可以有一定的策略
    }
    
    void pop(T* out) // 输出型参数:*, // 输入输出型:&
    {
        pthread_mutex_lock(&_mutex);
        //1. 判断
        while (is_empty()) //bug?
        {
            pthread_cond_wait(&_ccond, &_mutex);
        }
        // 2. 走到这里我们能保证,一定不为空
        *out = _q.front();
        _q.pop();

        // 3. 绝对能保证,阻塞队列里面,至少有一个空的位置!
        pthread_cond_signal(&_pcond); // 这里可以有一定的策略
        pthread_mutex_unlock(&_mutex);
    }
    
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }
    
private:
    bool is_empty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _q.size() == _maxcap;
    }
    
private:
    std::queue<T> _q;
    int _maxcap; // 队列中元素的上限
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond; // 生产者对应的条件变量
    pthread_cond_t _ccond; // 消费者对应的条件变量
};
  • Task.hpp
#include <iostream>
#include <string>
#include <cstdio>
#include <functional>

class CalTask
{
    using func_t = std::function<int(int, int, char)>;
    // typedef std::function<int(int,int)> func_t;
public:
    CalTask()
    {}
    CalTask(int x, int y, char op, func_t func)
        :_x(x), _y(y), _op(op), _callback(func)
    {}
    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
        return buffer;
    }
private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

const std::string oper = "+-*/%";

int mymath(int x, int y, char op)
{
    int result = 0;
    switch (op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if (y == 0)
        {
            std::cerr << "div zero error!" << std::endl;
            result = -1;
        }
        else
            result = x / y;
    }
    break;
    case '%':
    {
        if (y == 0)
        {
            std::cerr << "mod zero error!" << std::endl;
            result = -1;
        }
        else
            result = x % y;
    }
    break;
    default:
        // do nothing
        break;
    }

    return result;
}

class SaveTask
{
    typedef std::function<void(const std::string&)> func_t;
public:
    SaveTask()
    {}
    SaveTask(const std::string& message, func_t func)
        : _message(message), _func(func)
    {}
    void operator()()
    {
        _func(_message);
    }
private:
    std::string _message;
    func_t _func;
};

void Save(const std::string& message)
{
    const std::string target = "./log.txt";
    FILE* fp = fopen(target.c_str(), "a+");
    if (!fp)
    {
        std::cerr << "fopen error" << std::endl;
        return;
    }
    fputs(message.c_str(), fp);
    fputs("\n", fp);
    fclose(fp);
}
  • MainCp.cc
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <sys/types.h>
#include <unistd.h>
#include <ctime>

//C:计算
//S: 存储
template<class C, class S>
class BlockQueues
{
public:
    BlockQueue<C> *c_bq;
    BlockQueue<S> *s_bq;
};

void *productor(void *bqs_)
{
    BlockQueue<CalTask> *bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->c_bq;
    // BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);
    while (true)
    {
        // sleep(3);
        // 生产活动,从数据库?从网络,从外设??拿来的用户数据!!
        int x = rand() % 100 + 1; // 在这里我们先用随机数,构建一个数据
        int y = rand() % 10;
        int operCode = rand() % oper.size();
        CalTask t(x, y, oper[operCode], mymath);


        bq->push(t);
        std::cout << "productor thread, 生产计算任务: " << t.toTaskString() << std::endl;
        
    }
    return nullptr;
}

void *consumer(void *bqs_)
{
    BlockQueue<CalTask> *bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->c_bq;
    BlockQueue<SaveTask> *save_bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->s_bq;

    while (true)
    {
        // 消费活动
        CalTask t;
        bq->pop(&t);

        std::string result = t(); // 任务非常耗时!!
        std::cout << "cal thread,完成计算任务: " << result << " ... done"<< std::endl;

        // SaveTask save(result, Save);
        // save_bq->push(save);
        // std::cout << "cal thread,推送存储任务完成..." << std::endl; 

        //sleep(1);
    }
    return nullptr;
}

void *saver(void *bqs_)
{
    BlockQueue<SaveTask> *save_bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(bqs_))->s_bq;

    while(true)
    {
        SaveTask t;
        save_bq->pop(&t);
        t();
        std::cout << "save thread,保存任务完成..." << std::endl; 
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    BlockQueues<CalTask, SaveTask> bqs;
    bqs.c_bq = new BlockQueue<CalTask>();
    bqs.s_bq = new BlockQueue<SaveTask>();

    pthread_t c[2], p[3], s;

    pthread_create(p, nullptr, productor, &bqs);
    pthread_create(p+1, nullptr, productor, &bqs);
    pthread_create(p+2, nullptr, productor, &bqs);
    pthread_create(c, nullptr, consumer, &bqs);
    pthread_create(c+1, nullptr, consumer, &bqs);

    pthread_create(&s, nullptr, saver, &bqs);	//saver - 保存在文件

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    
    pthread_join(s, nullptr);	//

    delete bqs.c_bq;
    delete bqs.s_bq;
    return 0;
}
  • makefile
MainCp:MainCp.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f MainCp
  • 运行结果
    在这里插入图片描述

🌻3.探究:生产消费模型高校在哪里

  • 首先我们要清楚,生产放入队列、消费拿出队列的动作是原子的。
  • 对于生产端,我们构建一个任务可能十分耗时间,构建完成之后,可以竞争式的放进队列。简单来说,就是每个线程之间的任务构造相互独立,不需要一个一个任务串行构造,可以并发式构造,只有放进队列时要一个一个放进去,节省了构造任务的时间。
  • 对于消费端,只有将任务取出队列时要一个一个取,而任务(算法等)可以并发式的实现,节省了任务实现的时间。
  • 总结:可以在生产之前,和消费之后,让线程并行执行。

🌹🌹 多线3 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1099411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代理正向 反向代理

1.正向代理 主动发送流量 端口转发 反向代理 被动发送流量 正向代理的设置 画图 实验 利用 攻击机 外网 失控服务器 内网 外网都有 内部服务内网 使用工具 使用的恶意脚本 放到网页里 客户端 使用 网站访问 解析 一下 使用的工具 pyth…

Java学习_day01_hello java

构成 JDK JDK是java开发者工具&#xff0c;由JRE和一些开发工具组成。JRE JRE是java运行环境&#xff0c;由JVM和java核心类库组成。JVM JVM是java虚拟机&#xff0c;主要用来运行字节码。 执行过程 由IDE或文本编辑器&#xff0c;编写源代码&#xff0c;并将文件保存为*.ja…

谈谈 AOF

谈谈 AOF Append Only File&#xff0c;只追加文件。 AOF 文件存储的是具体的操作命令。 Redis 每执行一条写操作命令&#xff0c;执行完之后&#xff0c;就把该命令追加到 AOF_Buffer 缓冲区中&#xff0c;然后会使用某种写回策略&#xff0c;写回磁盘的AOF文件中。 Redis 重启…

【GD32】GD32F303串口设置DMA发生中断无法进入中断函数

在GD32F303官方提供的串口例程中&#xff0c;有一个DMA发生和接收中断例程&#xff0c;在模仿着写的过程中&#xff0c;能够正常发送数据&#xff0c;但是无法进入中断函数。DMA0_Channel3_IRQHandler函数时官方定义的弱函数&#xff0c;需要自己重新实现。如果开启了DMA0通道3…

ESP32单片机环境搭建(VScode + PlatformIO IDE)

一、环境搭建&#xff08;VScode PlatformIO IDE&#xff09; 1、官网下载VScode; 2、安装最新的插件&#xff08;C/C、PlatformIO IDE、python、Chinese&#xff09;&#xff1b; 3、在PlatformIO IDE中新建工程&#xff1a;Platforms——Projects——Create New Project——…

微信小程序-3

一、交互 API - - - 界面 - - - 交互 功能&#xff1a;提示 是否删除 1.wx.showToast 显示消息提示框 <button type"primary" bindtapclickBtn>按钮</button> <input style"margin: 20rpx;height: 60rpx;background: gainsboro;" type&…

百度文心一言 VS GPT

更多精华&#xff1a;即兴小索奇 | Link3 相信大家都关注AI&#xff0c;AI大模型已成为了科技领域的新焦点&#xff0c;各大科技巨头都争相推出自家的版本。其中&#xff0c;尤为引人注目的是中国科技巨头百度所推出的文心大模型。然而&#xff0c;即使在这激烈的竞争中&#x…

【数据结构与算法】字符串匹配,BF算法和KMP算法,next数组求法

朴素的模式匹配算法 bf算法 假设在主串S"helloworld"中找T"hellr"这个子串的位置 实现的思路如下 第一轮&#xff1a;子串中的第一个字符和主串中的第一个字符进行比较 如果相等&#xff0c;继续比较主串和子串中的第二个字符如果不相等&#xff0c;进行…

解决vue3 + vite + ts 中require失效的问题(require is not defind)

require is not defind因为require是属于webpack的方法&#xff0c;vite中找不到这个方法肯定报错 解决办法 通过vite官网了解到新的引入方式&#xff0c;我使用了其中一种 imgList: [{name: "lj",src: new URL(../../assets/img/applyList.png, import.meta.url).…

大数据 DataX 数据同步数据分析入门

目录 一、DataX 概览 1.1 DataX 是什么 1.2 DataX 3.0 概览 设计理念 当前使用现状 二、DataX 详解 2.1 DataX 3.0 框架设计 2.2 DataX 3.0 插件体系 2.3 DataX 3.0 核心架构 2.3.1 核心模块介绍 2.3.2 DataX 调度流程 2.4 DataX 3.0 的六大核心优势 2.4.1 可靠的…

为Mkdocs添加在线聊天(Tidio为例)

以Tidio为例,Tidio免费版已经完全够用且无需梯子 访问Tidio官网 要在您的网站上使用 javascript 代码方法安装 Tidio&#xff0c;您需要创建一个 Tidio 帐户。要创建 Tidio 帐户&#xff0c;请访问我们的网站<www.tidio.com>&#xff0c;然后单击 “开始” 按钮创建新的…

代码随想录算法训练营第23期day24|回溯算法理论基础、77. 组合

目录 一、回溯算法基础 回溯法模板 二、&#xff08;leetcode 77&#xff09;组合 剪枝 一、回溯算法基础 1.回溯的本质是穷举&#xff0c;穷举所有可能&#xff0c;然后选出想要的答案&#xff08;为了提升效率&#xff0c;最多再加一个剪枝&#xff09; 2.回溯法解决的…

第五章 运输层 | 计算机网络(谢希仁 第八版)

文章目录 第五章 运输层5.1 运输层协议概述5.1.1 进程之间的通信5.1.2 运输层的两个主要协议5.1.3 运输层的端口 5.2 用户数据报协议UDP5.2.1 UDP概述5.2.2 UDP的首部格式 5.3 传输控制协议TCP概述5.3.1 TCP最主要的特点5.3.2 TCP的连接 5.4 可靠传输的工作原理5.4.1 停止等待协…

SpringMVC源码分析(三)HandlerExceptionResolver启动和异常处理源码分析

问题&#xff1a;异常处理器在SpringMVC中是如何进行初始化以及使用的&#xff1f; Spring MVC提供处理异常的方式主要分为两种&#xff1a; 1、实现HandlerExceptionResolver方式&#xff08;HandlerExceptionResolver是一个接口&#xff0c;在SpringMVC有一些默认的实现也可以…

(ubuntu) 安装JDK

文章目录 前言参看java版本的命令&#xff1a;安装jdk命令安装jps关闭防火墙&#xff1a;查看端口占用&#xff1a;&#xff08;坑&#xff09;ubuntu上Mysql默认标明 区分大小写 前言 提示&#xff1a;常以为人是一个容器&#xff0c;盛着快乐&#xff0c;盛着悲哀。但是人不…

JUC并发编程——线程池学习:基础概念及三大方法、七大参数、四大拒绝策略(基于狂神说的学习笔记)

线程池 池化技术的本质&#xff1a;事先准备好一些资源&#xff0c;线程复用&#xff0c;用完即还&#xff0c;方便管理 默认大小&#xff1a;2 最大并发数max 根据电脑去设置&#xff0c;CPU密集型&#xff0c;IO密集型 线程池的好处&#xff1a; 降低资源的消耗提高响应的…

【马蹄集】—— 概率论专题

概率论专题 目录 MT2226 抽奖概率MT2227 饿饿&#xff01;饭饭&#xff01;MT2228 甜甜花的研究MT2229 赌石MT2230 square MT2226 抽奖概率 难度&#xff1a;黄金    时间限制&#xff1a;1秒    占用内存&#xff1a;128M 题目描述 小码哥正在进行抽奖&#xff0c;箱子里有…

双目项目实战---测距(获取三维坐标和深度信息)

目录 1.简介 2.模块讲解 2.1 立体校正 2.1.1 校正目的 2.1.2 校正方法 2.2 立体匹配和视差计算 2.3 深度计算 3.完整代码 1.简介 双目视觉是一种通过两个摄像机&#xff08;或者两个镜头&#xff09;同时拍摄到同一个场景&#xff0c;再通过计算机算法来获取该场景深度…

C++ - 智能指针的定制删除器

前言 在上一篇C 文章当中&#xff0c;对 智能指针的使用&#xff0c;做了很详细的介绍&#xff0c;对 C11 和 C98 库当中实现的一些常用智能指针做了很详细的介绍&#xff0c;但是智能指针的使用还有一些拓展用法。上篇文章链接&#xff1a;C - 智能指针 - auto_ptr - unique_…

Stm32_标准库_16_串口蓝牙模块_手机与蓝牙模块通信_手机传入信息能对芯片时间日期进行更改

实现了手机发送信息给蓝牙模块&#xff0c;程序对数据进行分析拆解&#xff0c;并更新自身数据 main.c: #include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Serial.h" #include "Ti…