[Linux]------线程同步和信号量

news2024/11/17 17:30:59

文章目录

  • 前言
  • 一、条件变量
    • 同步概念与竞态条件
    • 条件变量函数
      • 初始化
      • 销毁
      • 等待条件满足
      • 唤醒等待
    • 为什么pthread_cond_wait需要互斥量?
    • 条件变量使用规范
  • 二、生产者消费者模型
    • 为何要使用生产者消费者模型
    • 生产者消费者模型的优点
    • 基于BlockingQueue的生产者消费者模型
    • C++ queue模拟阻塞队列的生产消费模型
  • 三、POSIX信号量
    • 函数
      • 初始化信号量
      • 销毁信号量
      • 等待信号量
      • 发布信号量
    • 基于环形队列的生产消费模型
  • 总结


前言

线程互斥,他是对的,但他不一定合理。因为互斥有可能导致饥饿问题。所谓饥饿问题就是一个执行流,长时间得不到某种资源。这小章我来基于条件变量带大家认识线程同步。


正文开始!

一、条件变量

  • 当一个线程互斥地访问某个变量时,他可能发现在其他线程改变状态之前,他什么也做不了。
  • 例如一个线程访问队列的时候,发现队列为空,他只能等待,直到其他线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效的避免饥饿问题,叫做同步
  • 竞态条件:因为时序为题,而导致程序异常,我们称之为竞态条件。在这种场景下,这种问题也不难理解

条件变量函数

初始化

唤醒线程由系统唤醒变为让程序员自己唤醒。
条件变量必须要和互斥锁mutex一同使用。

在这里插入图片描述

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

代码实现

#include <iostream>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
using namespace std;

//定义一个条件变量
pthread_cond_t cond;
//定义一个互斥锁,并且初始化
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

//定义全局退出变量
volatile bool quit=false;
void* waitCommand(void* args)
{
    while(!quit)
    {
        //执行了下面的代码,证明某一种条件不就绪(现在还没有场景),要我这个线程等待
        //三个线程,都会在条件变量下进行排队
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//让对应的线程进行等待,等待被唤醒
        cout<<"thread id "<<pthread_self()<<"run..."<<endl;
        pthread_mutex_unlock(&mutex);
    }
    cout<<"thread id "<<pthread_self()<<"end..."<<endl;
    return nullptr;
}


int main()
{
    pthread_cond_init(&cond,nullptr);
    pthread_t t1;
    pthread_t t2;
    pthread_t t3;
    pthread_create(&t1,nullptr,waitCommand,nullptr);
    pthread_create(&t2,nullptr,waitCommand,nullptr);
    pthread_create(&t3,nullptr,waitCommand,nullptr);

    while(true)
    {
        char n='a';
        cout<<"请输入你的command(n/q): ";
        cin>>n;
        if(n=='n')
            pthread_cond_broadcast(&cond);
            //pthread_cond_signal(&cond);
        else
        {
            quit=true;
            break; 
        }
        sleep(1);
    }
    pthread_cond_broadcast(&cond);

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

在这里插入图片描述

为什么pthread_cond_wait需要互斥量?

  • 条件等待是一个线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且有友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全地获取和修改共享数据。

条件(对应的共享资源的状态)—>程序员要判断资源是否满足自己操作的要求
条件变量(条件满足或者不满足的时候,进行wait或signal的一种方式)

在这里插入图片描述

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

	// 错误的设计
	pthread_mutex_lock(&mutex);
	while (condition_is_false) {
	pthread_mutex_unlock(&mutex);
	//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
	pthread_cond_wait(&cond);
	pthread_mutex_lock(&mutex);
	}
	pthread_mutex_unlock(&mutex);
  • 由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait将错过这个信号,可能会导致线程用眼阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。
  • int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);进入该函数后,回去看条件量等不等于零?等于,就把互斥量变成1,直到cond_Wait返回,把条件量改成1,把互斥量恢复成原样。

