【Linux】多线程 -> 线程同步与基于BlockingQueue的生产者消费者模型

线程同步

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如:一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

条件变量函数

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict

attr);

参数:

cond:要初始化的条件变量

attr:NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待

mutex:互斥量,后面详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒一批线程。

int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个线程。

示例代码:

makefile:

testCond:testCond.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f testCond

testCond.cc:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>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);// 判断暂时省略std::cout << name << "->" << tickets << std::endl;tickets--;pthread_mutex_unlock(&mutex);}
}int main()
{// 通过条件变量控制线程的执行pthread_t t[4];for (int i = 0; i < 4; i++){char *name = new char[64];snprintf(name, 64, "thread %d", i + 1);pthread_create(t + i, nullptr, start_routine, (void *)name);}while (true){sleep(1);//pthread_cond_broadcast(&cond); // 唤醒一批线程pthread_cond_signal(&cond);//唤醒一个线程std::cout << "main thread wakeup one thread... " << std::endl;}for (const auto &i : t){pthread_join(i, nullptr);}return 0;
}

pthread_cond_signal:唤醒一个线程。      pthread_cond_broadcast:唤醒一批线程。

这些线程会持续等待一个条件变量的信号。主线程每隔 1 秒就会发送一个条件变量信号,唤醒其中一个等待的线程。被唤醒的线程会输出当前剩余的票数并将票数减 1。

可以看到,由于条件变量的存在,输出结果变得有顺序性。

  • 为什么 pthread_cond_wait 需要互斥量?

1. 保证条件检查和等待操作的原子性

在多线程环境中,线程需要先检查某个条件是否满足,如果不满足则进入等待状态。这个检查条件和进入等待的操作必须是原子的,否则可能会出现竞态条件

例如,在生产者 - 消费者模型中,消费者线程需要检查缓冲区是否为空,如果为空则等待。假设没有互斥量保护,可能会出现以下情况:

  • 消费者线程检查到缓冲区为空,准备进入等待状态。

  • 在消费者线程真正进入等待状态之前,生产者线程往缓冲区中添加了数据,并发出了条件变量的通知。

  • 消费者线程此时才进入等待状态,由于之前通知已经发出,消费者线程可能会一直等待下去,导致程序出现错误。

使用互斥量可以保证条件检查和进入等待状态这两个操作的原子性。当线程调用pthread_cond_wait时,它会先释放互斥量,然后进入等待状态;当被唤醒时,又会重新获取互斥量。这样就避免了上述竞态条件的发生。

2. 保护共享资源和条件变量

条件变量通常与共享资源相关联,线程在检查条件和修改共享资源时需要保证线程安全。互斥量可以用来保护这些共享资源,确保同一时间只有一个线程能够访问和修改它们

在调用pthread_cond_wait之前,线程需要先获取互斥量,这样可以保证在检查条件和进入等待状态时,其他线程不会同时修改共享资源和条件变量。当线程被唤醒后,再次获取互斥量,又可以保证在处理共享资源时的线程安全

生产者消费者模型

  • 为何要使用生产者消费者模型?

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

“321”原则(便于记忆)

  • 3种关系:生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥、同步)。
  • 2种角色:生产者线程和消费者线程。
  • 1种交易场所:一段特定结构的缓冲区。

优点

  1. 解耦:生产线程和消费线程解耦。
  2. 支持忙闲不均:生产和消费的一段时间的忙闲不均。
  3. 提高效率:支持并发。

基于BlockingQueue的生产消费模型

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

下面我们以单生产者,单消费者为例:

makefile:

MainCp:MainCp.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f MainCp

BlockQueue.hpp:

#include <iostream>
#include <queue>
#include <pthread.h>const int gmaxcap = 5;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.判断while (is_full())// if(is_full())//细节2:充当判断的语法必须是while,不能是if,因为在被唤醒时,有可能存在异常或伪唤醒。{// 细节1:pthread_cond_wait是在临界区啊。// 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_siganl(&_ccond);}void pop(T *out){pthread_mutex_lock(&_mutex);// 1.判断while (is_empty())// if(is_empty()){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; // 消费者对应的条件变量
};

 MainCp.cc:

#include "BlockQueue.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>void *consumer(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data;bq->pop(&data);std::cout << "消费数据:" << data << std::endl;}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。bq->push(data);std::cout << "生产数据:" << data << std::endl;}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueue<int> *bq = 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);delete bq;return 0;
}

