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

12.线程同步和生产消费模型

一.上集回顾

建议先学上篇博客,再向下学习,上篇博客的链接如下:

https://blog.csdn.net/weixin_60668256/article/details/154576356?fromshare=blogdetail&sharetype=blogdetail&sharerId=154576356&sharerefer=PC&sharesource=weixin_60668256&sharefrom=from_link

二.条件变量的简单demo

1.上集回顾

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>int ticket = 0;
pthread_mutex_t mutex;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *route(void *arg)
{char *id = (char*)arg;while (1) {pthread_mutex_lock(&mutex);if (ticket > 0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);} else {printf("%s wait on cond!\n",id);pthread_cond_wait(&cond,&mutex);//醒来的时候,会重新申请锁!!printf("%s 被叫醒了\n",id);}pthread_mutex_unlock(&mutex);}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex, NULL);pthread_create(&t1, NULL, route, (void*)"thread_1");pthread_create(&t2, NULL, route, (void*)"thread_2");pthread_create(&t3, NULL, route, (void*)"thread_3");pthread_create(&t4, NULL, route, (void*)"thread_4");int cnt = 10;while(true){sleep(5);ticket += cnt;printf("主线程放票咯! ticket: %d\n",ticket);pthread_cond_signal(&cond);}pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);return 0;
}

不是只有一个线程能拿到对应的锁吗?为啥都会在条件变量下等呢?

2.简单demo

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string>
#include <pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void* active(void* arg)
{std::string name = static_cast<const char*>(arg);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);printf("%s isactive!\n",name.c_str());pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t tid1,tid2,tid3;pthread_create(&tid1,nullptr,active,(void*)"thread-1");pthread_create(&tid2,nullptr,active,(void*)"thread-2");pthread_create(&tid3,nullptr,active,(void*)"thread-3");sleep(1);printf("Main thread ctrl begin...\n");while(true){printf("main wakup thread...\n");pthread_cond_signal(&cond);sleep(1);}pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);return 0;
}

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string>
#include <pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void* active(void* arg)
{std::string name = static_cast<const char*>(arg);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);printf("%s isactive!\n",name.c_str());pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t tid1,tid2,tid3;pthread_create(&tid1,nullptr,active,(void*)"thread-1");pthread_create(&tid2,nullptr,active,(void*)"thread-2");pthread_create(&tid3,nullptr,active,(void*)"thread-3");sleep(1);printf("Main thread ctrl begin...\n");while(true){printf("main wakup thread...\n");pthread_cond_broadcast(&cond);sleep(1);}pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);return 0;
}

三.生产者消费者模型

四.基于BlockingQueue的生产者消费者模型

1.单生成,单消费代码实现

a.main函数的实现

#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>using namespace BlockQueueModule;void* Consumer(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){int data;//1.从bq拿到数据bq->Pop(&data);//2.做处理sleep(1);printf("Consumer, get a data: %d\n",data);}
}void* Productor(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){//1.从外部获取数据int data = 10;//2.生产到bq中bq->Equeue(data);}
}int main()
{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;
}

b.blockqueue的大致框架

#pragma once#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>namespace BlockQueueModule
{template<class T>class BlockQueue{public:BlockQueue(){}void Equeue(const T& in){}void Pop(T* out){}~BlockQueue(){}private:std::queue<T> _q;              //保存数据的容器int _cap;                      //bq最大容量pthread_mutex_t _mutex;        //互斥pthread_cond_t _productor_cond;//生产者的条件变量pthread_cond_t _consumer_cond; //消费者的条件变量};
}

所以的线程互斥,线程同步的机制,都在我们对应的Equeue和Pop里面进行完成

c.BlockQueue函数的实现

BlockQueue(int cap = gcap):_cap(cap){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_productor_cond,nullptr);pthread_cond_init(&_consumer_cond,nullptr);}

d.~BlockQueue函数的实现

~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_productor_cond);pthread_cond_destroy(&_consumer_cond);}

e.其他函数的实现

bool IsFull(){return _q.size() == _cap;}bool IsEmpty(){return _q.empty();}

f.Equeue函数的实现