条件变量使用规范

  • 等待条件代码
	pthread_mutex_lock(&mutex);
	while(条件为假)
		pthread_cond_wait(&cond,&mutex);
	//修改条件
	pthread_mutex_unlick(&mutex);
  • 给条件发送信号代码
	pthread_mutex_lock(&mutex);
	//设置条件为真
	pthread_cond_signal(&c ond);
	pthread_mutex_unlick(&mutex);

二、生产者消费者模型

在这里插入图片描述

生产者和生产者(互斥) 消费者和消费者(互斥) 生产者和消费者(互斥/同步)—>3种关系
生产者和消费者:线程承担的—>2种角色
超市:内存中特定的一种内存结构(数据结构)—>1个交易场所

1.如何让多个消费者线程等待呢?又如何让消费者线程被唤醒呢?

2.如何让多个生产者线程等待呢?又如何让生产者线程被唤醒呢?

3.如何衡量消费者和生产者所关心的条件是否就绪呢?

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

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

生产者消费者模型的优点

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

在这里插入图片描述

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

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

在这里插入图片描述

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

为了便于理解,我以单生产者,但消费者来进行说明
BlockQueue.hpp

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

//新需求:我只想保存最新的5个任务,如果来了任务,老的任务,我想让他直接被丢弃
const uint32_t gDefaultCap=5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(uint32_t cap=gDefaultCap)
    :_cap(cap)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_conCond,nullptr);
        pthread_cond_init(&_proCond,nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_conCond);
        pthread_cond_destroy(&_proCond);
    }
    //生产接口 
    void push(const T& in)
    {
        //加锁
        //判断-->是否适合生产-->bq是否为满-->程序员视角的条件-->1.满(不生产) 2.不满(生产)
        //if(满) 不生产,休眠
        //else if(不满) 生产,唤醒消费者
        //生产
        //解锁
        lockQueue();
        //if(isFull())//未知原因导致函数调用错误,导致条件不满足
        while(isFull())
        {
            //before:当我等待的时候,会自动释放_mutex
            proBlockWait();//阻塞等待,等待被唤醒,被唤醒!=条件满足(概率虽然非常小)
            //after:当我醒来的时候,我是在临界区里醒来的!!
        }
        //条件满足,可以生产
        pushCore(in);
        unlockQueue();
        wakeupCon();//唤醒消费者
    }
    //消费接口
    T pop()
    {
        //加锁
        //判断-->是否适合消费-->bq是否为空-->程序员视角的条件-->1.空(不消费) 2.非空(消费)
        //if(空) 不消费,休眠
        //else if(非空) 消费,唤醒生产者
        //消费
        //解锁
        lockQueue();
        //if(isEmpty())//未知原因导致函数调用错误,导致条件不满足
        while(isEmpty())
        {
            conBlockWait();//阻塞等待,等待被唤醒
        }
        //条件满足,可以消费
        T tmp=popCore();
        unlockQueue();
        wakeupPro();//唤醒生产者
        return tmp;
    }
private:
    void lockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlockQueue()
    {
       pthread_mutex_unlock(&_mutex);
    }
    bool isEmpty()
    {
        return _bq.empty();
    }
    bool isFull()
    {
        return _bq.size()==_cap;
    }
    void proBlockWait()//生产者一定是在临界区中的!
    {
        //1.在阻塞线程的时候,会自动释放_mutex锁
        pthread_cond_wait(&_proCond,&_mutex);
        //2.当阻塞结束返回的时候,pthread_cond_wait,会自动帮你重新获得_mutex,然后才返回
    }
    void conBlockWait()//阻塞等待,等待被唤醒
    {
        //1.在阻塞线程的时候,会自动释放_mutex锁
        pthread_cond_wait(&_conCond,&_mutex);
        //2.当阻塞结束返回的时候,pthread_cond_wait,会自动帮你重新获得_mutex,然后才返回
    }
    void wakeupCon()//唤醒消费者
    {
        pthread_cond_signal(&_conCond);
    }
    void wakeupPro()//唤醒生产者
    {
        pthread_cond_signal(&_proCond);
    }
    void pushCore(const T& in)
    {
        _bq.push(in);//生产完成
    }
    T popCore()
    {
        T tmp=_bq.front();
        _bq.pop();
        return tmp;
    }
