【Linux】-cp模型

在这里插入图片描述
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、线程同步(条件变量)
  • 二、cp模型
    • 1.1模型的补充
    • 2.2 案例演示
    • 2.3 CP模型记忆
    • 总结


前言

上一篇博主花了很长时间带大家理解什么是线程,线程的作用,缺点,以及怎么去使用,相信大家已经自己去实践了一下,今天我们就来讲讲线程的一个很常见但也很重要的模型–cp模型,在讲解这个模型之前,博主要先讲解一下条件变量,因为他涉及到同步,一会博主都会详细介绍的,所以大家不用担心,话不多说,我们开始进入正文讲解。


一、线程同步(条件变量)

之前讲解了线程的互斥,简单的理解为对于一份临界资源只允许一个线程可以去访问他,而同步看上去和互斥是相反的词,实际不是的,在上一篇关于线程的讲解第六章节的时候提到多的抢票程序,说到第四点的时候就发现票被同一个线程抢走了,原因是在从线程的时间片内,刚释放锁的线程离该锁最近,别的线程还要唤醒,所以不做任何措施的线程刚释放锁的,就会立马去申请锁,所以我们的操作系统认为这样不好,一个共享资源让一个线程都去占用了,其他线程怎么办,所以就要想办法,你线程如果刚释放锁,就必须去后面排队,不能在去申请锁了。再去申请就会失败。

有了上面的知识铺垫,我们才有了线程同步的概念:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步,那线程同步怎么做到呢?? ----条件变量

当一个程序是多线程的,他每次竞争完锁之后都要去后面排队,是哪个后面呢??我们其中一个线程拥有锁之后,其他线程去申请锁就会失败,从而形成一个等待队列,而刚释放锁的线程,他想申请锁也会失败,所以去等待队列后面去排队,前提是申请锁失败(临界资源不就绪),才会去等待。 当锁被释放后,就要唤醒等待队列中的线程去申请锁,去访问临界资源,让程序继续去执行。


有来上面的讲解,我们知道条件变量必须有两个属性,一个是等待队列,一个是唤醒线程的标志位,我们的条件变量是锁的使用差不多,需要初始化。来看讲解:

我们创建多线程程序,每个线程对全局变量进行有顺序的加加:

