Linux_生产消费者模型

news2024/11/26 13:29:45

目录

1、生产消费者模型示意图

2、生产者消费者之间的关系 

3、定义交易场所

4、实现生产消费者模型

5、伪唤醒

6、多生产多消费者的实际运用 

7、POSIX信号量 

7.1 初始化信号量 

7.2 销毁信号量  

7.3 等待信号量

7.4 发布信号量

8、生产消费的环形队列模型

8.1 实现多生产多消费环形模型

结语


前言:

        使用生产消费者模型的原因如下:生产消费者模型常用于多线程并发式执行流,该模型具有同步与互斥机制。该模型的核心点在于用一个容器作为生产者和消费者相互通信的桥梁,使得生产者和消费者不直接进行交互,降低了生产者与消费者之间的强耦合度。好处在于生产者生产数据后即使消费者不来拿这个数据,生产者也可以继续生产,并且后续生产的数据不会覆盖之前的数据,只有当容器满了,生产者才会阻塞。只要容器内有数据,消费者就可以从容器中拿数据,无需等待生产者一个一个的生产数据。

1、生产消费者模型示意图

         生产消费者模型具体示意图如下:

2、生产者消费者之间的关系 

         生产消费者模型具有互斥机制,因此生产者和消费者之间肯定存在互斥概念,这里例举3个关系(关系指的是生产者对交易场所的操作和消费者对交易场所的操作之间的关系,简写成生产者和消费者的关系):

1、生产者和消费者的关系必须是互斥和同步的,当生产者访问交易场所时消费者不能访问交易场所,只有当生产者访问结束后消费者才可访问交易场所,即访问具有原子性。

2、生产者和生产者之间也是互斥,他们的关系是类似竞争。

3、消费者和消费者之间也是互斥,比如一个资源只能让一个消费者获取,则另一个消费者就获取不了了。

        综上所述可以发现生产者消费者在代码层面上是共享一把锁的。 

3、定义交易场所

        这里用队列来模拟交易场所,入队表示生产者访问场所,出队表示消费者访问场所。生产者和消费者就是两个线程,入队和出队是这两个线程要执行的操作。所以需要自己实现一个队列,并且对该队列的入队和出队进行同步互斥约束。

        将队列封装成一个类,自定义队列代码如下:

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
using namespace std;

template <class T>
class BlockQueue
{
    static const int defalutnum = 5;//队列的大小
public:
    BlockQueue(int maxcap = defalutnum):maxcap_(maxcap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
    }

    //出队-消费者
    T pop()
    {
        pthread_mutex_lock(&mutex_);
        if(q_.size() == 0) 
        {
            pthread_cond_wait(&c_cond_, &mutex_); //场所为空则消费者必须等待
        }
        
        T out = q_.front(); 
        q_.pop();//消费了一个数据
        pthread_cond_signal(&p_cond_); //唤醒生产者
        pthread_mutex_unlock(&mutex_);

        return out;
    }

    //入队-生产者
    void push(const T &in)
    {
        pthread_mutex_lock(&mutex_);
        if(q_.size() == maxcap_)
        { 
            pthread_cond_wait(&p_cond_, &mutex_); //场所为满则生产者必须等待
        }
        // 1. 队列没满 2.被唤醒 
        q_.push(in);//生产了一个数据                
        pthread_cond_signal(&c_cond_);//唤醒消费者
        pthread_mutex_unlock(&mutex_);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_cond_);
        pthread_cond_destroy(&p_cond_);
    }
private:
    std::queue<T> q_; // 复用容器队列
    int maxcap_;      // 队列大小
    pthread_mutex_t mutex_;//生产者和消费者共用一把锁,因为他们的互斥的
    pthread_cond_t c_cond_;//消费者的等待队列
    pthread_cond_t p_cond_;//生产者的等待队列
};

        上述代码的逻辑:刚开始消费者会先进入条件变量的等待队列中等待(等待时消费者会释放锁),生产者此时就申请到锁了,然后生产一个数据,只要场所里有了数据,就消费者就可以来拿数据,因此生产者进行入队操作后,就可以唤醒在等待队列里的消费者了。相反消费者只要出队了一个数据,表示场所里有剩余空间,消费者就可以唤醒生产者来生产数据了。

        注意:采用的唤醒策略是交互性唤醒

        小细节:虽然唤醒了对方,但是不意味着唤醒了那一刻对面就拿到锁了,而是自己要先释放锁对方才能申请到锁,对方申请到锁了才可以从等待队列中出来。

