【Linux】线程封装 | 线程互斥 | 基于阻塞队列的生产消费者模型

news2024/11/29 20:40:13

文章目录

  • 一、线程封装
  • 二、Linux线程互斥
    • 进程线程间的互斥相关背景概念
    • 互斥量mutex
    • 为什么上面的抢票代码可能无法获得正确结果?(票数为负)
    • 互斥量的接口
      • 1. 初始化互斥量
        • 方法一:静态分配
        • 方法二:动态分配:
      • 2. 销毁互斥量
      • 3. 互斥量加锁和解锁
  • 三、常见锁概念
    • 死锁
      • 死锁四个必要条件
      • 避免死锁的方法
      • 避免死锁的算法
  • 四、生产消费者模型
    • 为何要使用生产者消费者模型?
    • 生产者消费者模型优点
    • 基于BlockingQueue的生产者消费者模型
      • BlockingQueue是什么
      • C++ queue模拟阻塞队列的生产消费模型

一、线程封装

模拟封装C++11的thread:

#pragma once

#include <pthread.h>
#include <iostream>
#include <string>
#include <functional>

template<class T>
using func_t = std::function<void(T)>;

template<class T>
class Thread
{
public:
    Thread(const std::string& threadname, func_t<T> func, T data)
        :_tid(0)
        , _isrunning(false)
        , _threadname(threadname)
        , _func(func)
        , _data(data)
    {}

    static void* ThreadRoutine(void* args) // 类内方法,隐含了第一个形参this,改成static并把args作为this传
    {
        (void)args; // 使用一下,避免编译器告警

        Thread* ts = static_cast<Thread*>(args);
        ts->_func(ts->_data);
        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
        if (n == 0)
        {
            _isrunning = true;
            return true;
        }
        else
        {
            return false;
        }
    }

    bool Join()
    {
        if (!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if (n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }

    std::string ThreadName()
    {
        return _threadname;
    }

    bool IsRunning()
    {
        return _isrunning;
    }

    ~Thread() = default;
private:
    pthread_t _tid;
    std::string _threadname;
    bool _isrunning;
    func_t<T> _func;
    T _data;
};

主文件:

  1. 创建一个线程
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"

std::string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread - %d", number++);
    return name;
}

void Print(int num) // 传一个打印次数
{
    while (true)
    {
        std::cout << "hello world:" << num-- << std::endl;
        sleep(1);
    }
}

int main()
{
    Thread<int> t(GetThreadName(), Print, 10);
    std::cout << "Is thread running? " << t.IsRunning() << std::endl;
    t.Start();
	
    std::cout << "Is thread running? " << t.IsRunning() << std::endl;
	
    t.Join();
    return 0;
}

请添加图片描述

  1. 模拟一个多线程抢票的情况:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"

std::string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread - %d", number++);
    return name;
}