#include<iostream>
#include<pthread.h>
#include<vector>
#include <unistd.h>
using namespace std;int cnt=0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void* func(void* arg)
{uint64_t number = (uint64_t)arg;std::cout << "pthread: " << number << " create success" << std::endl;while(true){pthread_mutex_lock(&mutex);std::cout << "pthread: " << number<< " ,cnt:" <<cnt++<< std::endl;pthread_cond_wait(&cond,&mutex);pthread_mutex_unlock(&mutex);}
} 
int main()
{vector<pthread_t> tids;pthread_t tid;for(uint64_t i=1;i<=4;i++){//注意:最后一个参数不要传地址进去,因为线程的栈区不是共享的,这会导致后面的线程名都是i=4的。直接传拷贝就可以了。pthread_create(&tid,NULL,func,(void*)i);//创建4个线程usleep(1000);}while(true)//让主线程来实现唤醒操作。{sleep(1);pthread_cond_signal(&cond);//唤醒一个线程//pthread_cond_broadcast(&cond);//唤醒所有线程cout<<"主线程唤醒一个线程"<<endl;//  cout<<"主线程唤醒所有线程"<<endl;    }for(auto tid:tids)//主线程进行等待。{pthread_join(tid,NULL);}return 0;
}

在这里插入图片描述
在这里插入图片描述

通过结果来看我们达到了我们想要的效果,来解释程序的代码:

  1. 我们的条件变量也像锁一样需要进行初始化,可以使用函数,也可以使用全局的初始化
    在这里插入图片描述
  2. 使用函数进行初始化就需要使用pthread_cond_destroy()这个函数进行销毁,全局初始化的,则不用,这个和锁的使用是一样的。
  3. 我们的条件变量可以一次唤醒等待队列的一个线程,通常都是队头的,也可以一次唤醒队列中所有的线程。

为什么我们的等待要放在加锁解锁之间??
先改造我们之前RAII风格的抢票程序,让他变得也有顺序。
我们要加一个条件变量进去:
myticket.hpp:

#pragma once
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;
class mylock
{
public:mylock(pthread_mutex_t*lock,pthread_cond_t* cond):lock_(lock),cond_(cond){}void lock(){pthread_mutex_lock(lock_);}void unlock(){pthread_mutex_unlock(lock_);}void wait(){pthread_cond_wait(cond_,lock_);}~mylock(){}
private:pthread_mutex_t* lock_;pthread_cond_t* cond_;
};class lockguard
{
public:lockguard(pthread_mutex_t*lock,pthread_cond_t* cond):mutex_(lock,cond){ mutex_.lock();}void wait(){mutex_.wait();}~lockguard(){mutex_.unlock();}
private:mylock mutex_;};

mythread.cc:

z#include"myticket.hpp"
#define NUM 4
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
class threadData
{
public:threadData(int number){threadname = "thread-" + to_string(number);}public:string threadname;
};int tickets = 100; // 用多线程,模拟一轮抢票void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){lockguard lockg(&lock,&cond);//只在这歌循环里面有效,出作用域就销毁if(tickets > 0){//usleep(100);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;lockg.wait(); }elsebreak;usleep(13);}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= NUM; i++){pthread_t tid;threadData *td = new threadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);sleep(1);}while(true) {sleep(1);pthread_cond_signal(&cond); //唤醒在cond的等待队列中等待的一个线程,默认都是第一个//pthread_cond_broadcast(&cond);std::cout << "signal one thread..." << std::endl;}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}

在这里插入图片描述

通过一开始上面两个程序,我们发现条件变量是可以实现线程同步的,我们的条件变量的操作一种就四个函数,初始化和销毁没啥可讲的,唤醒肯定是由别的线程唤醒的,自己都在等待的不可能自己把自己唤醒的,一会讲解cp问题的时候会更加好理解,我们的最后一个函数就是等待,**我们把线程等待放到了加锁和解锁之间,我们上面说过,当我们申请资源不就绪的时候就会等待成功, 当我是持有锁的时候你让我去等待,那别的线程不就拿不到锁了,不用担心,我们等待函数让持有锁的线程去等待,会自动释放锁的。这个问题解决了,我们为什么去等待,一定是临界资源不就绪了 ,你怎么知道临界资源就绪还是不就绪呢??是你判断出来的,判断是访问临界资源吗??答案是的,所以判断必须在加锁之后,这也就导致了等待在加锁和解锁之间。对于第一个和第二个程序,他们的线程申请锁成功,获取到临界资源,他们不去等待而是在他们访问临界资源的时候,然后自己直接去等待,这样别人就可以申请到锁去访问了。 **

二、cp模型

上面说了那么多,我们终于将条件变量讲解完毕了,可以来讲解cp模型,他实际是叫生产者消费者模型,这个模型和我们生活中的案例非常符合,接下来讲解一个小故事带大家理解这个模型。

在这里插入图片描述

在我们日常生活中去的比较多的就是超市了,我们去超市直接去买东西,不需要等产品生产好了在去拿,而超市等商品没有了,直接去生产商去进货,有了超市的存在消费者和生产者之间存在的差异就抵消了,如果我们去生产商进行消费,那我们还要等生产出来才可以拿到,而且一次生产的特别少,这样是不行的。所以这个超市就是生产者和消费者共享的一个地方,让我们消费者和生产可以共同实现同步互斥。


1.模型的优点:
我们通过超市实现了消费者和生产者的忙闲不均。
将生产者和消费者实现了解藕。
支持并发(一会细说)


2.模型内部的关系:
(1)生产者与生产者
他们是互斥关系,多个生产者之间要分别给超市供货,好比同一个货架上已经放了一个生产商的货物,另一个就不能放了,货架多,让我们觉得生产者不是互斥的。所以他们之间要 互斥
(2)消费者与消费者
他们是互斥关系,虽然超市里面好多消费者一起去购物,但是同一个商品只能有一个消费者获得,当商品不足的时候,可能就会有多个消费者去抢同一个商品,所以他们之间要 互斥
(3)生产者与消费者
当我们消费者在进行消费的时候,你生产者过来把自己的商品放上去,那不就把之前的商品给覆盖了吗,万一消费者想要之前的商品不就获取不到了,所以两者要互斥,有一天,我们顾客想要打电话给超市问他方便面有没有,此时一直打不通,原因是我们生产方便面的产家一直给超市打电话,你要不要方便面,导致消费者一直打不进去电话,此时生产者就一直占有超市这个共享资源,所以刚打完电话就不要打了,排队去,五天后在,这样消费者才可以进行消费,所以生产者和消费者也要保持 同步 关系

我们先来实现单生产单消费的模型,然后在改。
对于这个超市,他的作用就是效率高,他的本质大号的缓存空间,今天我们实现的是基于BlockingQueue的生产者消费者模型
所以我们要有一个阻塞队列:
main.cc:

#include"BlockQueue.hpp"void* Productor(void* arg)
{BlockQueue<int>*bq=static_cast<BlockQueue<int>*>(arg);int data=0;while(true){data++;bq->push(data);cout<<"生产者生产了数据:"<<data<<endl;sleep(1);}
}
void* Consumer(void* arg)
{BlockQueue<int>*bq=static_cast<BlockQueue<int>*>(arg);while(true){int data=bq->pop();cout<<"消费者消费了数据:"<<data<<endl;}
}
int main()
{pthread_t productor;//定义一个生产者线程pthread_t consumer;//定义一个消费者线程BlockQueue<int>* bq=new BlockQueue<int>();//这是堆区,可以之间传地址的,堆区线程共享,之前是栈区的ipthread_create(&productor,nullptr,Consumer,bq);//创建一个消费者线程pthread_create(&consumer,nullptr,Productor,bq);//创建一个生产者线程//主线程什么事情都不干,监视两个线程就可以,唤醒是两个线程互相唤醒,不像之前讲解的需要主线程来进行唤醒。pthread_join(consumer,nullptr);pthread_join(productor,nullptr);delete bq;return 0;
}

BlockQueue.hpp:

#pragma once
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<queue>
using namespace std;template<class T>//模板类
class BlockQueue
{static const int defalutnum = 20;//阻塞队列的大小
public:BlockQueue(int maxSize=defalutnum):maxSize(maxSize){pthread_mutex_init(&lock,nullptr);//给锁进行初始化pthread_cond_init(&full,nullptr);//给两个条件变量进行初始化pthread_cond_init(&empty,nullptr);//控制一个高低,不让一生产就消费,也不让一消费就生产lower_water=maxSize/3;high_water=maxSize*2/3;}//  为什么wait是放在加锁喝解锁之间的,如果没有判断大家应该怎么理解,线程刚加锁就被放到条件变量下main去等待,那有什么意义,并且是持有锁的情况去放到条件变量等待的,那别人怎么拿到锁呢,原因是wait这个函数将持有锁的线程放到条件变量去等待回自动释放锁
//话说回来,我们刚才不加判断,直接去等待,那么这个加锁就没有意义,那什么时候去等待合适呢??答案是不符合条件的时候,临界资源不就绪的时候,别人在用,是我们通过if判断出来的,是我们程序员自己知道的,那么我们进行判断的时候是不是也在访问临界资源,就
//注定我们的等待是在加锁和解锁之间的,这个时候大家应该理解等待为什么放在加锁解锁之间了吧。T pop(){pthread_mutex_lock(&lock);//接下来要进行访问阻塞队列是临界资源需要加锁if(q.empty())//如果队列为空,消费者就不能进行消费,就要进入空的条件变量进行等待{pthread_cond_wait(&empty,&lock);//临界资源不就绪去排队}T data =q.front();//消费数据q.pop();if(q.size()<=lower_water) pthread_cond_signal(&full);///消费者消费一个,说明队列肯定不为满,所以唤醒一个生产者pthread_mutex_unlock(&lock);//进行解锁,给下一个要访问的线程进行使用return data;}void push(T data){pthread_mutex_lock(&lock);//接下来要进行访问阻塞队列是临界资源需要加锁if(q.size()==maxSize)//如果队列已经满了,生产者就不能进行生产,就要进入满的条件变量进行等待,临界资源不就绪{pthread_cond_wait(&full,&lock);//临界资源不就绪去排队}q.push(data);//生产数据if(q.size()>=high_water) pthread_cond_signal(&empty);//生产者生产一个,说明队列肯定不为空,所以唤醒一个消费者,通过其他线程来唤醒另一个线程。pthread_mutex_unlock(&lock);//进行解锁,给下一个要访问的线程进行使用}~BlockQueue(){//因为锁和条件变量都是全局初始化的,所以需要销毁pthread_mutex_destroy(&lock);//销毁锁pthread_cond_destroy(&full);//销毁满条件变量pthread_cond_destroy(&empty);//销毁空条件变量}
private:queue<T> q;//阻塞队列,相对于超市pthread_mutex_t lock;//定义一把锁pthread_cond_t full;//阻塞队列满的时候生产者进行排队的条件变量pthread_cond_t empty;//阻塞队列空的时候消费者进行排队的条件变量int maxSize;//队列最大值int lower_water;int high_water;
};

在这里插入图片描述
我们看到效果了,那我们多生产多消费怎么去实现呢,因为只有一把锁,所以我们可以一次创建多生产多消费模型,也可以维护上面三种关系,来看改动的代码:

int main()
{pthread_t productor;//定义一个生产者线程pthread_t consumer;//定义一个消费者线程vector<pthread_t> prods;//定义一个生产者线程组vector<pthread_t> conss;//定义一个消费者线程组BlockQueue<int>* bq=new BlockQueue<int>();//这是堆区,可以之间传地址的,堆区线程共享,之前是栈区的ifor(uint64_t i=1;i<=4;i++)//创建4个生产者{pthread_create(&productor,nullptr,Consumer,bq);//创建一个生产者线程prods.push_back(productor);//将生产者线程放入生产者线程组}for(uint64_t i=1;i<=4;i++)//创建四个消费者{pthread_create(&consumer,nullptr,Productor,bq);//创建一个消费者线程conss.push_back(consumer);//将消费者线程放入消费者线程组}//主线程什么事情都不干,监视两个线程就可以,唤醒是两个线程互相唤醒,不像之前讲解的需要主线程来进行唤醒。for(auto i:prods){pthread_join(i,nullptr);}for(auto i:conss){pthread_join(i,nullptr);}delete bq;return 0;
}#pragma once
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<queue>
using namespace std;template<class T>//模板类
class BlockQueue
{static const int defalutnum = 20;//阻塞队列的大小
public:BlockQueue(int maxSize=defalutnum):maxSize(maxSize){pthread_mutex_init(&lock,nullptr);//给锁进行初始化pthread_cond_init(&full,nullptr);//给两个条件变量进行初始化pthread_cond_init(&empty,nullptr);//控制一个高低,不让一生产就消费,也不让一消费就生产// lower_water=maxSize/3;// high_water=maxSize*2/3;}//  为什么wait是放在加锁喝解锁之间的,如果没有判断大家应该怎么理解,线程刚加锁就被放到条件变量下main去等待,那有什么意义,并且是持有锁的情况去放到条件变量等待的,那别人怎么拿到锁呢,原因是wait这个函数将持有锁的线程放到条件变量去等待回自动释放锁
//话说回来,我们刚才不加判断,直接去等待,那么这个加锁就没有意义,那什么时候去等待合适呢??答案是不符合条件的时候,临界资源不就绪的时候,别人在用,是我们通过if判断出来的,是我们程序员自己知道的,那么我们进行判断的时候是不是也在访问临界资源,就
//注定我们的等待是在加锁和解锁之间的,这个时候大家应该理解等待为什么放在加锁解锁之间了吧。T pop(){pthread_mutex_lock(&lock);//接下来要进行访问阻塞队列是临界资源需要加锁if(q.empty())//如果队列为空,消费者就不能进行消费,就要进入空的条件变量进行等待{pthread_cond_wait(&empty,&lock);//临界资源不就绪去排队}T data =q.front();//消费数据q.pop();cout<<"thread_id:"<<pthread_self()<<",消费者消费了数据:"<<data<<endl;usleep(12);if(q.size()!=20) pthread_cond_signal(&full);///消费者消费一个,说明队列肯定不为满,所以唤醒一个生产者pthread_mutex_unlock(&lock);//进行解锁,给下一个要访问的线程进行使用return data;}bool push(T data){pthread_mutex_lock(&lock);//接下来要进行访问阻塞队列是临界资源需要加锁if(q.size()==maxSize)//如果队列已经满了,生产者就不能进行生产,就要进入满的条件变量进行等待,临界资源不就绪{pthread_cond_wait(&full,&lock);//临界资源不就绪去排队}q.push(data);//生产数据cout<<"thread_id:"<<pthread_self()<<",生产者生产了数据:"<<data<<endl;usleep(13);if(q.size()!=0) pthread_cond_signal(&empty);//生产者生产一个,说明队列肯定不为空,所以唤醒一个消费者,通过其他线程来唤醒另一个线程。pthread_mutex_unlock(&lock);//进行解锁,给下一个要访问的线程进行使用}~BlockQueue(){//因为锁和条件变量都是全局初始化的,所以需要销毁pthread_mutex_destroy(&lock);//销毁锁pthread_cond_destroy(&full);//销毁满条件变量pthread_cond_destroy(&empty);//销毁空条件变量}
private:queue<T> q;//阻塞队列,相对于超市pthread_mutex_t lock;//定义一把锁pthread_cond_t full;//阻塞队列满的时候生产者进行排队的条件变量pthread_cond_t empty;//阻塞队列空的时候消费者进行排队的条件变量int maxSize;//队列最大值// int lower_water;// int high_water;
};

在这里插入图片描述

博主未来让大家看的更加清楚,将高低水位线去掉了。

1.1模型的补充

对于cp模型还有几点要补充

  1. 我们的生产商不但要往超市里面放商品,他也要抽时间生产商品,对于这个模型,不止要会放数据到阻塞队列里面,还要会获取数据,一般从网络或者用户去获取,而获取数据也要花时间。
  2. 对于消费者,我们不可能天天来超市消费,等我们买的商品使用完了才去购买,对于模型也一样,我们取到数据,还要进行处理,处理也要花时间。

我们cp模型前面说过效率较高,并发访问,这是为什么?我们只有一把锁,每次只能有一个线程访问阻塞队列,者不是串行访问吗??确实没错,但是当我们其中一个线程访问时,其他线程在获取数据或者处理数据,这样整体上就实现了并发访问,今天没有合适的场景,但是我么你不嗯呢个忽略cp模型有这个特性

伪唤醒: 重点
在这里插入图片描述
在这里插入图片描述

我们看到这个代码分别是生产者和消费者的代码,我们圈住的部分,假设我们的生产者生产了一个数据,此时阻塞队列刚好满了,唤醒消费者去访问了,消费者访问了一个,空出来一个,消费者
又去唤醒生产者去生产,此时消费者采取了从全部唤醒策略,将多个生产者线程都唤醒了,假设三个生产者线程必须重新去申请锁,才可以去访问,没有申请到的两个线程被挂起等待,我们申请锁不是执行上面第一行申请锁的函数,而是在等待函数内部去做的,申请成功返回,继续往下面执行,此时申请到锁的生产者线程生产了一个数据,此时队列又满了,然后去唤醒消费者线程,此时不止有消费者线程去申请锁,还有刚才两个被挂起的生产者线程也等着申请锁呢,万一此时其中一个申请到锁,在往里面插数据,就会导致益处,显然这样是不行的,所以我们不能使用if判断,而是要使用while判断。

2.2 案例演示

我们刚才写的是整形,接下来写一个计算器,你发数据,我给你处理数据,就可以完成任务的派发:
Task.hpp:

#pragma once
#include <iostream>
#include <string>std::string opers="+-*/%";enum{DivZero=1,ModZero,Unknown
};class Task
{
public:Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0){}void run(){switch (oper_){case '+':result_ = data1_ + data2_;break;case '-':result_ = data1_ - data2_;break;case '*':result_ = data1_ * data2_;break;case '/':{if(data2_ == 0) exitcode_ = DivZero;else result_ = data1_ / data2_;}break;case '%':{if(data2_ == 0) exitcode_ = ModZero;else result_ = data1_ % data2_;}            break;default:exitcode_ = Unknown;break;}}void operator ()(){run();}std::string GetResult(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=";r += std::to_string(result_);r += "[code: ";r += std::to_string(exitcode_);r += "]";return r;}std::string GetTask(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=?";return r;}~Task(){}private:int data1_;int data2_;char oper_;int result_;int exitcode_;};

大家下去自己去看看这个是怎么去运行的,把模版改一下。

2.3 CP模型记忆

我们上面说了CP模型是三种关系,两个角色,一个交易场所,所以我们采用321原则去记忆。

总结

对于CP模型,可以让我们更好是使用多线程去观察一些现象,也可以更好展示条件变量的作用,希望大家下去多去联系,这篇就讲解到这里了,下篇我们开始讲解信号量。

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

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

相关文章

IS-IS:04 DIS

IS-IS 协议只支持两种网络类型&#xff0c;即广播网络和点到点网络。与 OSPF 协议相同&#xff0c; IS-IS 协议在广播网络中会将网络视为一个伪节点 &#xff08; Pesudonde&#xff0c;简称 PSN&#xff09;&#xff0c;并选举出一台DIS &#xff08;Designated IS&#xff09…

ASP.NET Core 7 Web 使用Session

ASP.NET Core 好像不能像20年前那样直接使用Session函数&#xff0c;我使用如下方法 1、在NuGet安装以下2个包 2、在Program.cs注册 //注册Session builder.Services.AddSession(options > {options.IdleTimeout TimeSpan.FromMinutes(60);options.Cookie.HttpOnly fals…

CAN相关寄存器

1. CAN_ MCR&#xff1a;CAN主控制寄存器。主要负责CAN工作模式的配置。 CAN_BTR&#xff1a;位时序寄存器。用来设置分频/TBs1/TBs2/Tsw等参数&#xff0c;设置测试模式。 CAN_(T/R)IxR&#xff1a;标识符寄存器。存放&#xff08;待发送/接收)的报文ID、扩展ID、IDE位及RTR…

如何快速搭建实用的爬虫管理平台

目录 一、前言 二、选择合适的爬虫框架 三、搭建数据库 步骤1 步骤2 步骤3 四、搭建Web服务器 步骤1 步骤2 步骤3 步骤4 五、管理爬虫 六、总结 一、前言 爬虫是互联网数据采集的关键工具&#xff0c;但是随着数据量的增加和需求的多样化&#xff0c;手动运行和管…

SIFT图像特征表述

SIFT&#xff08;尺度不变特征变换&#xff09;是一种用于图像处理和计算机视觉领域的特征提取算法。其目的是检测和描述图像中的局部特征。SIFT特征对旋转、尺度缩放、亮度变化保持不变性&#xff0c;对视角变化、仿射变换、噪声也具有一定的稳健性。下面是SIFT特征提取的基本…

计算机网络体系架构认知--网络协议栈

文章目录 一.计算机网络分层架构各协议层和计算机系统的联系从整体上理解计算机网络通信计算机网络通信的本质 二.Mac地址,IP地址和进程端口号三.局域网通信与跨局域网通信局域网通信跨局域网通信全球互联的通信脉络 四.网络编程概述 一.计算机网络分层架构 实现计算机长距离网…

12.Golang中类的表示与封装

目录 概述类的表示代码结果 类的封装代码结果 结束 概述 Golang中类的表示与封装 类的表示 代码 注释掉的代码&#xff0c;并不能拿来当赋值或获取值来使用。 package mainimport "fmt"// 类大写则代表&#xff0c;可以被其它包使用 type Hero struct {// 属性方法大…

RHCE作业

1.写一个脚本&#xff0c;完成如下功能 传递一个参数给脚本&#xff0c;此参数为gzip、bzip2或者xz三者之一 (1) 如果参数1的值为gzip&#xff0c;则使用tar和gzip归档压缩/etc目录至/backups目录中&#xff0c;并命名为/backups/etc-20160613.tar.gz&#xff1b; (2) 如果参…

临紧光五行护盾

临紧光五行护盾基础名词解释 粒子系统仿真&#xff0c;离散事件系统设计临紧光五行护盾&#xff08;云藏山鹰临近光五行散射&#xff09;V-ATPase道装&#xff0c;意气实体过程光效集聚模拟器荀况数论云藏山鹰类型物粒子系统导引云藏山鹰类型物与冯诺依曼爆炸学物品分类表杨米尔…

【华为 ICT HCIA eNSP 习题汇总】——题目集10

1、以下哪个动态路由协议不能应用在 IPv6 网络中&#xff1f; A、IS-IS B、RIPng C、BGP4 D、OSPFv3 考点&#xff1a;路由技术原理 解析&#xff1a;&#xff08;A&#xff09; IS-ISv6 是在 IPv6 环境下&#xff0c;IS-IS 协议进行了相应的扩展和改进&#xff0c;以适应 IPv6…

项目风采展示【TRDa】

桌面功能介绍&#xff1a; 1&#xff1a;支持本地音乐、三方音乐控制播放展示功能&#xff1b; 2&#xff1a;支持陀螺仪 3&#xff1a;支持蓝牙列表显示。

STM32单片机的基本原理与应用(二)

单片机外部中断的基本原理 STM32 的每个 IO 都可以作为外部中断的中断输入口&#xff0c;这点也是 STM32 的强大之处。那么其中断是怎样实现的呢&#xff1f;其原理就是由GPIO口产生触发信号&#xff0c;通过NVIC中断控制器和EXTI外部中断控制器完成中断响应。使用外部中断可以…

RabbitMQ问题总结

:::info 使用场景 异步发送&#xff08;验证码、短信、邮件。。。&#xff09;MySQL 和 Redis、ES 之间的数据同步分布式事务削峰填谷… ::: 如何保证消息不丢失 上图是消息正常发送的一个过程&#xff0c;那在哪个环节中消息容易丢失&#xff1f;在哪一个环节都可能丢失 生…

opencv#32 可分离滤波

滤波的可分离性 就是将一个线性滤波变成多个线性滤波&#xff0c;这里面具体所指的是变成x方向的线性滤波和y方向的线性滤波。无论先做x方向的滤波还是y方向滤波&#xff0c;两者的叠加结果是一致的&#xff0c;这个性质取决于滤波操作是并行的&#xff0c;也就是每一个图像在滤…

vue3+echarts绘制某省区县地图

vue3echarts绘制某省区县地图 工作中经常需要画各种各样的图&#xff0c;echarts是使用最多的工具&#xff0c;接近春节&#xff0c;想把之前画的echarts图做一个整合&#xff0c;方便同事和自己随时使用&#xff0c;因此用vue3专门写了个web项目&#xff0c;考虑之后不断完善…

R12.2 EBS 修改 APPS 密码 详细步骤

目录 前言准备修改步骤1.关闭应用层2.FNDCPASS 修改密码3. 运行 autoconfig4.单独启动 webLogic 服务5.登录weblogic&#xff0c;更新apps密码6.启动应用层7.验证 结尾 前言 本文的目的是修改 apps 密码&#xff0c;主要参考官方文档 metalink 1674462.1&#xff0c;请注意本文…

Task04:DDPG、TD3算法

本篇博客是本人参加Datawhale组队学习第四次任务的笔记 【教程地址】https://github.com/datawhalechina/joyrl-book 【强化学习库JoyRL】https://github.com/datawhalechina/joyrl/tree/main 【JoyRL开发周报】 https://datawhale.feishu.cn/docx/OM8fdsNl0o5omoxB5nXcyzsInGe…

华为三层交换机之基本操作

Telnet简介 Telnet是一个应用层协议,可以在Internet上或局域网上使用。它提供了基于文本的远程终端接口&#xff0c;允许用户在本地计算机上登录到远程计算机&#xff0c;然后像在本地计算机上一样使用远程计算机的资源。Telnet客户端和服务器之间的通信是通过Telnet协议进行的…

OpenAI 降低价格并修复拒绝工作的“懒惰”GPT-4,另外ChatGPT 新增了两个小功能

OpenAI降低了GPT-3.5 Turbo模型的API访问价格&#xff0c;输入和输出价格分别降低了50%和25%。这对于使用API进行文本密集型应用程序的用户来说是一个好消息。 OpenAI官网&#xff1a;OpenAI AIGC专区&#xff1a;aigc 教程专区&#xff1a;AI绘画&#xff0c;AI视频&#x…

博弈论(牛客练习赛)

思路&#xff1a;我们考虑小念赢 1、如果n>1并且p0&#xff0c;小念可以连续取两次&#xff0c;相当于小念有挂&#xff0c;可以从必败态转为必胜态&#xff0c;必赢。 2、如果n>1并且m>n-1&#xff0c;小念第一次取n-1个&#xff0c;小念必赢。 代码&#xff1a; …