4、实现生产消费者模型

        有了上面的自定义栈结构,后续只需要在生产者线程中调用push函数,消费者线程中调用pop函数即可,实现生产消费者模型代码如下: 

#include "queue.hpp"

void *Consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);

    while (true)
    {
        int t = bq->pop();//消费者消费数据
        cout<<"消费者拿到一个数据"<<t<<endl;
        sleep(1);
    }
}

void *Productor(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    int num = 0;
    while (true)
    {
        bq->push(num);//生产者生产数据
        cout<<"生产者生产一个数据"<<num<<endl;
        num++;
    }
}

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;
}

        运行结果:


         从上述结果可以分析出两个关键点:

        1、为什么是生产者先生产了5个数据,然后消费者才过来消费,而不是生产一个数据消费者就来消费。

        答:因为上述代码中没有对生产者的循环调用进行sleep,并且生产者和消费者共用一把锁,在最开始的时候消费者会先触发条件变量(因为交易场所为空),会直接进入消费等待队列中。而生产者申请到锁后并不会触发条件变量(因为交易场所未满),所以生产者就可以拿着锁去访问临界资源,当他唤醒消费者等待队列并且释放锁后,此时还是他距离锁最近的(因为在代码中他释放锁之后的动作就是申请锁),因为生产者释放锁后没有对其进行任何的sleep,因此生产者在此时是对锁拥有最高优先级。 

        具体逻辑图如下:

        2、为什么消费者消费一次数据后就可以直接唤醒生产者呢?

        答:因为消费者调用完pop函数后sleep了1秒,导致消费者不再“离锁最近”,并且消费者消费数据后对生产者进行了唤醒,所以生产者的等待队列中没有任何线程跟其竞争锁,最终生产者被成功唤醒并申请到了锁,并执行push代码。

         具体逻辑图如下:

5、伪唤醒

        伪唤醒指的是当线程在等待队列中被唤醒了,实际上当前条件并不满足于该线程被唤醒,此时就会导致意料之外的错误,所以在线程被唤醒时,最好先检查一下当前的条件是否满足,因此上面代码中唤醒的条件应该从if改成while,以便防止伪唤醒发生,特别是在多生产多消费的情况下。

        防止伪唤醒的代码如下: 

 T pop()
    {
        pthread_mutex_lock(&mutex_);
        while(q_.size() == 0)//防止伪唤醒
        {
            pthread_cond_wait(&c_cond_, &mutex_); //场所为空则消费者必须等待
        }
        
        T out = q_.front(); 
        q_.pop();//消费了一个数据
        pthread_cond_signal(&p_cond_); //唤醒生产者
        pthread_mutex_unlock(&mutex_);

        return out;
    }


void push(const T &in)
    {
        pthread_mutex_lock(&mutex_);
        while(q_.size() == maxcap_)//防止伪唤醒
        { 
            pthread_cond_wait(&p_cond_, &mutex_); //场所为满则生产者必须等待
        }
        // 1. 队列没满 2.被唤醒 
        q_.push(in);//生产了一个数据                
        pthread_cond_signal(&c_cond_);//唤醒消费者
        pthread_mutex_unlock(&mutex_);
    }

        举例说明伪唤醒的错误:在多生产的场景下,当我们只需要生产一个数据时,多个生产者竞争一个锁确实可以只生产一个数据,但是当这个竞争到锁的生产者释放锁时,该锁并不会被消费者拿到,而是被等待队列中另一个生产者申请到锁,就会导致数据溢出的风险。因为多个生产者是处于同一个条件变量的等待队列中,当其中某个生产者重新调用wait函数回到该队列中时,他会释放锁,但是这个锁不会被他唤醒的消费者先申请到,而是被他所在队列的其他生产者拿到,这就导致其他生产者继续向满的队列生产。(总而言之,伪唤醒是一种不稳定的唤醒方式,伪唤醒的出现和系统调度已经线程竞争条件都有关系,因此当多线程的情况变得复杂时要注意防护伪唤醒

6、多生产多消费者的实际运用 

        在实际项目中,生产者通常是给消费者派发各种任务,有了生产消费者模型,就可以把这些任务写进交易场所中,然后消费者到场所中领取任务并执行,因此我们可以先定义一个任务类,用于任务的派发和执行。

        任务类的代码如下:

#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;//除0错误
                else result_ = data1_ / data2_;
            }
            break;
        case '%':
           {
                if(data2_ == 0) exitcode_ = ModZero;//模0错误
                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_;//退出码
};

        主函数执行生产消费者代码如下:

#include "queue.hpp"
#include "task.hpp"
#include <unistd.h>
#include <ctime>

void *Consumer(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);

    while (true)
    {
        // 消费
        Task t = bq->pop();
        // 计算
        t();
        std::cout << "处理任务: " << t.GetTask() << " 运算结果是: " \
        << t.GetResult() << " thread id: " << pthread_self() << std::endl;
    }
}

void *Productor(void *args)
{
    int len = opers.size();
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    int x = 10;
    int y = 20;
    while (true)
    {
        // 模拟生产者生产数据
        int data1 = rand() % 10 + 1;//随机数
        usleep(10);
        int data2 = rand() % 10;//随机数
        char op = opers[rand() % len];//随机运算符号
        Task t(data1, data2, op);

        // 生产
        bq->push(t);
        std::cout << "生产了一个任务: " << t.GetTask() << " thread id: " \
        << pthread_self() << std::endl;
        sleep(1);
    }
}

int main()
{
    srand(time(nullptr));

    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t c[3], p[5];
    for (int i = 0; i < 3; i++)
    {
        pthread_create(c + i, nullptr, Consumer, bq);
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_create(p + i, nullptr, Productor, bq);
    }

    for (int i = 0; i < 3; i++)
    {
        pthread_join(c[i], nullptr);
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(p[i], nullptr);
    }
    delete bq;
    return 0;
}

        运行结果:

7、POSIX信号量 

         POSIX信号量可以让多个线程访问同一个交易场所的不同区域,也就是说在这种情况下生产者和消费者不存在互斥,因为他们访问的是场所内不同的资源空间,当POSIX信号量为0时表示当前线程不能往下访问临界资源

7.1 初始化信号量 

        接口介绍如下:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem表示要初始化的信号量
//pshared为0表示线程间共享,非零表示进程间共享
//value表示信号量初始值

         初始化信号量时,必须先定义一个类型为sem_t的信号量变量。

7.2 销毁信号量  

        接口介绍如下:

int sem_destroy(sem_t *sem);//销毁一个信号量

7.3 等待信号量

        接口介绍如下:

int sem_wait(sem_t *sem);//等待信号量,会将信号量的值减1

7.4 发布信号量

         接口介绍如下:

int sem_post(sem_t *sem);//将信号量值加1

8、生产消费的环形队列模型

        上面的生产消费模型用的是队列作为容器,现在用数组取模的方式模拟环形队列作为生产消费模型的交易场所,示意图如下:

        上图表示交易场所为空时,生产者和消费者相遇,这时候生产者和消费者只有生产者可以往前走(生产者信号量不为0),因为场所内没有数据(消费者信号量为0),所以消费者无法消耗数据,也就无法往前走。


        当交易场所满的情况如下:

        可以发现当交易场所满的时候,生产者依然是和消费者指向同一块区域,并且此时也只有一方可以移动,即只有消费者可以移动(消费者信号量不为0,而生产者信号量为0),因为当前没有空间让生产者生产数据了,只能让消费者消费数据。


        以上是空或满的情况,当然,只要消费者没有和生产者相遇,那么他们两个可以同时对交易场所进行操作,因为这种情况他们两个的信号量都不为0,而这是上述自定义队列办不到的,这也是POSIX信号量的优势,只有当交易场所为空或为满时(其中一方的信号量为0时),生产者和消费者才是互斥关系。 

8.1 实现多生产多消费环形模型

        首先先定义一个环形队列,逻辑和上面自定义队列的类是一样的,只不过环形队列用的是vector来当作容器,并且使用信号量约束生产者和消费者,代码如下:

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <pthread.h>