// 不加锁的版本
int ticket = 10000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void GetTicket(std::string name)
{
    while (true)
    {
        if (ticket > 0)
        {
            usleep(100); // 充当抢票花费的时间
            printf("%s get a ticket: %d \n", name.c_str(), ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    std::string name1 = GetThreadName();
    Thread<std::string> t1(name1, GetTicket, name1);
	
    std::string name2 = GetThreadName();
    Thread<std::string> t2(name2, GetTicket, name2);
	
    std::string name3 = GetThreadName();
    Thread<std::string> t3(name3, GetTicket, name3);
	
    std::string name4 = GetThreadName();
    Thread<std::string> t4(name4, GetTicket, name4);
	
    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();
	
    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();
}

为什么减到0了???我们设置的是ticket > 0停止了啊!因为线程未加锁。


请添加图片描述


二、Linux线程互斥

进程线程间的互斥相关背景概念

  • 临界资源:多线程执行流共享的公共资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用,串行访问!
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

为什么上面的抢票代码可能无法获得正确结果?(票数为负)

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep 模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • --ticket 操作本身就不是一个原子操作,因为-- 操作并不是原子操作,而是对应三条汇编指令:
    load :将共享变量ticket从内存加载到寄存器中
    update : 更新寄存器里面的值,执行-1操作
    store :将新值,从寄存器写回共享变量ticket的内存地址
      400fc7:	8b 05 17 21 20 00    	mov    0x202117(%rip),%eax        # 6030e4 <ticket>
      400fcd:	83 e8 01             	sub    $0x1,%eax
      400fd0:	89 05 0e 21 20 00    	mov    %eax,0x20210e(%rip)        # 6030e4 <ticket>
    

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到以上这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

请添加图片描述


互斥量的接口

1. 初始化互斥量

初始化互斥量有两种方法:

方法一:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
					   const pthread_mutexattr_t *restrictattr);   
参数:
	mutex:要初始化的互斥量
	attr:NULL

2. 销毁互斥量

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

3. 互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_mutex_lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么 pthread_mutex_lock 调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

我们在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程继续竞争锁。

改进的售票系统:

// 加锁版本
// 加锁:
// 1. 我们要尽可能给少的代码块加锁,加锁牺牲效率,提高安全性
// 2. 一般加锁,都是给临界区加锁

int ticket = 10000; // 全局共享
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 全局锁

void GetTicket(std::string name)
{
    while (true)
    {
        // 2. 是由程序员自己保证的!规则都必须先申请锁
        // 3. 根据互斥的定义,任何时刻,只允许一个线程申请锁成功!
        // 多个线程申请锁失败,失败的线程怎么办?在mutex上进行阻塞,本质就是等待!
        pthread_mutex_lock(&mutex); // 1. 申请锁本身是安全的,原子的,为什么?因为申请锁的底层汇编指令只有一条
        if (ticket > 0) // 4. 一个线程在临界区中访问临界资源的时候,可不可能发生切换? 可能!
        {
            usleep(100); // 充当抢票花费的时间
            printf("%s get a ticket: %d \n", name.c_str(), ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

请添加图片描述


三、常见锁概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

单执行流可能产生死锁吗?


单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。

例如,在下面的代码中我们让主线程创建的新线程连续申请了两次锁。

#include <stdio.h>
#include <pthread.h>
 
pthread_mutex_t mutex;
void* Routine(void* arg)
{
   pthread_mutex_lock(&mutex);
   pthread_mutex_lock(&mutex);
   pthread_exit((void*)0);
}

int main()
{
   pthread_t tid;
   pthread_mutex_init(&mutex, NULL);
   pthread_create(&tid, NULL, Routine, NULL);
   pthread_join(tid, NULL);
   pthread_mutex_destroy(&mutex);
   return 0;
}

请添加图片描述

运行代码,此时该程序实际就处于一种被挂起的状态。用ps命令查看该进程时可以看到,该进程当前的状态是Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。

例如,当某一个进程在被CPU调度时,该进程需要用到锁的资源,但是此时锁的资源正在被其他进程使用:

  • 那么此时该进程的状态就会由R状态变为某种阻塞状态,比如S状态。并且该进程会被移出运行等待队列,被链接到等待锁的资源的资源等待队列,而CPU则继续调度运行等待队列中的下一个进程。
  • 此后若还有进程需要用到这一个锁的资源,那么这些进程也都会被移出运行等待队列,依次链接到这个锁的资源等待队列当中。
  • 直到使用锁的进程已经使用完毕,也就是锁的资源已经就绪,此时就会从锁的资源等待队列中唤醒一个进程,将该进程的状态由S状态改为R状态,并将其重新链接到运行等待队列,等到CPU再次调度该进程时,该进程就可以使用到锁的资源了。

锁本质就是一种软件资源,当我们申请锁时,锁当前可能并没有就绪,可能正在被其他线程所占用,此时当其他线程再来申请锁时,就会被放到这个锁的资源等待队列当中。


死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。

避免死锁的方法

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁的算法

  • 死锁检测算法(了解)
  • 银行家算法(了解)

四、生产消费者模型

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

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

请添加图片描述

三种关系:

  • 生产者之间是什么关系?竞争 - 互斥
  • 消费者之间是什么关系?竞争 - 互斥
  • 生产和消费者之间是什么关系?互斥 && 同步

“321”原则:

  • 3:三种关系
  • 2:两种角色,生产者(1 or n),消费者(1 or n),他们可以是线程或进程
  • 1:一个交易场所:内存空间

生产者消费者模型优点

基于阻塞队列的生产者消费者模型具备以下优点:

  • 解耦
  • 支持并发
  • 支持忙闲不均
  1. 解耦

    • 阻塞队列作为生产者和消费者之间的缓冲区,使它们能够独立运行,不需要直接相互通信。
    • 生产者和消费者只需与队列进行交互,而无需了解对方的存在或状态。
    • 这种解耦性简化了系统的设计和维护,使得生产者和消费者之间的关系更加松散,降低了耦合度。
  2. 支持并发

    • 阻塞队列通常是线程安全的数据结构,能够支持多个生产者和消费者并发地对其进行读写操作。
    • 多个线程可以同时向队列中添加数据或者从队列中取出数据,而不会出现数据不一致或竞争条件等并发问题。
  3. 支持忙闲不均

    • 当生产者和消费者的速度不匹配时,阻塞队列能够缓解生产者和消费者之间的压力差异。
    • 当生产者生产速度过快或消费者消费速度过慢时,队列可以暂时存储数据,避免生产者因为没有消费者而被阻塞,或者消费者因为没有生产者而处于忙碌状态。
    • 阻塞队列可以根据需求设置不同的容量,以调节生产者和消费者之间的速度差异,使系统能够更好地适应不同的负载情况。

如果我们在主函数中调用某一函数,那么我们必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种紧耦合。

对应到生产者消费者模型中,函数传参实际上就是生产者生产的过程,而执行函数体实际上就是消费者消费的过程,但生产者只负责生产数据,消费者只负责消费数据,在消费者消费期间生产者可以同时进行生产,因此生产者消费者模型本质是一种松耦合。


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

BlockingQueue是什么

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

C++ queue模拟阻塞队列的生产消费模型

为了方便理解,可以先以单生产者、单消费者为例进行实现,然后再增加生产者和消费者数量:请添加图片描述

其中的BlockQueue就是生产者消费者模型当中的交易场所,我们可以用C++STL库当中的queue进行实现。

Makefile

bq:Main.cc
    g++ -o bq Main.cc -std=c++11 -lpthread
.PHONY:clean
clean:
    rm -f bq;

主文件Main.cc

#include "BlockQueue.hpp"
#include "LockGuard.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>

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

    while (true)
    {
        Task t;
        // 1. 消费数据
        bq->Pop(&t);

        t();
        std::cout << "productor data: " << t.PrintResult() << std::endl;

        // 注意:消费者没有sleep!!!
    }

    return nullptr;
}

void* productor(void* args)
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);
    while (true)
    {
        // 1. 有数据
        // 生产前,你的任务从哪里来的呢???
        int data1 = rand() % 10; // [1, 10] // 将来深刻理解生产消费,就要从这里入手,TODO
        usleep(rand() % 123);
        int data2 = rand() % 10; // [1, 10] // 将来深刻理解生产消费,就要从这里入手,TODO
        usleep(rand() % 123);
        char oper = operators[rand() % (operators.size())];
        Task t(data1, data2, oper);
        std::cout << "productor task: " << t.PrintTask() << std::endl;

        // 2. 进行生产
        bq->Push(t);
    }
}

int main()
{
    srand((uint16_t)time(NULL) ^ getpid() ^ pthread_self());
    BlockQueue<Task>* bq = new BlockQueue<Task>();

    pthread_t c[3], p[2]; // 生产者和消费者

    pthread_create(&c[0], nullptr, consumer, bq);
    pthread_create(&c[1], nullptr, productor, bq);
    pthread_create(&c[2], nullptr, productor, bq);
    pthread_create(&p[0], nullptr, consumer, bq);
    pthread_create(&p[1], nullptr, productor, bq);

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(c[2], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);

    return 0;
}

阻塞队列的实现BlockQueue.hpp

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include "LockGuard.hpp"

const int defalutcap = 5;

template<class T>
class BlockQueue
{
public:
    BlockQueue(int cap = defalutcap)
        :_capacity(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }

    bool IsEmpty()
    {
        return _q.size() == 0;
    }

    void Push(const T& in) // 生产者的
    {
        pthread_mutex_lock(&_mutex);

        if (IsFull())
        {
            // 阻塞等待
            pthread_cond_wait(&_p_cond, &_mutex);
        }
        _q.push(in);

        pthread_cond_signal(&_c_cond);
        pthread_mutex_unlock(&_mutex);

    }

    void Pop(T* out) // 消费者的
    {
        pthread_mutex_lock(&_mutex);

        if (IsEmpty())
        {
            // 阻塞等待
            pthread_cond_wait(&_c_cond, &_mutex);
        }
        *out = _q.front();

        _q.pop();

        pthread_cond_signal(&_p_cond);
        pthread_mutex_unlock(&_mutex);

    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }

private:
    std::queue<T> _q;
    int _capacity; // _q.size() == _capacity
    pthread_mutex_t _mutex;
    pthread_cond_t _p_cond; // 给生产者的条件变量,有数据了来叫它
    pthread_cond_t _c_cond; // 给消费者的条件变量,有空间了来叫它

    int _consumer_water_line;
    int _productor_water_line;
};

生产消费的任务类Task.hpp

#pragma once
#include <iostream>
#include <string>

const int defaultvalue = 0;

enum
{
    ok = 0,
    div_zero = 1,
    mod_zero = 2,
    unknown = 3
};

const std::string operators = "+-*/%)(&";

class Task
{
public:
    Task() = default;
    ~Task() = default;

    Task(int x, int y, char op)
        : data_x(x)
        , data_y(y)
        , _operator(op)
        , result(defaultvalue)
        , code(ok)
    {}

    void Run()
    {
        switch (_operator)
        {
        case '+':
            result = data_x + data_y;
            break;
        case '-':
            result = data_x - data_y;
            break;
        case '*':
            result = data_x * data_y;
            break;
        case '/':
        {
            if (data_y == 0)
            {
                code = div_zero;
            }
            else
            {
                result = data_x / data_y;
            }
        }
        break;

        case '%':
        {
            if (data_y == 0)
            {
                code = mod_zero;
            }
        }
        break;

        default:
            code = unknown;
            break;

        }
    }

    void operator()()
    {
        Run();
    }

    std::string PrintTask()
    {
        std::string s;
        s = std::to_string(data_x);
        s += _operator;
        s += std::to_string(data_y);
        s += "=?";

        return s;
    }

    std::string PrintResult()
    {
        std::string s;
        s = std::to_string(data_x);
        s += _operator;
        s += std::to_string(data_y);
        s += "=";
        s += std::to_string(result);
        s += " [";
        s += std::to_string(code);
        s += "]";

        return s;
    }

private:
    int data_x;
    int data_y;
    char _operator;

    int result;
    int code;  // 结果码,0:结果可信 !0:结果不可信
};

锁的生命周期管理LockGuard.hpp

#pragma once

#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock)
        :_lock(lock)
    {}

    void Lock()
    {
        pthread_mutex_lock(_lock);
    }

    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }

    ~Mutex() = default;
private:
    pthread_mutex_t* _lock;
};


class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock)
        :_mutex(lock)
    {
        _mutex.Lock();
    }

    ~LockGuard()
    {
        _mutex.Unlock();
    }