private:
    queue<T> _bq;//blockqueue
    uint32_t _cap;//容量
    pthread_mutex_t _mutex;//保护阻塞队列的互斥锁
    pthread_cond_t _conCond;//让消费者等待的条件变量
    pthread_cond_t _proCond;//让生产者等待的条件变量

};

Test.cc

void* consumer(void* args)
{
    BlockQueue<int>* bqp=static_cast<BlockQueue<int>*>(args);
    while(true)
    {
    	//sleep(2);//消费的慢一点
        //1.消费数据
        int data=bqp->pop();
        cout<<"consumer 消费数据完成: "<<data<<endl;
    }
}
void* productor(void* args)
{
    BlockQueue<int>* bqp=static_cast<BlockQueue<int>*>(args);
    while(true)
    {
        //1.制作数据
        int data=rand()%10;
        //2.生产数据
        bqp->push(data);
        cout<<"productor 生产数据完成: "<<data<<endl;
        sleep(2);//生产慢一些
    }

}
int main()
{
    srand((unsigned long)time(nullptr)^getpid());
    //定义一个阻塞队列
    //创建两个线程,productor,consumer
    //productor-----------consumer
    BlockQueue<int> bq;
    pthread_t c,p;
    pthread_create(&c,nullptr,consumer,&bq);
    pthread_create(&p,nullptr,productor,&bq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    return 0;
}

生产的慢一些

在这里插入图片描述
消费的慢一些
在这里插入图片描述

模拟实现计算任务

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

//新需求:我只想保存最新的5个任务,如果来了任务,老的任务,我想让他直接被丢弃
const uint32_t gDefaultCap=5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(uint32_t cap=gDefaultCap)
    :_cap(cap)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_conCond,nullptr);
        pthread_cond_init(&_proCond,nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_conCond);
        pthread_cond_destroy(&_proCond);
    }
    //生产接口 
    void push(const T& in)
    {
        //加锁
        //判断-->是否适合生产-->bq是否为满-->程序员视角的条件-->1.满(不生产) 2.不满(生产)
        //if(满) 不生产,休眠
        //else if(不满) 生产,唤醒消费者
        //生产
        //解锁
        lockQueue();
        //if(isFull())//未知原因导致函数调用错误,导致条件不满足
        while(isFull())
        {
            //before:当我等待的时候,会自动释放_mutex
            proBlockWait();//阻塞等待,等待被唤醒,被唤醒!=条件满足(概率虽然非常小)
            //after:当我醒来的时候,我是在临界区里醒来的!!
        }
        //条件满足,可以生产
        pushCore(in);
        unlockQueue();
        wakeupCon();//唤醒消费者
    }
    //消费接口
    T pop()
    {
        //加锁
        //判断-->是否适合消费-->bq是否为空-->程序员视角的条件-->1.空(不消费) 2.非空(消费)
        //if(空) 不消费,休眠
        //else if(非空) 消费,唤醒生产者
        //消费
        //解锁
        lockQueue();
        //if(isEmpty())//未知原因导致函数调用错误,导致条件不满足
        while(isEmpty())
        {
            conBlockWait();//阻塞等待,等待被唤醒
        }
        //条件满足,可以消费
        T tmp=popCore();
        unlockQueue();
        wakeupPro();//唤醒生产者
        return tmp;
    }
private:
    void lockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlockQueue()
    {
       pthread_mutex_unlock(&_mutex);
    }
    bool isEmpty()
    {
        return _bq.empty();
    }
    bool isFull()
    {
        return _bq.size()==_cap;
    }
    void proBlockWait()//生产者一定是在临界区中的!
    {
        //1.在阻塞线程的时候,会自动释放_mutex锁
        pthread_cond_wait(&_proCond,&_mutex);
        //2.当阻塞结束返回的时候,pthread_cond_wait,会自动帮你重新获得_mutex,然后才返回
    }
    void conBlockWait()//阻塞等待,等待被唤醒
    {
        //1.在阻塞线程的时候,会自动释放_mutex锁
        pthread_cond_wait(&_conCond,&_mutex);
        //2.当阻塞结束返回的时候,pthread_cond_wait,会自动帮你重新获得_mutex,然后才返回
    }
    void wakeupCon()//唤醒消费者
    {
        pthread_cond_signal(&_conCond);
    }
    void wakeupPro()//唤醒生产者
    {
        pthread_cond_signal(&_proCond);
    }
    void pushCore(const T& in)
    {
        _bq.push(in);//生产完成
    }
    T popCore()
    {
        T tmp=_bq.front();
        _bq.pop();
        return tmp;
    }