const static int defaultcap = 5;

template<class T>
class RingQueue{
private:
    void P(sem_t &sem)//--信号量
    {
        sem_wait(&sem);
    }
    void V(sem_t &sem)//++信号量
    {
        sem_post(&sem);
    }
    void Lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    void Unlock(pthread_mutex_t &mutex)
    {
        pthread_mutex_unlock(&mutex);
    }
public:
    RingQueue(int cap = defaultcap)
    :ringqueue_(cap), cap_(cap), c_step_(0), p_step_(0)
    {
        sem_init(&cdata_sem_, 0, 0);
        sem_init(&pspace_sem_, 0, cap);

        pthread_mutex_init(&c_mutex_, nullptr);
        pthread_mutex_init(&p_mutex_, nullptr);
    }
    void Push(const T &in) // 生产
    {
        P(pspace_sem_);

        Lock(p_mutex_); // ?
        ringqueue_[p_step_] = in;
        // 位置后移,维持环形特性
        p_step_++;
        p_step_ %= cap_;
        Unlock(p_mutex_); 

        V(cdata_sem_);

    }
    void Pop(T *out)       // 消费
    {
        P(cdata_sem_);

        Lock(c_mutex_); // ?
        *out = ringqueue_[c_step_];
        // 位置后移,维持环形特性
        c_step_++;
        c_step_ %= cap_;
        Unlock(c_mutex_); 

        V(pspace_sem_);
    }
    ~RingQueue()
    {
        sem_destroy(&cdata_sem_);
        sem_destroy(&pspace_sem_);

        pthread_mutex_destroy(&c_mutex_);
        pthread_mutex_destroy(&p_mutex_);
    }
private:
    std::vector<T> ringqueue_;
    int cap_;

    int c_step_;       // 消费者下标
    int p_step_;       // 生产者下标

    sem_t cdata_sem_;  // 消费者关注的数据资源
    sem_t pspace_sem_; // 生产者关注的空间资源

    pthread_mutex_t c_mutex_;
    pthread_mutex_t p_mutex_;
};

         信号量的使用逻辑:生成一个数据,则生成者的信号量减减,消费者的信号量加加。相反,消费一个数据,则消费者的信号量减减,生成者的信号量加加。


        主函数实现生成消费数据的代码如下(注意这里的任务列表依旧用上文的任务类,只不过加了一个默认构造):

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include "sem.hpp"
#include "task.hpp"

using namespace std;

struct ThreadData
{
    RingQueue<Task> *rq;
    std::string threadname;
};

void *Productor(void *args)
{
    // sleep(3);
    ThreadData *td = static_cast<ThreadData*>(args);
    RingQueue<Task> *rq = td->rq;
    std::string name = td->threadname;
    int len = opers.size();
    while (true)
    {
        // 1. 获取数据
        int data1 = rand() % 10 + 1;
        usleep(10);
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);

        // 2. 生产数据
        rq->Push(t);
        cout << "生成任务完毕 : " << t.GetTask() << " : " << name << endl;

        sleep(1);
    }
    return nullptr;
}

void *Consumer(void *args)
{
    ThreadData *td = static_cast<ThreadData*>(args);
    RingQueue<Task> *rq = td->rq;
    std::string name = td->threadname;

    while (true)
    {
        // 1. 消费数据
        Task t;
        rq->Pop(&t);
       
        // 2. 处理数据
        t();
        cout << "消费者拿到一个任务 : " << t.GetTask() << " : " << 
        name << " 结果: " << t.GetResult() << endl;
        sleep(1);

    }
    return nullptr;
}

