当前位置: 首页 > news >正文

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

在这里插入图片描述

文章目录

  • ☀️一、线程同步
    • 🌻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.ccg++ -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 8class 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,不能用ifwhile (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;}elseresult = x / y;}break;case '%':{if (y == 0){std::cerr << "mod zero error!" << std::endl;result = -1;}elseresult = x % y;}break;default:// do nothingbreak;}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.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f MainCp
  • 运行结果
    在这里插入图片描述

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

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

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

相关文章:

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

文章目录 ☀️一、线程同步&#x1f33b;1.条件变量&#x1f33b;2.同步概念与竞态条件&#x1f33b;3.条件变量函数&#x1f33b;4.条件变量使用规范&#x1f33b;5.代码案例 ☀️二、生产者消费者模型&#x1f33b;1.为何要使用生产者消费者模型&#x1f33b;2.生产者消费者模…...

JUC并发编程——四大函数式接口(基于狂神说的学习笔记)

四大函数式接口 函数式接口&#xff1a;只有一个方法的接口 &#xff0c;例如&#xff1a;Runnable接口 Function 函数型接口&#xff0c;有一个输入参数&#xff0c;有一个输出 源码&#xff1a; /*** Represents a function that accepts one argument and produces a resul…...

【2】c++11新特性(稳定性和兼容性)—>超长整型 long long

c11标准要求long long整型可以在不同的平台上有不同的长度&#xff0c;但是至少64位&#xff0c;long long整型有两种&#xff1a; 有符号long long&#xff1a;–对应类型的数值可以使用LL或者ll后缀 long long num1 123456789LL; long long num2 123456789ll;无符号unsign…...

AI算法检测对无人军用车辆的MitM攻击

南澳大利亚大学和查尔斯特大学的教授开发了一种算法来检测和拦截对无人军事机器人的中间人&#xff08;MitM&#xff09;攻击。 MitM 攻击是一种网络攻击&#xff0c;其中两方&#xff08;在本例中为机器人及其合法控制器&#xff09;之间的数据流量被拦截&#xff0c;以窃听或…...

运维 | 如何在 Linux 系统中删除软链接 | Linux

运维 | 如何在 Linux 系统中删除软链接 | Linux 介绍 在 Linux 中&#xff0c;符号链接&#xff08;symbolic link&#xff0c;或者symlink&#xff09;也称为软链接&#xff0c;是一种特殊类型的文件&#xff0c;用作指向另一个文件的快捷方式。 使用方法 我们可以使用 ln…...

Jmeter接口测试:jmeter导入和导出接口的处理

JMeter测试导入接口 利用Jmeter测试上传文件&#xff0c;首先可根据接口文档或者fiddler抓包分析文件上传的接口&#xff1b;如下图&#xff1a; 以下是我通过fiddler所截取的文件上传的接口 1、填写导入接口的信息 查看文件上传栏下的填写信息&#xff1a; 文件名称&#x…...

一文了解 Go fmt 标准库的常用占位符及其简单使用

今天分享的内容是 Go fmt 标准库的常用占位符及其简单使用。如果本文对你有帮助&#xff0c;不妨点个赞&#xff0c;如果你是 Go 语言初学者&#xff0c;不妨点个关注&#xff0c;一起成长一起进步&#xff0c;如果本文有错误的地方&#xff0c;欢迎指出 占位符 通过占位符&a…...

Linux命令(94)之history

linux命令之history 1.history介绍 linux命令history会记录并显示用户所执行过的所有命令&#xff0c;也可以对其命令进行修改和删除操作。 2.history用法 history [参数] history参数 参数说明-a将当前会话的历史信息追加到历史文件(.bash_history)中-c删除所有条目从而清…...

Prompt 驱动架构设计:探索复杂 AIGC 应用的设计之道?

你是否曾经想过&#xff0c;当你在 Intellij IDEA 中输入一个段代码时&#xff0c;GitHub 是如何给你返回相关的结果的&#xff1f;其实&#xff0c;这背后的秘密就是围绕 Prompt 生成而构建的架构设计。 Prompt 是一个输入的文本段落或短语&#xff0c;用于引导 AI 生成模型执…...

【代码随想录】算法训练营 第三天 第二章 链表 Part 1

目录 链表基础 链表的定义 203. 移除链表元素 题目 思路 代码 直接删除法 虚拟头结点辅助法 707. 设计链表 题目 思路 代码 206. 反转链表 题目 思路 代码 双指针法 递归法 链表基础 链表是一种通过指针串在一起的线性结构&#xff0c;每个节点都由数据域和指…...

winform开发经验(1)——调用Invoke更新UI时程序卡死原因以及解决办法

