13-5_Qt 5.9 C++开发指南_基于信号量的线程同步_Semaphore

news2024/12/23 4:08:59

文章目录

  • 1. 信号量的原理
  • 2. 双缓冲区数据采集和读取线程类设计
  • 3. QThreadDAQ和QThreadShow 的使用
  • 4. 源码
    • 4.1 可视化UI设计框架
    • 4.2 qmythread.h
    • 4.3 qmythread.cpp
    • 4.4 dialog.h
    • 4.5 dialog.cpp

1. 信号量的原理

信号量(Semaphore)是另一种限制对共享资源进行访问的线程同步机制,它与互斥量(Mutex)相似,但是有区别。一个互斥量只能被锁定一次,而信号量可以多次使用。信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区。

QSemaphore 是实现信号量功能的类,它提供以下几个基本的函数:

  • acquire(int n)尝试获得 n 个资源。如果没有这么多资源,线程将阻塞直到有 n 个资源可用
  • release(int n)释放 n 个资源,如果信号量的资源已全部可用之后再 release(),就可以创建更多的资源,增加可用资源的个数:
  • int available()返回当前信号量可用的资源个数,这个数永远不可能为负数,如果为 0,就说明当前没有资源可用;
  • bool tryAcquire(int n = 1),尝试获取 n 个资源,不成功时不阻塞线程。

定义QSemaphore 的实例时,可以传递一个数值作为初始可用的资源个数。

下面的一段示意代码,说明 QSemaphore 的几个函数的作用。

QSemaphore WC(5);  // WC.available() == 5,初始资源个数为 5个
WC.acquire(4):  // WC.available() == 1,用了4 个资源,还剩余1个可用
WC.release(2);  // WC.available() == 3,释放了2个资源,剩余3个可用
WC.acquire(3);  // WC.available() == 0,又用了3 个资源,剩余0个可用
WC.tryAcquire(1);  //因为WC.available() == 0,返回 false,
WC.acquire();  //因为 wc.available() == 0,没有资源可用,阻塞

为了理解信号量及上面这段代码的意义,可以假想变量 WC 是一个公共卫生间,初始化时定义WC有5个位置可用。

  • WC.acquire(4),成功进去 4 个人,占用了4 个位置,还剩余1个位置

  • WC.release(2),出来了 2个人,剩余3 个位置可用:

  • WC.acquire(3),又进去 3 个人,剩余0个位置可用;

  • WC.tryAcquire(1),有一个人尝试进去,但是因为没有位置了,他不等待,走了,tryAcquire()函数返回 false:

  • WC.acquire(),有一个人必须进去,但是因为没有位置了,他就一直在外面等着,直到有其他人出来,空余出位置来。

互斥量相当于列车上的卫生间,一次只允许一个人进出,信号量则是多人公共卫生间,允许多人进出。n 个资源就是信号量需要保护的共享资源,至于资源如何分配,就是内部处理的问题了。

2. 双缓冲区数据采集和读取线程类设计

理解:可以用于实现自行定义的缓冲区大小,利用2个子线程对不断产生的数据不间断进行写入及处理,主线程主要进行显示

信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区,适用于Producer/Consumer 模型。

在实例 samp13_5中,创建类似于 Producer/Consumer 模型的两个线程类 QThreadDAQ 和QThreadShow。qmythread.h 文件中这两个类的定义如下:

#ifndef QMYTHREAD_H
#define QMYTHREAD_H

//#include    <QObject>
#include    <QThread>

class QThreadDAQ : public QThread
{
    Q_OBJECT

private:
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QThreadDAQ();
    void    stopThread();
};

class QThreadShow : public QThread
{
    Q_OBJECT
private:
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QThreadShow();
    void    stopThread();
signals:
    void    newValue(int *data,int count, int seq);
};
#endif // QMYTHREAD_H

