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

news2025/2/25 0:20:08

线程同步

条件变量

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

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

同步概念

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

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

条件变量函数

初始化

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.cc
	g++ -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.cc
	g++ -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.cc
	g++ -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.coloradmin.cn/o/2304622.html

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

相关文章

Docker Mysql 数据迁移

查看启动命令目录映射 查看容器名称 docker ps查看容器的启动命令 docker inspect mysql8.0 |grep CreateCommand -A 20如下图所示:我这边是把/var/lib/mysql 目录映射到我宿主机的/mnt/mysql/data目录下,而且我的数量比较大使用方法1的话时间比较久,所以我采用方法2 如果没…

四步彻底卸载IDEA!!!

各位看官早安午安晚安呀 如果您觉得这篇文章对您有帮助的话 欢迎您一键三连&#xff0c;小编尽全力做到更好 欢迎您分享给更多人哦 大家好&#xff0c;我们今天来学习四步彻底卸载IDEA&#xff01;&#xff01;&#xff01; 首先我要提醒各位 如果你想删除 IDEA 相关&#xf…

HTTP实验(ENSP模拟器实现)

HTTP概述 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;&#xff0c;设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。 HTTP定义了多种请求方法&#xff0c;常用的包括&#xff1a; GET&#xff1a;请求资源。 POST&…

【网络安全】常见的web攻击

1、SQL注入攻击 定义&#xff1a; 攻击者在HTTP请求中注入恶意的SQL代码&#xff0c;当服务器利用参数构建SQL语句的时候&#xff0c;恶意的SQL代码被一起构建,并在数据库中执行。 示例&#xff1a; 用户登录&#xff1a; 输入用户名xx&#xff0c; 密码 or 1 …

登录-05.JWT令牌-介绍

一.JWT令牌 JWT令牌是一种简洁的、自包含的格式&#xff0c;用于在通讯双方之间以json数据格式安全的传输数据。说白了&#xff0c;JWT令牌就是将json格式的数据进行封装&#xff0c;从而实现安全传输。 所谓简洁&#xff0c;就是指JWT令牌就是一个简单的字符串。 所谓自包含…

K8S下redis哨兵集群使用secret隐藏configmap内明文密码方案详解

#作者&#xff1a;朱雷 文章目录 一、背景环境及方案说明1.1、环境说明1.2、方案一&#xff1a;使用配置文件设置密码1.3、方案二&#xff1a;使用args 的命令行传参设置密码 二、redis secret configmap deployment参考2.1 创建secret-redis.yaml参考2.2 修改configmap配置参…

Spring框架基本使用(Maven详解)

前言&#xff1a; 当我们创建项目的时候&#xff0c;第一步少不了搭建环境的相关准备工作。 那么如果想让我们的项目做起来方便快捷&#xff0c;应该引入更多的管理工具&#xff0c;帮我们管理。 Maven的出现帮我们大大解决了管理的难题&#xff01;&#xff01; Maven&#xf…

鸿蒙NEXT应用App测试-专项测试(DevEco Testing)

注意&#xff1a;大家记得先学通用测试在学专项测试 鸿蒙NEXT应用App测试-通用测试-CSDN博客 注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注…

Rocky8 源码安装 HAProxy

HAProxy 是一款开源的高性能 负载均衡器 和 反向代理 软件&#xff0c;专注于处理高并发流量分发&#xff0c;广泛应用于企业级架构中提升服务的可用性、扩展性和安全性。 一、HAProxy 简介 1.1.HAProxy 是什么&#xff1f; 本质&#xff1a; 基于 C 语言开发 的轻量级工具&a…

【从0做项目】Java文档搜索引擎(9)烧脑终章!

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 文章导读 零&#xff1a;项目结果展示 一&#xff1a;导入 二&#xff1a;问题引入 1&#xff1a;情…

Linux 第二次脚本作业

1、需求&#xff1a;判断192.168.1.0/24网络中&#xff0c;当前在线的ip有哪些&#xff0c;并编写脚本打印出来。 2、设计一个 Shell 程序&#xff0c;在/userdata 目录下建立50个目录&#xff0c;即 user1~user50&#xff0c;并设置每个目录的权限&#xff0c;其中其他用户的权…

mysql的源码包安装

安装方式一&#xff1a;&#xff08;编译好的直接安装&#xff09; 1.添加一块10G的硬盘&#xff0c;给root逻辑卷扩容 &#xff08;下面安装方式二有&#xff0c;一模一样的装就行&#xff0c;我就不写了&#xff0c;再写的话篇幅就太长了&#xff09; 2.下载编译好的源码包…

#渗透测试#批量漏洞挖掘#畅捷通T+远程命令执行漏洞

免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停止本文章读。 目录 一、漏洞概况 二、攻击特征 三、应急处置…

【2024 CSDN博客之星】大学四年,我如何在CSDN实现学业与事业的“双逆袭”?

前言&#xff1a; Hello大家好&#xff0c;我是Dream。不知不觉2024年已经过去&#xff0c;自己也马上迈入23岁&#xff0c;感慨时间飞快&#xff0c;从19岁刚入大学加入CSDN&#xff0c;到现在大学毕业已经整整四年了。CSDN陪伴我走过了最青涩的四年大学时光&#xff0c;在这里…

06排序 + 查找(D1_排序(D1_基础学习))

目录 学习预热&#xff1a;基础知识 一、什么是排序 二、为什么要排序 三、排序的稳定性 四、排序稳定性的意义 五、排序分类方式 方式一&#xff1a;内外分类 方式二&#xff1a;比较分类 六、排序算法性能评估 1. 算法的时间复杂度 2. 算法的空间复杂度 七、知识小…

【数据挖掘】深度挖掘

【数据挖掘】深度挖掘 目录&#xff1a;1. 减少样本集的数量知识点示例 2. 对噪声比集剪枝知识点示例建立局部树代码示例&#xff08;使用 Python 和 scikit - learn 库构建局部决策树&#xff09;代码解释注意事项 最大超平面定义原理求解方法代码示例&#xff08;使用 Python…

【Linux】基于UDP/TCP套接字编程与守护进程

目录 一、网路套接字编程 &#xff08;一&#xff09;基础概念 1、源IP地址与目的IP地址 2、端口号 3、TCP与UDP 4、网络字节序 &#xff08;二&#xff09;套接字编程接口 1、socket 常见API 2、sockaddr结构 &#xff08;三&#xff09;UDP套接字 1、UDP服务器创建…

C++跳表实现,封装成Skiplist类

跳表 (Skip List) 是由 William Pugh 发明的一种查找数据结构&#xff0c;支持对数据的快速查找&#xff0c;插入和删除。 跳表的期望空间复杂度为O(n) &#xff0c;跳表的查询&#xff0c;插入和删除操作的期望时间复杂度都为O(logn)。 算法讲解149【扩展】有序表专题2-跳表_哔…

【uni-app】对齐胶囊容器组件

代码碎片 <template><div><view :style"{ height: ${statusBarHeight}px }"></view><viewclass"":style"{height: ${menuButtonHeight menuButtonPadding * 2}px,width: ${menuButtonInfo.left}px,}"><slot …

计算机毕业设计SpringBoot+Vue.jst0甘肃非物质文化网站(源码+LW文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…