如果不加控制,生产消费就会疯狂的执行,没有顺序。

  • 你怎么证明它是一个阻塞队列呢?

让生产者每隔一秒生产一个,消费者一直消费。那么最终的预期结果就是生产一个,消费一个;生产一个,消费一个。

void *consumer(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data;bq->pop(&data);std::cout << "消费数据:" << data << std::endl;}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。bq->push(data);std::cout << "生产数据:" << data << std::endl;sleep(1);}return nullptr;
}

让消费者每隔一秒消费一个,生产者一直生产。那么最终的预期结果就是消费一个,生产一个;消费一个,生产一个。

void *consumer(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data;bq->pop(&data);std::cout << "消费数据:" << data << std::endl;sleep(1);}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);while (true){// 生产活动int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。bq->push(data);std::cout << "生产数据:" << data << std::endl;}return nullptr;
}

这就是基于阻塞队列的生产消费模型。

上面我们阻塞队列里放的就是一个整形数据,我们可以再完善一下。我们是可以直接在阻塞队列中放任务的。让生产者给消费者派发任务。

makefile:

MainCp:MainCp.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f MainCp

BlockQueue.hpp:

#include <iostream>
#include <queue>
#include <pthread.h>const int gmaxcap = 5;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.判断while (is_full())// if(is_full())//细节2:充当判断的语法必须是while,不能是if,因为在被唤醒时,有可能存在异常或伪唤醒。eg:一个生产者十个消费者,broadcast唤醒。{// 细节1:pthread_cond_wait是在临界区啊。// 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_siganl(&_ccond);}void pop(T *out){pthread_mutex_lock(&_mutex);// 1.判断while (is_empty())// if(is_empty()){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:

#pragma once#include <iostream>
#include <cstdio>
#include <functional>class Task
{using func_t = std::function<int(int, int, char)>;// typedef std::function<int(int,int,char)>func_t;
public:Task(){}Task(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;
};

MainCp.cc:

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>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;}return result;
}void *consumer(void *bq_)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);while (true){// 消费活动Task t;bq->pop(&t);std::cout << "消费任务:" << t() << std::endl;//sleep(1);}return nullptr;
}
void *productor(void *bq_)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);while (true){// 生产活动int x = rand() % 100 + 1; // 这里我们先用一个随机数构建一个数据。int y = rand() % 10;int operCode = rand() % oper.size();Task t(x, y, oper[operCode], mymath);bq->push(t);std::cout << "生产任务:" << t.toTaskString() << std::endl;sleep(1);}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueue<Task> *bq = new BlockQueue<Task>();pthread_t c, p;// 生产消费要看到同一份资源,也就是阻塞队列pthread_create(&c, nullptr, consumer, bq);pthread_create(&p, nullptr, productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);delete bq;return 0;
}

让生产者sleep1秒,看到的结果就是生产一个任务,消费一个任务。

让消费者sleep1秒,看到的结果就是消费一个任务,生产一个任务。

这样,我们就完成了一个线程给另一个线程派发任务:生产者给消费者派发任务。

  • 上面是单生产者,单消费者,如果我们直接改成多个生产者多个消费者可以吗?

MainCp.cc:

//
//...
//...
int main()
{srand((unsigned long)time(nullptr) ^ getpid());BlockQueue<Task> *bq = new BlockQueue<Task>();pthread_t c, c1, p, p1, p2;// 生产消费要看到同一份资源,也就是阻塞队列pthread_create(&p, nullptr, productor, bq);pthread_create(&p1, nullptr, productor, bq);pthread_create(&p2, nullptr, productor, bq);pthread_create(&c1, nullptr, consumer, bq);pthread_create(&c, nullptr, consumer, bq);pthread_join(c, nullptr);pthread_join(c1, nullptr);pthread_join(p, nullptr);pthread_join(p1, nullptr);pthread_join(p2, nullptr);delete bq;return 0;
}

可以看到是可以的。无论外部的线程再多,真正进入到阻塞队列里生产或消费的线程永远只有一个。

生产者要向blockqueue里放任务,消费者要向blockqueue里取任务。由于有锁的存在,这个(生产过程和消费过程)过程是串行的,也就是blockqueue里任何时刻只有一个执行流。那么:

  • 那么生产者消费者模型高效在哪里呢?创建多线程生产和消费的意义是什么呢?

1、对于生产者而言,它获取数据构建任务,是需要花时间的。

  • 如果这个任务的构建非常耗时,这个线程(生产者)在构建任务的同时,其他线程(生产者)可以并发的继续构建任务。