QThreadDAQ 是数据采集线程,例如在使用数据采集卡进行连续数据采集时,需要一个单独的线程将采集卡采集的数据读取到缓冲区内。
QThreadShow 是数据读取线程,用于读取已存满数据的缓冲区中的数据并传递给主线程显示,采用信号与槽机制与主线程交互。
QThreadDAQ/QThreadShow 类的定义与使用 QWaitCondition 的实例 samp13_4中的QThreadProducer/QThreadConsumer 类的定义类似,只是QThreadShow 的信号 newValue()采用了指针作为传递参数,用于一次传递出一个缓冲区的数据。

qmythread.cpp 文件中QThreadDAQ和QThreadShow 的主要功能代码如下:

#include    "qmythread.h"
#include    <QSemaphore>

const int BufferSize = 8;
int buffer1[BufferSize];
int buffer2[BufferSize];
int curBuf=1; //当前正在写入的Buffer

int bufNo=0; //采集的缓冲区序号

quint8   counter=0;//数据生成器

QSemaphore emptyBufs(2);//信号量:空的缓冲区个数,初始资源个数为2
QSemaphore fullBufs; //满的缓冲区个数,初始资源为0

QThreadDAQ::QThreadDAQ()
{

}

void QThreadDAQ::stopThread()
{
    m_stop=true;
}

void QThreadDAQ::run()
{
    m_stop=false;//启动线程时令m_stop=false
    bufNo=0;//缓冲区序号
    curBuf=1; //当前写入使用的缓冲区
    counter=0;//数据生成器

    int n=emptyBufs.available();
    if (n<2)  //保证 线程启动时emptyBufs.available==2
      emptyBufs.release(2-n);

    while(!m_stop)//循环主体
    {
        emptyBufs.acquire();//获取一个空的缓冲区
        for(int i=0;i<BufferSize;i++) //产生一个缓冲区的数据
        {
            if (curBuf==1)
                buffer1[i]=counter; //向缓冲区写入数据
            else
                buffer2[i]=counter;
            counter++; //模拟数据采集卡产生数据

            msleep(50); //每50ms产生一个数
        }

        bufNo++;//缓冲区序号
        if (curBuf==1) // 切换当前写入缓冲区
          curBuf=2;
        else
          curBuf=1;

        fullBufs.release(); //有了一个满的缓冲区,available==1
    }
    quit();
}

void QThreadShow::run()
{
    m_stop=false;//启动线程时令m_stop=false

    int n=fullBufs.available();
    if (n>0)
       fullBufs.acquire(n); //将fullBufs可用资源个数初始化为0

    while(!m_stop)//循环主体
    {
        fullBufs.acquire(); //等待有缓冲区满,当fullBufs.available==0阻塞

        int bufferData[BufferSize];
        int seq=bufNo;

        if(curBuf==1) //当前在写入的缓冲区是1,那么满的缓冲区是2
            for (int i=0;i<BufferSize;i++)
               bufferData[i]=buffer2[i]; //快速拷贝缓冲区数据
        else
            for (int i=0;i<BufferSize;i++)
               bufferData[i]=buffer1[i];

        emptyBufs.release();//释放一个空缓冲区
        emit    newValue(bufferData,BufferSize,seq);//给主线程传递数据
    }
    quit();
}

QThreadShow::QThreadShow()
{

}

void QThreadShow::stopThread()
{
    m_stop=true;
}

在共享变量区定义了两个缓冲区 buffer1和 buffer2,都是长度为 BufferSize 的数组。

变量 curBuf 记录当前写入操作的缓冲区编号,其值只能是 1或2,表示 bufferl 或 buffer2,bufNo是累积的缓冲区个数编号,counter 是模拟采集数据的变量。
信号量emptyBufs 初始资源个数为2,表示有2个空的缓冲区可用。

信号量 fullBufs初始化资源个数为0,表示写满数据的缓冲区个数为零。