private:
    Mutex _mutex;
};

相关说明:

  • 阻塞队列是会被生产者和消费者同时访问的临界资源,因此我们需要用一把互斥锁将其保护起来。
  • 生产者线程要向阻塞队列当中Push数据,前提是阻塞队列里面有空间,若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
  • 消费者线程要从阻塞队列当中Pop数据,前提是阻塞队列里面有数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。
  • 因此在这里我们需要用到两个条件变量,一个条件变量用来描述队列为空,另一个条件变量用来描述队列已满。当阻塞队列满了的时候,要进行生产的生产者线程就应该在full条件变量下进行等待;当阻塞队列为空的时候,要进行消费的消费者线程就应该在empty条件变量下进行等待。
  • 不论是生产者线程还是消费者线程,它们都是先申请到锁进入临界区后再判断是否满足生产或消费条件的,如果对应条件不满足,那么对应线程就会被挂起。但此时该线程是拿着锁的,为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
  • 当生产者生产完一个数据后,意味着阻塞队列当中至少有一个数据,而此时可能有消费者线程正在empty条件变量下进行等待,因此当生产者生产完数据后需要唤醒在empty条件变量下等待的消费者线程。
  • 同样的,当消费者消费完一个数据后,意味着阻塞队列当中至少有一个空间,而此时可能有生产者线程正在full条件变量下进行等待,因此当消费者消费完数据后需要唤醒在full条件变量下等待的生产者线程。