private:
    queue<T> _bq;//blockqueue
    uint32_t _cap;//容量
    pthread_mutex_t _mutex;//保护阻塞队列的互斥锁
    pthread_cond_t _conCond;//让消费者等待的条件变量
    pthread_cond_t _proCond;//让生产者等待的条件变量

};

//Task.hpp
#pragma once
#include <iostream>
#include <string>

class Task
{
public:
    Task(int one = 0, int two = 0, char op = '+')
        : _elemOne(one), _elemTwo(two), _operator(op)
    {}
    int operator()()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (_operator)
        {
        case '+':
            result = _elemOne + _elemTwo;
            break;
        case '-':
            result = _elemOne - _elemTwo;
            break;
        case '*':
            result = _elemOne * _elemTwo;
            break;
        case '/':
        {
            if (_elemTwo == 0)
            {
                std::cout << "div zero,abort" << std::endl;
                result = -1;
            }
            else
            {
                result = _elemOne / _elemTwo;
            }
        }
        break;
        case '%':
        {
            if (_elemTwo == 0)
            {
                std::cout << "mod zero,abort" << std::endl;
                result = -1;
            }
            else
            {
                result = _elemOne % _elemTwo;
            }
        }
        break;
        default:
            std::cout << "非法操作: " << _operator << std::endl;
            break;
        }
        return result;
    }
    void get(int& one,int& two,char& op)
    {
        one=_elemOne;
        two=_elemTwo;
        op=_operator;
    }
private:
    int _elemOne;
    int _elemTwo;
    char _operator;
};
//main.cc
#include "BlockQueue.hpp"
#include"Task.hpp"
#include<ctime>
const std::string ops="+-*/%";

//并发,并不是在临界区中并发(一般),而是在生产前(before blockqueue),消费后(after blockqueue)对应的并发

void* consumer(void* args)
{
    BlockQueue<Task>* bqp=static_cast<BlockQueue<Task>*>(args);
    while(true)
    {
        //1.消费任务
        Task t=bqp->pop();
        //处理任务
        int result=t();
        int one,two;
        char op;
        t.get(one,two,op);//输出型参数
        cout<<"consumer["<<pthread_self()<<"]"<<(unsigned long)time(nullptr)
        <<" 消费了一个任务: "<<one<<op<<two<<"="<<result<<endl;
    }
}
void* productor(void* args)
{
    BlockQueue<Task>* bqp=static_cast<BlockQueue<Task>*>(args);
    while(true)
    {
        //1.制作任务
        int one=rand()%50;
        int two=rand()%20;
        char op=ops[rand()%ops.size()];
        Task t(one,two,op);
        //2.生产任务
        bqp->push(t);
        cout<<"productor["<<pthread_self()<<"]"<<(unsigned long)time(nullptr)
        <<" 生产了一个任务: "<<one<<op<<two<<"=?"<<endl;
    }

}
int main()
{
    srand((unsigned long)time(nullptr)^getpid());
    BlockQueue<Task> bq;
    pthread_t c,p;
    pthread_create(&c,nullptr,consumer,&bq);
    pthread_create(&p,nullptr,productor,&bq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);

    return 0;
}

在这里插入图片描述

三、POSIX信号量

信号量是一个计数器,描述临界资源数量的计数器
计数器-- —> P—>申请资源
计数器++ —> V—>释放资源

信号量如果为1的话就称之为二元信号量