QThreadDAQ::run()采用双缓冲方式进行模拟数据采集,线程启动时初始化共享变量,特别的是使emptyBufs 的可用资源个数初始化为2。
在while 循环体里,第一行语句 emptyBufs.acquire()使信号量emptyBufs 获取一个资源,即获取一个空的缓冲区。用于数据缓存的有两个缓冲区,只要有一个空的缓冲区,就可以向这个缓冲区写入数据。

while 循环体里的 for 循环每隔 50 毫秒使 counter 值加 1,然后写入当前正在写入的缓冲区,当前写入哪个缓冲区由 curBuf 决定。counter 是模拟采集的数据,连续增加可以判断采集的数据是否连续。

完成 for 循环后正好写满一个缓冲区,这时改变 curBuf 的值,切换用于写入的缓冲区。

写满一个缓冲区之后,使用 fullBufs.release()为信号量 fullBufs 释放一个资源,这时 fullBufs.available==l,表示有一个缓冲区被写满了。这样,QThreadShow 线程里使用 fullBufs.acquire()就可以获得一个资源,可以读取已写满的缓冲区里的数据。

QThreadShow::run()用于监测是否有已经写满数据的缓冲区,只要有缓冲区写满了数据,就立刻读取出数据,然后释放这个缓冲区给 OThreadDAQ 线程用于写入。

QThreadShow::run()函数的初始化部分使 fullBufs.available==0,即线程刚启动时是没有资源的。

在 while循环体里第一行语句就是通过 fullBufs.acquire()以阻塞方式获取一个资源,只有当QThreadDAQ 线程里写满一个缓冲区,执行一次fullBufs.release()后,fullBufs.acquire()才获得资源并执行后面的代码。后面的代码就立即用临时变量将缓冲区里的数据读取出来,再调用emptyBufs.release()给信号量emptyBufs 释放一个资源,然后发射信号 newValue,由主线程读取数据并显示。
所以,这里使用了双缓冲区、两个信号量实现采集和读取两个线程的协调操作。采集线程里使用emptyBufs.acquire()获取可以写入的缓冲区。
实际使用数据采集卡进行连续数据采集时,采集线程是不能停顿下来的,也就是说万一读取线程执行较慢,采集线程是不会等待的。所以实际情况下,读取线程的操作应该比采集线程快。

3. QThreadDAQ和QThreadShow 的使用

设计窗口基于 QDialog 应用程序 samp13_5,对话框的类定义如下(省略了一些不重要的或与前面实例重复的部分内容):

class Dialog : public QDialog
{
    Q_OBJECT

private:
    QThreadDAQ   threadProducer;
    QThreadShow   threadConsumer;
private slots:
    void    onthreadB_newValue(int *data, int count, int bufNo);

};

Dialog类定义了两个线程的实例,threadProducer 和 threadConsumer。

自定义了一个槽函数 onthreadB_newValue(),用于与 threadConsumer 的信号关联,在 Dialog的构造函数里进行了关联。

connect(&threadConsumer,SIGNAL(newValue(int*,int,int)),this,SLOT(onthreadB_newValue(int*,int,int)));

槽函数onthreadB_newValue()的功能就是读取一个缓冲区里的数据并显示,其实现代码如下

void Dialog::onthreadB_newValue(int *data, int count, int bufNo)
{ //读取threadConsumer 传递的缓冲区的数据
    QString  str=QString::asprintf("第 %d 个缓冲区:",bufNo);
    for (int i=0;i<count;i++)
    {
        str=str+QString::asprintf("%d, ",*data);
        data++;
    }
    str=str+'\n';

    ui->plainTextEdit->appendPlainText(str);
}