生产者消费者步调一致

运行代码后我们可以看到生产者和消费者的执行步调是一致的:请添加图片描述

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

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

相关文章

性能优化(CPU优化技术)-NEON指令详解

原文来自ARM SIMD 指令集&#xff1a;NEON 简介 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;高性能&#xff08;HPC&#xff09;开发基础教程 &#x1f380;CSDN主页 发狂的小花 &#x1f304;人生秘诀&#xf…

如何让自己上百度百科?个人百科词条创建

百度百科&#xff0c;作为我国最大的中文百科全书&#xff0c;其影响力和权威性不言而喻。能够登上百度百科&#xff0c;意味着个人的知名度、成就和社会影响力得到了广泛认可。那么&#xff0c;如何才能让自己上百度百科呢&#xff1f;接下来伯乐网络传媒就来给大家讲解一下。…

HarmonyOS NEXT应用开发之Navigation实现多设备适配案例

介绍 在应用开发时&#xff0c;一个应用需要适配多终端的设备&#xff0c;使用Navigation的mode属性来实现一套代码&#xff0c;多终端适配。 效果图预览 使用说明 将程序运行在折叠屏手机或者平板上观看适配效果。 实现思路 本例涉及的关键特性和实现方案如下&#xff1a…

[AIGC] 在Spring Boot中指定请求体格式