2、对于消费者而言,它拿到任务以后,是需要花时间处理这个任务的!

  • 如果这个任务的处理非常耗时,这个线程(消费者)在处理任务的同时,其他线程(消费者)可以并发的继续从阻塞队列里拿任务处理。

所以,高效并不体现在生产者把任务放进阻塞队列里高效,或者消费者从阻塞队列里拿任务高效。而是,体现在多个线程可以同时并发的构建或处理任务。

对于单生产单消费,它的并发性体现在,消费者从阻塞队列里拿任务和生产者构建任务,或者生产者往阻塞队列里放任务和消费者处理任务的过程是并发的。

总结:生产消费模型高效体现在,可以在生产前,和消费之后,让线程并行执行。

创建多线程生产和消费的意义:

多个线程可以并发生产,并发消费。

以上就是线程同步和基于阻塞队列的生产者消费者模型。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPF的页面设计和实用功能实现

目录 一、TextBlock和TextBox 1. 在TextBlock中实时显示当前时间 二、ListView 1.ListView显示数据 三、ComboBox 1. ComboBox和CheckBox组合实现下拉框多选 四、Button 1. 设计Button按钮的边框为圆角&#xff0c;并对指针悬停时的颜色进行设置 一、TextBlock和TextBox…

Ubuntu24.04LTS的下载安装超细图文教程(VMware虚拟机及正常安装)

&#x1f638;个人主页&#x1f449;&#xff1a;神兽汤姆猫 &#x1f4d6;系列专栏&#xff1a;开发语言环境配置 、 Java学习 、Java面试 、Markdown等 学习上的每一次进步&#xff0c;均来自于平时的努力与坚持。 &#x1f495;如果此篇文章对您有帮助的话&#xff0c;请点…

buu-get_started_3dsctf_2016-好久不见39

栈溢出外平栈 1外平栈与内平栈的区别 外平栈&#xff1a; 栈帧的局部变量和返回地址之间没有额外的对齐或填充。返回地址直接位于局部变量的上方&#xff08;即栈顶方向&#xff09;。在计算偏移时&#xff0c;不需要额外加 4&#xff08;因为返回地址紧邻局部变量&#xff09…

QML Component 与 Loader 结合动态加载组件

在实际项目中&#xff0c;有时候我们写好一个组件&#xff0c;但不是立即加载出来&#xff0c;而是触发某些条件后才动态的加载显示出来&#xff0c;当处理完某些操作后&#xff0c;再次将其关闭掉&#xff1b; 这样的需求&#xff0c;可以使用 Component 包裹着组件&#xff…

vim修改只读文件

现象 解决方案 对于有root权限的用户&#xff0c;在命令行输入 :wq! 即可强制保存退出

UML顺序图的建模方法及应用示例