void Equeue(const T& in){pthread_mutex_lock(&_mutex);//想放数据也是要有条件的//1.在临界区中等待是必然的!if(IsFull()){//2.等待的时候,会释放_mutexpthread_cond_wait(&_productor_cond,&_mutex);//wait的时候,必定是持有锁的//3.返回,线程被唤醒 && 重新申请并持有锁(它会在临界区内醒来)}//4.到这里,我们一定可以进行生产了_q.push(in);pthread_mutex_unlock(&_mutex);}

g.Pop函数的实现

void Pop(T* out){pthread_mutex_lock(&_mutex);if(IsEmpty()){pthread_cond_wait(&_consumer_cond,&_mutex);}//到这里我们必定有数据*out = _q.fornt();_q.pop();pthread_mutex_unlock(&_mutex);}

这个Pop函数和上述的Equeue函数差不多

2.代码debug

上述的Equeue和我们的Pop有没有那里不对呢?

上述代码全部都在进行休眠,但是谁来将他们唤醒呢?

当没有资源时,消费者在进行等待,那么谁只有会有资源呢?(答案是生产者,所以当我们进行生产的时候,就将等待的消费者进行唤醒(前提是要有消费者),生产者等待时也同理)

所有,我们可以加上两个成员函数,来统计我们的生产者和消费者等待的个数

int _cwait_num;
int _pwait_num;
BlockQueue(int cap = gcap):_cap(cap),_cwait_num(0),_pwait_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_productor_cond,nullptr);pthread_cond_init(&_consumer_cond,nullptr);}

a.Equeue代码更新

void Equeue(const T& in){pthread_mutex_lock(&_mutex);//想放数据也是要有条件的//1.在临界区中等待是必然的!if(IsFull()){std::cout << "生产者进入等待..." << std::endl;//2.等待的时候,会释放_mutex_pwait_num++;pthread_cond_wait(&_productor_cond,&_mutex);//wait的时候,必定是持有锁的_pwait_num--;//3.返回,线程被唤醒 && 重新申请并持有锁(它会在临界区内醒来)std::cout << "生产者被唤醒..." << std::endl;}//4.到这里,我们一定可以进行生产了_q.push(in);//一定有数据if(_cwait_num){pthread_cond_signal(&_consumer_cond);}pthread_mutex_unlock(&_mutex);}

b.Pop代码更新

void Pop(T* out){pthread_mutex_lock(&_mutex);if(IsEmpty()){std::cout << "消费者进入等待..." << std::endl;_cwait_num++;pthread_cond_wait(&_consumer_cond,&_mutex);_cwait_num--;std::cout << "消费者被唤醒..." << std::endl;}//到这里我们必定有数据*out = _q.fornt();_q.pop();//一定有空间if(_pwait_num){pthread_cond_signal(&_productor_cond);}pthread_mutex_unlock(&_mutex);}

c.生产者无限生产,消费者慢一点进行消费

#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>using namespace BlockQueueModule;void* Consumer(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){sleep(2);int data;//1.从bq拿到数据bq->Pop(&data);//2.做处理printf("Consumer, 消费了一个数据: %d\n",data);}
}void* Productor(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);int data = 10;while(true){//1.从外部获取数据//2.生产到bq中bq->Equeue(data);printf("productor 生产了一个数据: %d\n",data);data++;}
}int main()
{BlockQueue<int>* bq = new BlockQueue<int>(5);//共享资源 -> 临界资源//单生产,单消费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;
}

消费的是历史数据,生产的是新的数据

d.消费者先进行消费,然后生产者再进行生产

#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>using namespace BlockQueueModule;void* Consumer(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){int data;//1.从bq拿到数据bq->Pop(&data);//2.做处理printf("Consumer, 消费了一个数据: %d\n",data);}
}void* Productor(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);int data = 10;while(true){sleep(2);//1.从外部获取数据//2.生产到bq中bq->Equeue(data);printf("productor 生产了一个数据: %d\n",data);data++;}
}int main()
{BlockQueue<int>* bq = new BlockQueue<int>(5);//共享资源 -> 临界资源//单生产,单消费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;
}

在解锁之前和解锁之后等,都是可以的

单生成单消费这里,我们是不会出现啥问题的

五.多生产,多消费

1.代码整体修改