在使用Spring Boot开发Web应用的时候&#xff0c;我们经常会遇到需要接收并处理HTTP请求的情况。一个HTTP请求通常包括一个请求行、若干请求头和一个请求体。请求体在POST和PUT请求中特别重要&#xff0c;因为它通常用于向服务器传递数据。 文章目录 创建并使用一个Java Bean指…

分布式搜索引擎elasticsearch专栏三

1.数据聚合 聚合&#xff08;aggregations&#xff09;可以让我们极其方便的实现对数据的统计、分析、运算。例如&#xff1a; 什么品牌的手机最受欢迎&#xff1f; 这些手机的平均价格、最高价格、最低价格&#xff1f; 这些手机每月的销售情况如何&#xff1f; 实现这些…

3.19作业

1、思维导图 2、模拟面试题 1&#xff09;TCP通信中的三次握手和四次挥手 答&#xff1a;三次握手 客户端向服务器发送连接请求 服务器向客户端回复应答并向客户端发送连接请求 客户端回复服务端&#xff0c;并建立联系 四次挥手 进程a向进程b发送断开连接请求…

vue axios 缓存 接口请求实现缓存加载

文章写的多了&#xff0c;开头就不知道怎么写了&#xff0c;硬挤一些句子总觉的卖弄。其实更多的想留下各位看官&#xff0c;多多的点赞&#xff0c;多多的关注&#xff0c;多的收藏。为将来的博客化动作做好前期数据粉丝基础。哦哦哦&#xff0c;我在想啥呢。。这大下午的。。…

软件工程-第11章 内容总结

如果不想读这本书&#xff0c;直接看这一章即可。 11.1 关于软件过程范型 11.2 关于软件设计方法

java算法第28天 | 93.复原IP地址 78.子集 90.子集II

93.复原IP地址 思路&#xff1a; 这里startIndex为插入‘.’的位置&#xff0c;使用回溯法遍历所有插入的位置&#xff0c;直接在原始字符串上操作。要注意的是开闭区间的规定&#xff08;这里我规定的是左闭右闭区间&#xff09;。还要明确什么时候能return。 class Solution…