1.信号量申请成功了,就一定保证你会拥有一部分临界资源!(资源预定机制)
2.临界资源可以当成整体,可以看做一小部分一小部分!(结合场景)

  • POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的。但POSIX可以用于线程间同步。

函数

初始化信号量

在这里插入图片描述

参数
pshared :0表示线程间共享,非零表示进程间共享
value:信号量的初始值

销毁信号量

在这里插入图片描述

等待信号量

功能:等待信号量,会将信号量的值减1
在这里插入图片描述

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号量的值加1
在这里插入图片描述
刚才我们写的生产着消费者的例子是基于queue的,起空间可以动态分配,现在是基于固定大小的环形队列重写这个程序(POSIX信号量):

基于环形队列的生产消费模型

  • 环形队列采用数组模拟,用模运算来模拟环形特性。
    在这里插入图片描述
    什么时候会发生访问同一个位置呢?

1.我们两个指向同一个位置的时候,只有满or空的时候!(互斥和同步)
其他的时候,都指向不同的位置!(并发)

注意:(后续操作的基本原则)
1.空:消费者不能超过生产者。—>(生产者先运行)
2.满:生产者不能把消费者套一个圈,往后继续写入。—>(消费者先运行)

信号量来保证以上的条件!!!

生产者最关心的是空间资源!!—>N—>P(N) N:[N,0]

消费者最关心的是数据资源!!—>N—>V(N) N:[0,N]

在这里插入图片描述

//RingQueue.hpp
#pragma once 
#include<iostream>
#include<string>
#include<vector>
#include<semaphore.h>
using namespace std;

const int gCap=5;

template<class T>
class RingQueue
{
public:
    RingQueue(int cap=gCap)
    :_ringqueue(cap)
    ,_pIndex(0)
    ,_cIndex(0)
    {
        //生产者使用的
        sem_init(&_roomSem,0,_ringqueue.size());
        //消费者使用的
        sem_init(&_dataSem,0,0);

        pthread_mutex_init(&_pmutex,nullptr);
        pthread_mutex_init(&_cmutex,nullptr);

    }
    //生产者
    void push(const T& in)
    {
        sem_wait(&_roomSem);
        pthread_mutex_lock(&_pmutex);
        
        _ringqueue[_pIndex]=in;//生产的过程
        _pIndex++;//写入位置后移
        _pIndex%=_ringqueue.size();//更新下标,保证环形特性
        
        pthread_mutex_unlock(&_pmutex);
        sem_post(&_dataSem);
    }
    //消费者
    T pop()
    {
        sem_wait(&_dataSem);
        pthread_mutex_lock(&_cmutex);
        
        T tmp=_ringqueue[_cIndex];
        _cIndex++;
        _cIndex%=_ringqueue.size();
        
        pthread_mutex_unlock(&_cmutex);
        sem_post(&_roomSem);
        
        return tmp;
    }

    ~RingQueue()
    {
        sem_destroy(&_roomSem);
        sem_destroy(&_dataSem);
        pthread_mutex_destroy(&_pmutex);
        pthread_mutex_destroy(&_cmutex);
    }
private:
    vector<T> _ringqueue;//环形队列
    sem_t _roomSem;      //衡量空间计数器,productor
    sem_t _dataSem;      //衡量数据计数器,consumer
    int _pIndex;         //当前生产者写入的位置,如果是多线程,_pIndex也是临界资源
    int _cIndex;         //当前消费者读取的位置,如果是多线程,_cIndex也是临界资源
    pthread_mutex_t _pmutex;
    pthread_mutex_t _cmutex;
};
//main.cc
#include"RingQueue.hpp"
#include<ctime>
#include<unistd.h>