传递的指针型参数int*data 是一个数组指针,count 是缓冲区长度。(此处注意主线程和子线程利用信号槽传递数组值的方法

“启动线程”和“结束线程”两个按钮的代码如下(省略了按键使能控制的代码):

void Dialog::on_btnStopThread_clicked()
{//结束线程
//    threadConsumer.stopThread();//结束线程的run()函数执行
    threadConsumer.terminate(); //因为threadB可能处于等待状态,所以用terminate强制结束
    threadConsumer.wait();//

    threadProducer.terminate();//结束线程的run()函数执行
    threadProducer.wait();//

    ui->btnStartThread->setEnabled(true);
    ui->btnStopThread->setEnabled(false);
}

void Dialog::on_btnStartThread_clicked()
{//启动线程
    threadConsumer.start();
    threadProducer.start();

    ui->btnStartThread->setEnabled(false);
    ui->btnStopThread->setEnabled(true);
}

启动线程时,先启动 threadConsumer,再启动 threadProducer,否则可能丢失第1个缓冲区的数据。
结束线程时,都采用 terminate()函数强制结束线程,因为两个线程之间有互锁的关系,若不使用terminate()强制结束会出现线程无法结束的问题。

程序运行时的界面如图 13-3 所示
在这里插入图片描述
从图 13-3 可以看出,没有出现丢失缓冲区或数据点的情况,两个线程之间协调的很好,将QThreadDAQ:run()函数中模拟采样率的延时时间调整为2秒也没问题(正常设置为50毫秒)。

在实际的数据采集中,要保证不丢失缓冲区或数据点,数据读取线程的速度必须快过数据写入缓冲区的线程的速度。

4. 源码

4.1 可视化UI设计框架

在这里插入图片描述

4.2 qmythread.h

#ifndef QMYTHREAD_H
#define QMYTHREAD_H

//#include    <QObject>
#include    <QThread>

class QThreadDAQ : public QThread
{
    Q_OBJECT

private:
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QThreadDAQ();
    void    stopThread();
};

class QThreadShow : public QThread
{
    Q_OBJECT
private:
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QThreadShow();
    void    stopThread();
signals:
    void    newValue(int *data,int count, int seq);
};
#endif // QMYTHREAD_H

4.3 qmythread.cpp

#include    "qmythread.h"
#include    <QSemaphore>

const int BufferSize = 8;
int buffer1[BufferSize];
int buffer2[BufferSize];
int curBuf=1; //当前正在写入的Buffer

int bufNo=0; //采集的缓冲区序号

quint8   counter=0;//数据生成器

QSemaphore emptyBufs(2);//信号量:空的缓冲区个数,初始资源个数为2
QSemaphore fullBufs; //满的缓冲区个数,初始资源为0

QThreadDAQ::QThreadDAQ()
{

}

void QThreadDAQ::stopThread()
{
    m_stop=true;
}

void QThreadDAQ::run()
{
    m_stop=false;//启动线程时令m_stop=false
    bufNo=0;//缓冲区序号
    curBuf=1; //当前写入使用的缓冲区
    counter=0;//数据生成器

    int n=emptyBufs.available();
    if (n<2)  //保证 线程启动时emptyBufs.available==2
      emptyBufs.release(2-n);

    while(!m_stop)//循环主体
    {
        emptyBufs.acquire();//获取一个空的缓冲区
        for(int i=0;i<BufferSize;i++) //产生一个缓冲区的数据
        {
            if (curBuf==1)
                buffer1[i]=counter; //向缓冲区写入数据
            else
                buffer2[i]=counter;
            counter++; //模拟数据采集卡产生数据

            msleep(50); //每50ms产生一个数
        }

        bufNo++;//缓冲区序号
        if (curBuf==1) // 切换当前写入缓冲区
          curBuf=2;
        else
          curBuf=1;

        fullBufs.release(); //有了一个满的缓冲区,available==1
    }
    quit();
}

void QThreadShow::run()
{
    m_stop=false;//启动线程时令m_stop=false

    int n=fullBufs.available();
    if (n>0)
       fullBufs.acquire(n); //将fullBufs可用资源个数初始化为0

    while(!m_stop)//循环主体
    {
        fullBufs.acquire(); //等待有缓冲区满,当fullBufs.available==0阻塞

        int bufferData[BufferSize];
        int seq=bufNo;

        if(curBuf==1) //当前在写入的缓冲区是1,那么满的缓冲区是2
            for (int i=0;i<BufferSize;i++)
               bufferData[i]=buffer2[i]; //快速拷贝缓冲区数据
        else
            for (int i=0;i<BufferSize;i++)
               bufferData[i]=buffer1[i];

        emptyBufs.release();//释放一个空缓冲区
        emit    newValue(bufferData,BufferSize,seq);//给主线程传递数据
    }
    quit();
}

QThreadShow::QThreadShow()
{

}

void QThreadShow::stopThread()
{
    m_stop=true;
}

4.4 dialog.h

#ifndef DIALOG_H
#define DIALOG_H

#include    <QDialog>
#include    <QTimer>

#include    "qmythread.h"

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

private:
    QThreadDAQ   threadProducer;
    QThreadShow   threadConsumer;
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();

private slots:
    void    onthreadA_started();
    void    onthreadA_finished();

    void    onthreadB_started();
    void    onthreadB_finished();

    void    onthreadB_newValue(int *data, int count, int bufNo);


    void on_btnClear_clicked();

    void on_btnStopThread_clicked();

    void on_btnStartThread_clicked();

private:
    Ui::Dialog *ui;
};