int main()
{
    srand(time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>(50);

    pthread_t c[5], p[3];

    for (int i = 0; i < 1; i++)
    {
        ThreadData *td = new ThreadData();
        td->rq = rq;
        td->threadname = "Productor-" + std::to_string(i);

        pthread_create(p + i, nullptr, Productor, td);
    }
    for (int i = 0; i < 1; i++)
    {
        ThreadData *td = new ThreadData();
        td->rq = rq;
        td->threadname = "Consumer-" + std::to_string(i);

        pthread_create(c + i, nullptr, Consumer, td);
    }

    for (int i = 0; i < 1; i++)
    {
        pthread_join(p[i], nullptr);
    }
    for (int i = 0; i < 1; i++)
    {
        pthread_join(c[i], nullptr);
    }

    return 0;
}

        运行结果:

结语

        以上就是关于生产消费者模型的讲解,生产消费者模型可以提高并发式执行程序的效率,他的核心点在于交易场所的设计,以及生产者和消费者访问场所的时机,若时机出现偏差则会导致线程对锁的竞争不公平。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!! 

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

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

相关文章

接口测试之测试原则、测试用例、测试流程详解

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、接口的介绍 软件测试中&#xff0c;常说的接口有两种&#xff1a;图形用户接口&#xff08;GUI&#xff0c;人与程序的接口&#xff09;、应用程序编程接口&…

AWS云服务器购买:亚马逊云服务器的价格真的那么贵吗?一年要花多少钱?

亚马逊云服务器是全球领先的云计算服务提供商之一&#xff0c;其服务覆盖全球多个地区&#xff0c;拥有众多的客户和合作伙伴。然而&#xff0c;对于很多人来说&#xff0c;AWS的价格一直是一个热门话题。那么&#xff0c;亚马逊云服务器的价格真的那么贵吗&#xff1f;一年要花…

python爬虫Selenium模块及测试案例详解

什么是selenium&#xff1f; &#xff08;1&#xff09;Selenium是一个用于Web应用程序测试的工具。 &#xff08;2&#xff09;Selenium 测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。 &#xff08;3&#xff09;支持通过各种driver&#xff08;FirfoxDrive…

粘包问题、mmap和分片上传

一、粘包问题&#xff1a; 如果一端要把文件发给另一端&#xff0c;要发送两个部分的数据&#xff1a;其一是文件名&#xff0c;用于对端创建文件&#xff1b;另一个部分是文件内容。服务端在接收文件名&#xff0c;实际上并不知道有多长&#xff0c; 所以它会试图把网络缓冲区…

怎么把照片变漫画?学会这几招让照片秒变漫画

在这个追求图片创意与趣味性的时代&#xff0c;照片的“变身”游戏正悄然风靡。 从滤镜的巧妙运用到拍摄姿势的创新突破&#xff0c;人们不断探索着让照片焕发新生的无限可能。 而今&#xff0c;一股将照片转化为漫画风格的新潮流正席卷而来&#xff0c;它不仅保留了照片的记…

【PPT方案】大数据湖建设方案

背 景&#xff1a;大数据湖的发展背景与建设理念 体 系&#xff1a;大数据湖体系规划与建设思路 生态圈&#xff1a;探索新兴业务入湖建设模式 共 享&#xff1a;大数据湖统一访问共享规划 运 营&#xff1a;大数据湖一体化运营管理建设 软件全套资料部分文档清单&…

恐怖数字暗影:猜中才能逃离

大家可以看看这个&#xff0c;也很有意思&#xff01; 猜数字游戏&#xff08;老六版&#xff09;-CSDN博客 1、 剧情介绍 在一个阴暗潮湿的古堡中&#xff0c;你独自一人走进了一间散发着诡异气息的房间。房间的正中央有一张古老的桌子&#xff0c;上面放着一本泛黄的羊皮卷…

Java二十三种设计模式-装饰器模式(7/23)

装饰器模式&#xff1a;动态扩展功能的灵活之选 引言 装饰器模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;用于在不修改对象自身的基础上&#xff0c;通过添加额外的职责来扩展对象的功能。 基础知识&#xff0c;java设计模式总体来说设计…

MT19937

MT19937 文章目录 MT19937题型1 逆向extract_number[SUCTF2019]MT 题型2 预测随机数[GKCTF 2021]Random 题型3逆向twist[V&N2020 公开赛]Backtrace 题型4 逆向init扩展题型WKCTF easy_random 现成模块randcrack库Extend MT19937 Predictor库 MT19937是一种周期很长的伪随机…

【C++】深刻基础笔记

目录 关于debug&#xff1a; 多文件编译&#xff1a; 编译器工作原理 预处理&#xff1a; 如何将机器二进制转换成可以看懂的文件 链接器的工作原理 Pointers指针 Reference引用 C线程 程序如何从源文件变成exe可执行文件&#xff1f; 首先是预处理器#include <..…

斐波那契数列的多种解法 C++实现,绘图部分用Python实现

斐波那契数列的多种解法 C实现&#xff0c;绘图部分用Python实现 flyfish 斐波那契数列&#xff08;Fibonacci sequence&#xff09;是一个经典的数列&#xff0c;定义如下&#xff1a; { 0 if n 0 1 if n 1 F ( n − 1 ) F ( n − 2 ) if n > 1 \begin{cases} 0 &…

c++网络编程实战——开发基于ftp协议的文件传输模块(二) 配置ftp服务与手动执行ftp命令

配置FTP服务 一.前言 博主的环境是阿里云服务器&#xff0c;操作系统版本为 ubuntu20.04,一下所有操作都基于以上环境下进行的操作&#xff0c;同时为了简化操作我将开放同一个云服务器的不同端口&#xff0c;让它同时充当服务端和客户端&#xff0c;大家如果想测试效果更好且…

Java基础:类与对象,递归,方法

类与对象的区别和联系 1.类是抽象的,概念的他是数据类型 2.对象是具体的实际的,代表一个具体事务 3.类是对象的模板,对象是类的个体 **对象在内存中的存在形式** 基本数据类型在堆,引用类型如String,数组在方法区 对象的属性 属性基本说明 1.成员变量or成员属性 属性的定…

双向链表<数据结构 C版>

目录 关于链表的分类 双向链表结构体 初始化 尾插 头插 打印 判断是否为空 尾删 头删 查找 指定位置之后的插入 指定位置的删除 销毁 关于链表的分类 根据链表的三大特性&#xff0c;单向or双向、带头or不带头、循环or不循环&#xff0c;可将链表分为2*2*2&#xf…

利用ascp下载SRA Explorer中转录组数据

最近在windows系统里下载了MobaXterm可以远程登入服务器&#xff0c;处理RNA的数据&#xff0c;需要从NCBI数据库上下载数据。本文提供用虚拟机ubuntu或者linux系统下载Aspera的方法和问题解决&#xff0c;以及从NCBI上批量下载数据库、最后得到一个项目里的所有fastq文件。 A…

前端:Vue学习-2

前端&#xff1a;Vue学习-2 1. vue的生命周期2. 工程化开发和脚手架Vue CLI2.1 组件化开发2.2 scoped解决样式冲突2.3 data是一个函数2.4 组件通信2.5 非父子通信- event bus事件&#xff0c;provide&inject 3.v-model原理->实现父子组件双向绑定4. sync 修饰符->实现…

谷粒商城实战笔记-42-前端基础-Vue-生命周期和钩子函数

下面是Vue官网关于生命周期及不同阶段的钩子函数的图示。 Vue 实例生命周期是指从一个组件被创建到最终被销毁的整个过程。 在这一过程中&#xff0c;Vue 提供了一系列的“钩子”函数&#xff0c;在生命周期的不同阶段执行自定义的代码。 以下是 Vue 对象生命周期的主要阶段…

C语言实现二叉树以及二叉树的详细介绍

目录 1.树概念及结构 1.1树的概念 1.2树的相关概念 1.3树的表示 2.二叉树概念及结构 2.1二叉树的概念 2.2特殊的二叉树 2.3二叉树的性质 2.4二叉树的存储结构 3.二叉树顺序结构--特殊的二叉树--堆及其实现 3.1堆的概念及结构 3.2堆的实现 3.2.1堆的结构 3.2.2堆…

filebeat生产环境配置

配置文件属性 生产配置 filebeat.inputs: - type: logenabled: truepaths: - /tmp/logs/*.log- /var/log/system.log- /var/log/wifi.logsymlinks: truejson.keys_under_root: truejson.message_key: xxxjson.add_error_key: true# 如果想卡部分日志&#xff0c;比如用时间作…

Monaco 使用 HoverProvider

Monaco 中自定义 Hover&#xff0c;Hover 效果是指当鼠标移动文字上展示出提示效果&#xff0c;就像页面上的 Tooltip 效果。最终页面的显示效果如下&#xff1a; 通过 registerHoverProvider 注册 Hover 触发时的处理方法 接口中提供了 4 个参数&#xff0c;前两个参数比较重…