1、问题代码如下: private void Form1_Load(object sender, EventArgs e){this.Invoke(new Action(()...

JNI 的数据类型以及和Java层之间的数据转换

JNI的数据类型和类型签名 数据类型 JNI的数据类型包含两种&#xff1a;基本类型和引用类型。 基本类型主要有jboolean、jchar、jint等&#xff0c;它们和Java中的数据类型的对应关系如下表所示。 JNI中的引用类型主要有类、对象和数组&#xff0c;它们和Java中的引用类型的对…...

EFLK与logstash过滤

目录 一、Filebeat工作原理&#xff1a; 二、为什么要使用Filebeat&#xff1a; 三、Filebeat和Logstash的区别&#xff1a; 四、logstash 的过滤插件&#xff1a; 五、FilebeatELK 部署&#xff1a; 1. 安装filebeat&#xff1a; 2. 设置 filebeat 的主配置文件&#xff1…...

docker jenkins

mkdir jenkins_home chown -R 1000:1000 /root/jenkins_home/docker run -d --name myjenkins -v /root/jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restarton-failure jenkins/jenkins:lts-jdk17参考 Official Jenkins Docker imageDocker 搭建 Jenkins …...

单例模式之「双重校验锁」

单例模式之「双重校验锁」 单例模式 单例即单实例&#xff0c;只实例出来一个对象。一般在创建一些管理器类、工具类的时候&#xff0c;需要用到单例模式&#xff0c;比如JDBCUtil 类&#xff0c;我们只需要一个实例即可&#xff08;多个实例也可以实现功能&#xff0c;但是增…...

2023年中国商业版服务器操作系统市场发展规模分析:未来将保持稳定增长[图]

服务器操作系统一般指的是安装在大型计算机上的操作系统&#xff0c;比如Web服务器、应用服务器和数据库服务器等&#xff0c;是企业IT系统的基础架构平台&#xff0c;也是按应用领域划分的三类操作系统之一。同时服务器操作系统也可以安装在个人电脑上。 服务器操作系统分类 …...

BIM如何通过3D开发工具HOOPS实现WEB轻量化?

随着建筑行业的数字化转型和信息建模技术的不断发展&#xff0c;建筑信息模型&#xff08;BIM&#xff09;已经成为设计、建造和管理建筑项目的标准。然而&#xff0c;BIM模型通常包含大量的数据&#xff0c;导致在Web上的传输和查看效率低下。为了解决这一挑战&#xff0c;HOO…...

Unity 3D基础——通过四元数控制对象旋转

在这个例子中&#xff0c;通过键盘的左右方向来控制场景中的球体 Sphere 的横向运动&#xff0c;而 Cube 立方体则会一直朝着球体旋转。 1.在场景中新建一个 Cube 立方体和一个 Sphere 球体&#xff0c;在 Inspector 视图中设置 Cube 立方体的坐标为&#xff08;3&#xff0c;0…...

python--短路运算,把0、空字符串和None看成 False,其他数值和非空字符串都看成 True

代码 print(3 and 4 and 5) # 5 print(5 and 6 or 7) # 6 4 > 3 and print(‘hello world’) # 输出hello world 注释&#xff1a; 在逻辑运算中&#xff0c;不一定逻辑运算符的两边都是纯表达式。也可以是数值类型的数据。 Python把0、空字符串和None看成 False&#xff…...

《算法通关村第一关——链表青铜挑战笔记》

《算法通关村第一关——链表青铜挑战笔记》 Java如何构造出链表 概念 如何构造出链表&#xff0c;首先必须了解什么是链表&#xff01; 单向链表就像一个铁链一样&#xff0c;元素之间相互链接&#xff0c;包含多个节点&#xff0c;每个节点有一个指向后继元素的next指针。…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查

在对接支付宝API的时候&#xff0c;遇到了一些问题&#xff0c;记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...

Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?

Golang 面试经典题&#xff1a;map 的 key 可以是什么类型&#xff1f;哪些不可以&#xff1f; 在 Golang 的面试中&#xff0c;map 类型的使用是一个常见的考点&#xff0c;其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

Docker 运行 Kafka 带 SASL 认证教程

Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明&#xff1a;server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

2024年赣州旅游投资集团社会招聘笔试真

2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

让AI看见世界:MCP协议与服务器的工作原理

让AI看见世界&#xff1a;MCP协议与服务器的工作原理 MCP&#xff08;Model Context Protocol&#xff09;是一种创新的通信协议&#xff0c;旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天&#xff0c;MCP正成为连接AI与现实世界的重要桥梁。…...

什么是Ansible Jinja2

理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具&#xff0c;可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板&#xff0c;允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板&#xff0c;并通…...

aardio 自动识别验证码输入

技术尝试 上周在发学习日志时有网友提议“在网页上识别验证码”&#xff0c;于是尝试整合图像识别与网页自动化技术&#xff0c;完成了这套模拟登录流程。核心思路是&#xff1a;截图验证码→OCR识别→自动填充表单→提交并验证结果。 代码在这里 import soImage; import we…...

数据分析六部曲?

引言 上一章我们说到了数据分析六部曲&#xff0c;何谓六部曲呢&#xff1f; 其实啊&#xff0c;数据分析没那么难&#xff0c;只要掌握了下面这六个步骤&#xff0c;也就是数据分析六部曲&#xff0c;就算你是个啥都不懂的小白&#xff0c;也能慢慢上手做数据分析啦。 第一…...