#endif // DIALOG_H

4.5 dialog.cpp

#include "dialog.h"
#include "ui_dialog.h"

void Dialog::closeEvent(QCloseEvent *event)
{//窗口关闭
    if (threadProducer.isRunning())
    {
        threadProducer.terminate();//结束线程的run()函数执行
        threadProducer.wait();//
    }

    if (threadConsumer.isRunning())
    {
        threadConsumer.terminate(); //因为threadB可能处于等待状态,所以用terminate强制结束
        threadConsumer.wait();//
    }

    event->accept();
}

Dialog::Dialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Dialog)
{
    ui->setupUi(this);

    connect(&threadProducer,SIGNAL(started()),this,SLOT(onthreadA_started()));
    connect(&threadProducer,SIGNAL(finished()),this,SLOT(onthreadA_finished()));

    connect(&threadConsumer,SIGNAL(started()),this,SLOT(onthreadB_started()));
    connect(&threadConsumer,SIGNAL(finished()),this,SLOT(onthreadB_finished()));

    connect(&threadConsumer,SIGNAL(newValue(int*,int,int)),
            this,SLOT(onthreadB_newValue(int*,int,int)));
}

Dialog::~Dialog()
{
    delete ui;
}

void Dialog::onthreadA_started()
{
    ui->LabA->setText("Thread Producer状态: started");
}

void Dialog::onthreadA_finished()
{
    ui->LabA->setText("Thread Producer状态: finished");
}

void Dialog::onthreadB_started()
{
    ui->LabB->setText("Thread Consumer状态: started");
}

void Dialog::onthreadB_finished()
{
    ui->LabB->setText("Thread Consumer状态: finished");
}

void Dialog::onthreadB_newValue(int *data, int count, int bufNo)
{ //读取threadConsumer 传递的缓冲区的数据
    QString  str=QString::asprintf("第 %d 个缓冲区:",bufNo);
    for (int i=0;i<count;i++)
    {
        str=str+QString::asprintf("%d, ",*data);
        data++;
    }
    str=str+'\n';

    ui->plainTextEdit->appendPlainText(str);
}

void Dialog::on_btnClear_clicked()
{
    ui->plainTextEdit->clear();
}

void Dialog::on_btnStopThread_clicked()
{//结束线程
//    threadConsumer.stopThread();//结束线程的run()函数执行
    threadConsumer.terminate(); //因为threadB可能处于等待状态,所以用terminate强制结束
    threadConsumer.wait();//

    threadProducer.terminate();//结束线程的run()函数执行
    threadProducer.wait();//

    ui->btnStartThread->setEnabled(true);
    ui->btnStopThread->setEnabled(false);
}