void* productor(void* args)
{
    RingQueue<int>* rqp=static_cast<RingQueue<int>*>(args);
    while(true)
    {
    	//sleep(2);
        int data=rand()%10;
        rqp->push(data);
        cout<<"pthread["<<pthread_self()<<"] 生产了一个数据: "<<data<<endl;
    }
}
void* consumer(void* args)
{
    RingQueue<int>* rqp=static_cast<RingQueue<int>*>(args);
    while(true)
    {
        sleep(2);
        int data=rqp->pop();
        cout<<"pthread["<<pthread_self()<<"] 消费了一个数据: "<<data<<endl;
    }
}
int main()
{
    srand((unsigned long)time(nullptr)^getpid());
    RingQueue<int> rq;
    pthread_t c,p;
    pthread_create(&p,nullptr,productor,&rq);
    pthread_create(&c,nullptr,consumer,&rq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    return 0;
}

消费线程消费慢一点

在这里插入图片描述

生产线程生产的慢一点

在这里插入图片描述


总结

(本章完!)

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

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

相关文章

1.MyBatis简介

1.概念 MyBatis是一款开源的持久层框架&#xff0c;它支持定制化SQL、存储过程以及高级映射。 与其它ORM框架不同&#xff0c;MyBatis没有将Java对象与数据表关联起来&#xff0c;而是作为Java方法和SQL语句的桥梁&#xff0c;一般称它为“半自动化ORM”框架。 2.Mybatis架构 …

【软件测试】在我刚上岗时,资深测试给我的建议让我受益匪浅......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 建立测试基线 当我还…

BNB Chain对Zebec生态大力扶持,ZBC或继续登录一线平台

在行业早期开始&#xff0c;流支付赛道就已经具备了早期的轮廓&#xff0c;而在流支付协议Zebec Protocol出现后&#xff0c;该领域被推向了一个新的发展高度&#xff0c;并得到加密领域以及传统商业领域的高度关注。而随着生态的商业进展不断推进、生态不断壮大&#xff0c;Ze…

C++代码优化(1):条款1~4

"不要烟火不要星光&#xff0c;只要问问你内心的想法。" 本栏仅仅 由对《Effictive C》其中的一系列条款&#xff0c;掺杂着自己一些愚钝的理解而写的。 ---前言 条款01: 尽量以const、enum、inline 替换 #define 在谈及上述好几个关键字 与define宏定义的关系&…

Intel i226芯片4端口千兆以太网卡 2.5GPoE工业相机图像采集卡介绍

PCIe-8634图像采集卡是一款基于 Intel i226芯片高性能千兆工业级 PCIe*4 POE网卡,具有传输速度高、兼容性强、性能稳定的特点&#xff0c;可广泛主要应用于网络高清监控、无线覆盖、工业自动化等领域。 RJ45千兆网络采用4 k Intel226千兆网络芯片,支持10/100/1000/2500Mbps传输…

microservices 简介

油鹳视频 Microservices explained - the What, Why and How? https://www.youtube.com/watch?vrv4LlmLmVWk&t2s microservices 是一种软件体系结构&#xff0c; microservices architecture(微服务架构) 是与传统的 monolithic architecture(整体式架构&#xff0c;一体…

微信转盘抽奖小程序如何制作?

微信转盘抽奖小程序如何制作&#xff1f;大概需要多少钱&#xff1f; 价格方面&#xff0c;平台按年收费&#xff0c;一年1498至2498元。 明码标价&#xff0c;7天退款制度&#xff0c;随时退。 微信转盘抽奖小程序如何制作步骤: 1.进入第三方微信转盘抽奖小程序制作平台官…

计算机结构体系:指令调度与循环展开题型 (非凭感觉的方法详解)

文章目录题目初始分析1.确定所使用的各个寄存器的作用2.将循环体内容语句和控制语句分开3.找出每一条循环体内容指令的代价并排序4.找出每一条循环体控制指令的代价并排序5.基于贪婪算法的最优循环展开体系结构这门课程中&#xff0c;指令调度和循环展开可以说是课程最困难的地…

负载均衡反向代理下的webshell

负载均衡(Load Balance) 是一种廉价的扩容的方案&#xff0c;它的概念不是本文的重点&#xff0c;不知道的可以去查资料学习。实现负载均衡的方式有很多种&#xff0c;比如 DNS 方式、HTTP 重定向方式、IP 负载均衡方式、反向代理方式等等。 其中像 HTTP 重定向方式、DNS方式等…

BioPython ② | 面向对象编程Object Oriented Programming

BioPython ② | Python面向对象编程 题目要求 定义分子类&#xff08;Molecule&#xff09;作为基类&#xff0c;包含集合elements和weight作为其属性&#xff0c;用初始化函数&#xff0c;将elements初始化为空集&#xff0c;weight初始化为None&#xff1b;定义show_weight…

进阶 - Git的远程仓库

本篇文章&#xff0c;是基于我自用Windows&#xff08;Win10&#xff09;系统当做示例演示 Git的远程仓库 之前我们一直在探讨 Git 的一些命令&#xff0c;也提及了仓库的概念。如果只是在一个仓库里管理文件历史&#xff0c;Git 和 SVN 真没啥区别。 Git 是分布式版本控制系…

02 stata入门【计量经济学及stata应用】

安装&#xff1a;建议直接在微信搜索&#xff0c;很多公众号有安装包资源及下载教程 不同版本在基本功能上无较大差异&#xff0c;一般为SE&#xff0c;更为专业MP&#xff0c;只是在处理变量个数或容量等存在不同 界面 历史命令&#xff1b;结果窗口&命令窗口&#xff1b…

字节跳动岗位薪酬体系曝光,看完感叹:不服不行,想高薪还得是学这个。。。。

目录&#xff1a;导读 前言 01岗位职级 02岗位薪酬 03绩效考核与晋升 大厂软件测试岗经验分享 一、软件测试基础篇&#xff1a;2022版 二、MySQL篇&#xff1a;2022版 三、 Linux篇&#xff1a;2022版 四、 Web测试 五、接口测试 六、APP测试 七、性能测试 八、Se…

Nacos一些理解

下载Mysql //下载mysql docker pull mysql:5.7 //运行容器 docker run -p 3306:3306 --name mysql -v /home/mysql/log:/var/log/mysql -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORDxx -d mysql:5.7 将导入数据库 1.新建数据库 nacos /*Navicat Premiu…

HyperMesh宝典 | 跟着老师学,你也可以做好二次开发

说到二次开发&#xff0c;你的脑海里是不是浮现出了“码农”两个字&#xff1f;有人可能会问&#xff0c;码农又是什么&#xff1f; 你是不是觉得二次开发这种码农干的事情感觉起来也太困难了吧&#xff1f; 其实有时候二次开发真的很简单&#xff0c;懂一点二次开发会让你的工…

机器学习中的数学原理——多项式回归

这个专栏主要是用来分享一下我在机器学习中的学习笔记及一些感悟&#xff0c;也希望对你的学习有帮助哦&#xff01;感兴趣的小伙伴欢迎私信或者评论区留言&#xff01;这一篇就更新一下《白话机器学习中的数学——多项式回归》&#xff01; 目录 一、什么多项式回归 二、算法…

Java#33(IO流)

目录 一.IO流 作用: (对于程序而言)用于读写数据(本地数据, 网络数据) 二.IO流体系 1.字节输出流 2.字节输入流 3.文件拷贝 3.字符集 字符流 字符输入流 字符输出流 缓冲流 转换流 序列化流 ​编辑反序列流 打印流 一.IO流 I: input O: output 流: 想流…

Linux下创建动态链接库与静态链接库

动态链接库 Linux下的动态链接库文件扩展名为so&#xff0c;可以用多个文件生成一个动态链接库。 在头文件中定义三个函数&#xff0c;三个函数分别于三个cpp文件中实现。 将三个cpp文件编译成动态库libdynamic.so -fPIC表示编译为位置独立的代码&#xff0c;如果不选择默…

使用YOLOv5练自己的数据集

说明 上次使用学习了如何运行yolov5检测自己的数据&#xff0c;这次学习yolov5如何训练自己的数据集 本次记录如何使用yolov5训练自己的数据集以及遇到报错解决方案 数据 使用数据&#xff1a;水果数据集 数据包含了png图片和相应的标注文件 切分数据 代码如下&#xff1…

[ vulhub漏洞复现篇 ] solr 远程命令执行(CVE-2019-0193)

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…