在 vite 开发环境,使用https自签证书 --- mkcert

在 vite 开发环境&#xff0c;使用https自签证书 — mkcert 使用basicSsl&#xff08;vitejs/plugin-basic-ssl&#xff09; 在vite开发环境中&#xff0c;使用 basicSsl 插件能暂时提供https服务&#xff0c;同时&#xff0c;也会面临总是提示一下的问题,如下图 提示https证…

2024.3.20 使用maven打包jar文件和保存到本地仓库

2024.3.20 使用maven打包jar文件和保存到本地仓库 使用maven可以很方便地打包jar文件和导入jar文件&#xff0c;同时还可以将该文件保存在本地仓库重复调用。 使用maven打包jar文件和保存到本地仓库 package打包文件。 install导入本地仓库。 使用maven导入jar文件 点击“…

智能合约 之 部署ERC-20

Remix介绍 Remix是一个由以太坊社区开发的在线集成开发环境&#xff08;IDE&#xff09;&#xff0c;旨在帮助开发者编写、测试和部署以太坊智能合约。它提供了一个简单易用的界面&#xff0c;使得开发者可以在浏览器中直接进行智能合约的开发&#xff0c;而无需安装任何额外的…

WPF —— 控件模版和数据模版

1:控件模版简介: 自定义控件模版&#xff1a;自己添加的样式、标签&#xff0c;控件模版也是属于资源的一种&#xff0c; 每一个控件模版都有一唯一的 key&#xff0c;在控件上通过template属性进行绑定 什么场景下使用自定义控件模版&#xff0c;当项目里面多个地方…

基于python考试分析系统的设计和实现-flask-django-nodejs-php

随着电子技术的普及和快速发展&#xff0c;线上管理系统被广泛的使用&#xff0c;有很多商业机构都在实现电子信息化管理&#xff0c;图书推荐也不例外&#xff0c;由比较传统的人工管理转向了电子化、信息化、系统化的管理。   本文的重点是对考试分析系统展开了详细的描述&a…

整型数组按个位值排序 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 给定一个非空数组(列表)&#xff0c;其元素数据类型为整型&#xff0c;请按照数组元素十进制最低位从小到大进行排序&#xff0c;十进制最低位相同的元素&#xf…

英伟达GTC2024大会开幕,发布机器人003计划,引领具身智能新时代

一、背景 在全球科技创新的前沿阵地&#xff0c;2024年3月的英伟达GPU技术大会&#xff08;GTC&#xff09;再次成为全球瞩目的焦点。在此次盛会上&#xff0c;英伟达公司创始人兼首席执行官黄仁勋先生不仅展示了其公司在加速计算和生成式AI领域的最新突破&#xff0c;更震撼发…

[Uni-app] 微信小程序的圆环进度条

效果图&#xff1a; 组件完整代码如下&#xff1a; <template><view class"base-style":style"position: relative;width: diameter px;height: diameter px;display: flex;flex-direction: row;background-color: bgColor ;"><!…

基于nodejs+vue班级管理系统的设计与实现-flask-django-python-php

随着电子技术的普及和快速发展&#xff0c;线上管理系统被广泛的使用&#xff0c;有很多事业单位和商业机构都在实现电子信息化管理&#xff0c;班级管理系统也不例外&#xff0c;由比较传统的人工管理转向了电子化、信息化、系统化的管理。随着互联网技术的高速发展&#xff0…

基于SpringBoot+Redis实现接口限流

前言 业务中需要对一些接口进行限流处理&#xff0c;防止机器人调用或者保证服务质量&#xff1b; 实现方式 基于redis的lua脚本 引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis&…

idea import的maven类报红

idea 报红/显示红色的原因 一般报红&#xff0c;显示红色&#xff0c;是因为 idea 在此路径下&#xff0c;找不到这个类。 找到是哪个 jar 包的类导致 idea 报红 点击报红的路径的上一层&#xff0c;进入jar 包。比如&#xff1a; import com.aaa.bbb.ccc.DddDto;这个 impo…