void Dialog::on_btnStartThread_clicked()
{//启动线程
    threadConsumer.start();
    threadProducer.start();

    ui->btnStartThread->setEnabled(false);
    ui->btnStopThread->setEnabled(true);
}

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

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

相关文章

2023年8月美团外卖3-18元红包优惠券天天领取活动日历及美团外卖红包领取使用

2023年8月美团外卖3-18元红包天天领取活动日历 根据上图美团外卖红包领取活动时间表以下时间可以天天领取3-18元美团外卖红包优惠券&#xff1a; 1、2023年8月18日 可领取美团外卖18元神券节红包&#xff1b; 2、2023年8月每周六、周日每天可领取12元美团外卖节红包&#xff…

聊聊工程化 Docker 的最新趋势以及最佳实践

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

AI绘画大师,轻松塑造绝美画作!

前言 随着科技的不断进步&#xff0c;人工智能&#xff08;AI&#xff09;正逐渐渗透到各个领域&#xff0c;艺术也不例外。AI绘 画大师&#xff0c;即 AI 绘画 API&#xff0c;作为其中的一颗明珠&#xff0c;正在引领着艺术创作的革命&#xff0c;它为创作者和艺术爱好者带来…

docker容器创建私有仓库(第三篇)

目录 六、创建私有仓库 七、Docker资源限制 7.1、CPU使用率 7.2、CPU共享比例 7.3、CPU周期限制 7.4、CPU核心限制 7.5、CPU 配额控制参数的混合案例 7.6、内存限制 7.7、Block IO 的限制 7.8、限制bps 和iops 8、Docker数据持久化 8.1、数据持久化介绍 8.2、Volum…

python制作超炫流星雨表白,python好看的流星雨代码

大家好&#xff0c;本文将围绕python制作超炫流星雨表白展开说明&#xff0c;python好看的流星雨代码是一个很多人都想弄明白的事情&#xff0c;想搞清楚html流星雨代码可复制需要先了解以下几个事情。 本文讲述了Python代码示例&#xff1a;实现流星雨特效&#xff01;具有很好…

使用 GitHub Copilot 进行 Prompt Engineering 的初学者指南(译)

文章目录 什么是 GitHub Copilot ?GitHub Copilot 可以自己编码吗&#xff1f;GitHub Copilot 的底层是如何工作的&#xff1f;什么是 prompt engineering?这是 prompt engineering 的另一个例子 使用 GitHub Copilot 进行 prompt engineering 的最佳实践提供高级上下文&…

KepwareEX配置API REST接口

服务端Kepware设置 API允许连接设置 创建通道 请求地址(POST)&#xff1a; https://<主机名_或_ip>:<端口>/config/v1/project/channels 以下示例使用postman工具访问API创建了一个名为Channel1 的通道&#xff0c;其使用在本地主机运行的服务器中的Simulator …

【LeetCode】剑指 Offer Ⅱ 第2章:数组(8道题) -- Java Version

题库链接&#xff1a;https://leetcode.cn/problem-list/e8X3pBZi/ 题目解决方案剑指 Offer II 006. 排序数组中两个数字之和双指针&#xff08;异向&#xff09; ⭐剑指 Offer II 007. 数组中和为 0 的三个数排序 双指针&#xff08;异向&#xff09; ⭐剑指 Offer II 008. 和…

应用在心率血氧健康监测耳机中的三合一灯珠

随着生活节奏的加快&#xff0c;工作压力的加大&#xff0c;越来越多的人开始注重健身&#xff0c;如此一来&#xff0c;可穿戴健身追踪设备就变得很流行。通过对脉搏血氧测量原理的研究&#xff0c;人们已经发现只要测量出两种波长的透射光在一个完整的脉搏波中光强度的变化量…

面试必考精华版Leetcode328. 奇偶链表