"BlockQueue.hpp"#pragma once#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>namespace BlockQueueModule
{static const int gcap = 10;template<class T>class BlockQueue{private:bool IsFull(){return _q.size() == _cap;}bool IsEmpty(){return _q.empty();}public:BlockQueue(int cap = gcap):_cap(cap),_cwait_num(0),_pwait_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_productor_cond,nullptr);pthread_cond_init(&_consumer_cond,nullptr);}void Equeue(const T& in){pthread_mutex_lock(&_mutex);//想放数据也是要有条件的//1.在临界区中等待是必然的!if(IsFull()){std::cout << "生产者进入等待..." << std::endl;//2.等待的时候,会释放_mutex_pwait_num++;pthread_cond_wait(&_productor_cond,&_mutex);//wait的时候,必定是持有锁的_pwait_num--;//3.返回,线程被唤醒 && 重新申请并持有锁(它会在临界区内醒来)std::cout << "生产者被唤醒..." << std::endl;}//4.到这里,我们一定可以进行生产了_q.push(in);//一定有数据if(_cwait_num){pthread_cond_signal(&_consumer_cond);}pthread_mutex_unlock(&_mutex);}void Pop(T* out){pthread_mutex_lock(&_mutex);if(IsEmpty()){std::cout << "消费者进入等待..." << std::endl;_cwait_num++;pthread_cond_wait(&_consumer_cond,&_mutex);_cwait_num--;std::cout << "消费者被唤醒..." << std::endl;}//到这里我们必定有数据*out = _q.front();_q.pop();//一定有空间if(_pwait_num){pthread_cond_signal(&_productor_cond);}pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_productor_cond);pthread_cond_destroy(&_consumer_cond);}private:std::queue<T> _q;              //保存数据的容器int _cap;                      //bq最大容量pthread_mutex_t _mutex;        //互斥pthread_cond_t _productor_cond;//生产者的条件变量pthread_cond_t _consumer_cond; //消费者的条件变量int _cwait_num;int _pwait_num;};
}

这里如果是单生成单消费,那么就没有问题,但是如果是我们对应的多生产和多消费,那么这样就行不通了,因为我们唤醒,是要将所有的代码进行唤醒,而不是只进行唤醒一个线程

#pragma once#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>namespace BlockQueueModule
{static const int gcap = 10;template<class T>class BlockQueue{private:bool IsFull(){return _q.size() == _cap;}bool IsEmpty(){return _q.empty();}public:BlockQueue(int cap = gcap):_cap(cap),_cwait_num(0),_pwait_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_productor_cond,nullptr);pthread_cond_init(&_consumer_cond,nullptr);}void Equeue(const T& in){pthread_mutex_lock(&_mutex);//想放数据也是要有条件的//1.在临界区中等待是必然的!while(IsFull()){std::cout << "生产者进入等待..." << std::endl;//2.等待的时候,会释放_mutex_pwait_num++;pthread_cond_wait(&_productor_cond,&_mutex);//wait的时候,必定是持有锁的_pwait_num--;//3.返回,线程被唤醒 && 重新申请并持有锁(它会在临界区内醒来)std::cout << "生产者被唤醒..." << std::endl;}//4.到这里,我们一定可以进行生产了_q.push(in);//一定有数据if(_cwait_num){pthread_cond_signal(&_consumer_cond);}pthread_mutex_unlock(&_mutex);}void Pop(T* out){pthread_mutex_lock(&_mutex);while(IsEmpty()){std::cout << "消费者进入等待..." << std::endl;_cwait_num++;pthread_cond_wait(&_consumer_cond,&_mutex);//伪唤醒_cwait_num--;std::cout << "消费者被唤醒..." << std::endl;}//到这里我们必定有数据*out = _q.front();_q.pop();//一定有空间if(_pwait_num){pthread_cond_signal(&_productor_cond);}pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_productor_cond);pthread_cond_destroy(&_consumer_cond);}private:std::queue<T> _q;              //保存数据的容器int _cap;                      //bq最大容量pthread_mutex_t _mutex;        //互斥pthread_cond_t _productor_cond;//生产者的条件变量pthread_cond_t _consumer_cond; //消费者的条件变量int _cwait_num;int _pwait_num;};
}