《UML 2.5基础、建模与设计实践》(李波&#xff0c;姚丽丽&#xff0c;朱慧)【摘要 书评 试读】- 京东图书 顺序图是强调消息时间顺序的交互图&#xff0c;它描述了对象之间传送消息的时间顺序&#xff0c;用于表示用例中的行为顺序。顺序图将交互关系表示为一个二维图&#x…

docker 安装jenkins

使用docker 容器安装jenkins比较方便&#xff0c;但是细节比较重要&#xff0c;这里实战安装了一遍&#xff0c;可用&#xff1a; 拉取最新的jenkins镜像 docker pull jenkins/jenkins 如果没有翻墙的话&#xff0c;可以会有下面的报错&#xff1a; Error response from dae…

My Metronome for Mac v1.4.2 我的节拍器 支持M、Intel芯片

应用介绍 My Metronome 是一款适用于 macOS 的专业节拍器应用程序&#xff0c;旨在帮助音乐家、作曲家、学生和任何需要精确节奏控制的人进行练习。无论是进行乐器练习、音乐创作还是演出排练&#xff0c;My Metronome 都能为用户提供精准的节拍支持和灵活的功能&#xff0c;确…

第1章大型互联网公司的基础架构——1.12 多机房:主备机房

除了要考虑机房内的各个组件&#xff0c;也要考虑机房自身的高可用问题。使用单机房架构搭建互联网应用后台&#xff0c;虽然接入层、业务服务层、存储层均具备高可用架构&#xff0c;但由于机房是单点&#xff0c;所以还是避免不了机房故障会造成整个应用无法访问的问题。可能…

EasyRTC:基于WebRTC与P2P技术,开启智能硬件音视频交互的全新时代

在数字化浪潮的席卷下&#xff0c;智能硬件已成为我们日常生活的重要组成部分&#xff0c;从智能家居到智能穿戴&#xff0c;从工业物联网到远程协作&#xff0c;设备间的互联互通已成为不可或缺的趋势。然而&#xff0c;高效、低延迟且稳定的音视频交互一直是智能硬件领域亟待…

项目设置内网 IP 访问实现方案

在我们平常的开发工作中&#xff0c;项目开发、测试完成后进行部署上线。比如电商网站、新闻网站、社交网站等&#xff0c;通常对访问不会进行限制。但是像企业内部网站、内部管理系统等&#xff0c;这种系统一般都需要限制访问&#xff0c;比如内网才能访问等。那么一个网站应…

ProfiNet转EtherNet/IP罗克韦尔PLC与监控系统通讯案例

一、案例背景 在新能源产业蓬勃发展的当下&#xff0c;大型光伏电站作为绿色能源的重要输出地&#xff0c;其稳定高效的运行至关重要。某大型光伏电站占地面积广阔&#xff0c;内部设备众多&#xff0c;要保障电站的稳定运行&#xff0c;对站内各类设备进行集中监控与管理必不可…

C++STL——map和set

C教学总目录 map和set 1、set1.1、set简介1.2、set接口简介1.3、set的使用1.4、set其他接口的使用1.5、multiset 2、map2.1、map简介2.2、pair使用2.3、map接口使用2.4、multimap 1、set 1.1、set简介 如图&#xff1a;set是类模板&#xff0c;参数T表示存储的数据类型&#x…

【Research Proposal】基于提示词方法的智能体工具调用研究——研究问题

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;研究问题1. 如何优化提示词方法以提高智能体的工具调用能力&#xff1f;2. 如何解决提示词方法在多模态任务中的挑战&#xff1f;3. 如何通过提示词优化智能体…

PLC数据采集网关(三格电子)

产品概述 PLC转Modbus网关型号SG-PLC-Private&#xff08;PLC私有协议网关&#xff09;&#xff0c;是三格电子推出的工业级网关&#xff08;以下简称网关&#xff09;&#xff0c;主要用于在不需要对PLC编程的情况下将PLC数据映射到Modbus TCP(映射的方式符合PLC工程师使用习惯…

【HBase】HBaseJMX 接口监控信息实现钉钉告警

目录 一、JMX 简介 二、JMX监控信息钉钉告警实现 一、JMX 简介 官网&#xff1a;Apache HBase ™ Reference Guide JMX &#xff08;Java管理扩展&#xff09;提供了内置的工具&#xff0c;使您能够监视和管理Java VM。要启用远程系统的监视和管理&#xff0c;需要在启动Java…

Qt开发⑥Qt常用控件_下_多元素控件+容器类控件+布局管理器

目录 1. 多元素控件 1.1 ?Widget 和 ?View 之间的区别 1.2 List Widget 纵向列表 1.3 Table Widget 表格 1.4 Tree Widget 树形控件 2. 容器类控件 2.1 Group Box 分组框 2.2 Tab Widget 标签页控件 3. 布局管理器 3.1 垂直布局QVBoxLayout 3.2 水平布局QHBoxLayo…

科普mfc100.dll丢失怎么办?有没有简单的方法修复mfc100.dll文件

当电脑频繁弹窗提示“mfc100.dll丢失”或应用程序突然闪退时&#xff0c;这个看似普通的系统文件已成为影响用户体验的核心痛点。作为微软基础类库&#xff08;MFC&#xff09;的核心组件&#xff0c;mfc100.dll直接关联着Visual Studio 2010开发的大量软件运行命脉。从工业设计…

并行计算考前复习整理

并行计算考前复习整理 &#xff08;lwg老师会在最后一节课跟大家讲考点&#xff0c;考试考的东西不会在考点之外&#xff0c;这里面我整理的内容已经将考点全部囊括&#xff0c;最终100分&#xff09; 一、向量求和函数 C语言的串行化实现 CUDA的并行化实现 1、问题一&am…

Windows - 通过ssh打开带有图形界面的程序 - 一种通过计划任务的曲折实现方式

Windows(奇思妙想) - 通过ssh打开带有图形界面的程序 - 一种通过计划任务的曲折实现方式 前言 Windows启用OpenSSH客户端后就可以通过SSH的方式访问Windows了。但是通过SSH启动的程序&#xff1a; 无法显示图形界面会随着SSH进程的结束而结束 于是想到了一种通过执行“计划…