题目&#xff1a; 代码(首刷看解析 day22&#xff09;&#xff1a; class Solution { public:ListNode* oddEvenList(ListNode* head) {if(headnullptr) return nullptr;ListNode* oddhead;ListNode* evenHeadhead->next;;ListNode* evenevenHead;while(even!nullptr&&…

华为OD机试真题 Java 实现【N进制减法】【2023 B卷 200分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、Java算法源码五、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 …

flask中实现restful-api

flask中实现restful-api 举例&#xff0c;我们可以创建一个用于管理任务&#xff08;Task&#xff09;的API。在这个例子中&#xff0c;我们将有以下API&#xff1a; GET /tasks: 获取所有任务POST /tasks: 创建一个新的任务GET /tasks/<id>: 获取一个任务的详情PUT /t…

加速产品开发周期:PDM系统的设计流程优化

加速产品开发周期&#xff1a;PDM系统的设计流程优化 在现代商业竞争日益激烈的背景下&#xff0c;企业迫切需要加速产品开发周期&#xff0c;快速推向市场&#xff0c;抢占先机。PDM系统&#xff08;Product Data Management&#xff0c;产品数据管理&#xff09;作为一个高效…

Android系统源码 目录结构

前言&#xff1a;Android官方在线看源码地址 https://cs.android.com/ 1.Android系统架构 Android系统架构分为五层&#xff0c;从上到下依次是应用层、应用框架层、系统运行库层、硬件抽象层和Linux内核层。 AOSP 架构 AOSP 的软件堆栈包含以下层&#xff1a; 图 1. AOSP …

【PCB专题】案例:Allegro如何在PCB板上直接修改封装中的特定焊盘

在实际产品设计中我们很可能因为结构、封装的制约而将两个器件放的很近。并且也可能因为是接口器件,所以要求上锡量要多。 但是因为成本的原因我们很可能不会去为了几个器件增加钢网的阶数,以求获得更多的锡量,让PIN脚爬锡更好。而会通过扩大钢网开口的形式来增加锡量。 如…

前端技术基础-css

前端技术基础-css【了解】 一、css理解 概念&#xff1a;CSS&#xff1a;C(cascade) SS(StyleSheet) &#xff0c;级联样式表。作用&#xff1a;对网页提供丰富的视觉效果&#xff0c;进行美化页面(需要在html页面基础上)样式规则&#xff1a;样式1&#xff1a;值1;样式2&…

FPGA学习——电子时钟模拟(新)

文章目录 一、数码管简介二、C4开发板数码管原理图三、代码实现四、实现效果五、总结 博主在之前曾经编写过一篇电子时钟的博客&#xff08;详情请见此篇博文&#xff09;&#xff0c;但曾经编写的电子时钟&#xff0c;未显示小数点位&#xff0c;同时当时的数码管模块是为了电…

增量式PID算法及其MATLAB实现

增量式PID算法是一种常用的控制算法,用于控制系统中的反馈控制。它通过对系统的误差进行递推式的计算,实现对系统输出的调节,使得系统的输出逐渐趋向于设定值。 delta u(k)=u(k)-u(k-1)=Kp*(e(k)-e(k-1))+Ki*e(k)+Kd*(e(k)-2*e(k-1)+e(k-2)) PID算法由三个部分组成:比例(…

如何通过Navicat连接Oracle数据库

本文介绍如何通过Navicat 连接Oracle数据库。以往总是使用Oracle客户端来连接Oracle数据库&#xff0c;但是Oracle客户端一般有几百M的大小&#xff0c;而且安装繁琐配置麻烦。如果可以通过Navicat直接连接Oracle则会非常轻松方便。 1、下载Instant Client Base 用使用Navicat…

【contenteditable属性将元素改为可编辑状态】

元素添加contenteditable属性之后点击即可进入编辑状态 像这种只修改一条属性不必再打开弹框进行编辑&#xff0c;使用contenteditable会很方便 添加失焦、回车、获焦事件 如 <p :contenteditable"item.contenteditable || false"keydown.enter"key($event…