#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>using namespace BlockQueueModule;void* Consumer(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){int data;//1.从bq拿到数据bq->Pop(&data);//2.做处理printf("Consumer, 消费了一个数据: %d\n",data);}
}void* Productor(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);int data = 10;while(true){sleep(2);//1.从外部获取数据//2.生产到bq中bq->Equeue(data);printf("productor 生产了一个数据: %d\n",data);data++;}
}int main()
{BlockQueue<int>* bq = new BlockQueue<int>(5);//共享资源 -> 临界资源//单生产,单消费pthread_t c1,c2,p1,p2,p3;pthread_create(&c1,nullptr,Consumer,bq);pthread_create(&c2,nullptr,Consumer,bq);pthread_create(&p1,nullptr,Productor,bq);pthread_create(&p2,nullptr,Productor,bq);pthread_create(&p3,nullptr,Productor,bq);pthread_join(c1,nullptr);pthread_join(c2,nullptr);pthread_join(p1,nullptr);pthread_join(p2,nullptr);pthread_join(p3,nullptr);delete bq;return 0;
}

这里我们创建了2个生产者,3个消费者(我们刚刚改了while,所以支持多生产多消费了)

2.封装(mutex)条件变量

a.条件变量框架搭建

#pragma once#include <iostream>
#include <pthread.h>namespace CondModule
{class Cond{public:Cond(){int n = ::pthread_cond_init(&_cond,nullptr);(void)n;}~Cond(){int n = ::pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}

b.其他函数的封装

#pragma once#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"namespace CondModule
{using namespace LockModule;class Cond{public:Cond(){int n = ::pthread_cond_init(&_cond,nullptr);(void)n;}void Wait(Mutex& mutex)//让线程曾经的锁释放曾经的锁{int n = ::pthread_cond_wait(&_cond,mutex.LockPtr());(void)n;}void Notify(){int n = ::pthread_cond_signal(&_cond);(void)n;}void NotifyAll(){int n = ::pthread_cond_broadcast(&_cond);(void)n;}~Cond(){int n = ::pthread_cond_destroy(&_cond);(void)n;}private:pthread_cond_t _cond;};
}

3.封装BlockQueue

#pragma once#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
#include "Cond.hpp"namespace BlockQueueModule
{using namespace LockModule;using namespace CondModule;static const int gcap = 10;template<class T>class BlockQueue{private:bool IsFull(){return _q.size() == _cap;}bool IsEmpty(){return _q.empty();}public:BlockQueue(int cap = gcap):_cap(cap),_cwait_num(0),_pwait_num(0){}void Equeue(const T& in){LockGuard lockguard(_mutex);while(IsFull()){std::cout << "生产者进入等待..." << std::endl;_pwait_num++;_productor_cond.Wait(_mutex);_pwait_num--;std::cout << "生产者被唤醒..." << std::endl;}_q.push(in);if(_cwait_num){_consumer_cond.Notify();}}void Pop(T* out){LockGuard lockguard(_mutex);while(IsEmpty()){std::cout << "消费者进入等待..." << std::endl;_cwait_num++;_consumer_cond.Wait(_mutex);_cwait_num--;std::cout << "消费者被唤醒..." << std::endl;}//到这里我们必定有数据*out = _q.front();_q.pop();//一定有空间if(_pwait_num){_productor_cond.Notify();}}~BlockQueue(){}private:std::queue<T> _q;              //保存数据的容器int _cap;                      //bq最大容量Mutex _mutex;        //互斥Cond _productor_cond;//生产者的条件变量Cond _consumer_cond; //消费者的条件变量int _cwait_num;int _pwait_num;};
}

六.传递任务

1.场景1

"Task.hpp"#pragma once#include <iostream>
#include <unistd.h>namespace TaskModule
{class Task{public:Task(){}Task(int a,int b):x(a),y(b){}void Excute(){sleep(1);//模拟任务处理时长result = x + y;}int X(){return x;}int Y(){return y;}int Result(){return result;}~Task(){}private:int x;int y;int result;};
}
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>using namespace BlockQueueModule;
using namespace TaskModule;
void* Consumer(void* args)
{BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);while(true){Task t;//1.从bq拿到数据bq->Pop(&t);//2.做处理t.Excute();printf("Consumer, 处理了一个任务: %d+%d=%d\n",t.X(),t.Y(),t.Result());}
}void* Productor(void* args)
{BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);int data = 10;while(true){//1.从外部获取数据int x = rand() % 10 + 1;int y = rand() % 20 + 1;Task t(x,y);//2.生产到bq中bq->Equeue(t);printf("productor 生产了一个任务: %d+%d=?\n",t.X(),t.Y());}
}int main()
{srand(time(nullptr) ^ getpid());BlockQueue<Task>* bq = new BlockQueue<Task>(5);//共享资源 -> 临界资源//单生产,单消费pthread_t c1,c2,p1,p2,p3;pthread_create(&c1,nullptr,Consumer,bq);pthread_create(&c2,nullptr,Consumer,bq);pthread_create(&p1,nullptr,Productor,bq);pthread_create(&p2,nullptr,Productor,bq);pthread_create(&p3,nullptr,Productor,bq);pthread_join(c1,nullptr);pthread_join(c2,nullptr);pthread_join(p1,nullptr);pthread_join(p2,nullptr);pthread_join(p3,nullptr);delete bq;return 0;
}

2.场景2

#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <functional>void test()
{std::cout << "haha test..." << std::endl;
}
void hello()
{std::cout << "hehe hello..." << std::endl;
}
using task_t = std::function<void()>;using namespace BlockQueueModule;void* Consumer(void* args)
{BlockQueue<task_t>* bq = static_cast<BlockQueue<task_t>*>(args);while(true){task_t t;//1.从bq拿到数据bq->Pop(&t);//2.做处理t();}
}void* Productor(void* args)
{BlockQueue<task_t>* bq = static_cast<BlockQueue<task_t>*>(args);int data = 10;while(true){sleep(1);bq->Equeue(test);}
}int main()
{srand(time(nullptr) ^ getpid());BlockQueue<task_t>* bq = new BlockQueue<task_t>(5);//共享资源 -> 临界资源//单生产,单消费pthread_t c1,c2,p1,p2,p3;pthread_create(&c1,nullptr,Consumer,bq);pthread_create(&c2,nullptr,Consumer,bq);pthread_create(&p1,nullptr,Productor,bq);pthread_create(&p2,nullptr,Productor,bq);pthread_create(&p3,nullptr,Productor,bq);pthread_join(c1,nullptr);pthread_join(c2,nullptr);pthread_join(p1,nullptr);pthread_join(p2,nullptr);pthread_join(p3,nullptr);delete bq;return 0;
}

7.补充

生产者消费者,要串行执行那么高效在哪里呢?

生产者在获取任务的时候,消费者可以从区域上拿任务,还可以进行处理任务

消费者处理数据的时候,生产者可以从外部拿数据

http://www.dtcms.com/a/589305.html

相关文章:

  • 消费级MCU如何管理内存
  • zabbix监控ES集群健康状态并触发钉钉告警
  • 一个网站需要几个人建设厅网站技术负责人要求
  • 2025知识协作工具选型,confluence vs 语雀 vs sward哪一款更好用?
  • 【C++】IO多路复用(select、poll、epoll)
  • 高低温环境下DC-DC芯片启动行为对比研究
  • IntelliJIdea 工具新手操作技巧
  • 第3节 STM32 串口通信
  • 网站页面优化内容包括哪些科技信息网站建设的背景
  • 网站做的关键词被屏蔽百度云盘做网站空间
  • 打砖块——反弹算法与碰撞检测
  • 大连网站设计报价建设网站的策划书
  • 何超谈“AI元宇宙将引领场景革命 “十五五”勾勒科技新蓝图”
  • watch监视-ref基本类型数据
  • 基于单片机的超声波人体感应PWM自动调光灯设计与实现
  • 保定微网站 建设郑州网站建设361
  • [Java EE] 计算机基础
  • 【Playwright自动化】安装和使用
  • logstatsh push 安装
  • C# OpenCVSharp实现Hand Pose Estimation Mediapipe
  • Java和.NET的核心差异
  • 基于灰关联分析与数据场理论的雷达信号分选优化方法
  • Linux Socket 编程全解析:UDP 与 TCP 实现及应用
  • 【NTN卫星通信】什么是LEO卫星技术
  • 郑州市建网站个人对网络营销的看法
  • 罗湖网站建设公司上海seo推广公司
  • 厦门市小学生计算机 C++语言竞赛(初赛)题目精讲与训练(整数的数据类型)
  • VC:11月9日加更,结构行情
  • 杨和网站设计河北邯郸永利ktv视频
  • 里氏替换原则Liskov Substitution Principle,LSP