C++客户端Qt开发——多线程编程(二)

news2024/9/22 5:28:50

多线程编程(二) 

③线程池

Qt中线程池的使用 | 爱编程的大丙

1>线程池

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

2>线程池原理

在各个编程语言的语种中都有线程池的概念,并且很多语言中直接提供了线程池,作为程序猿直接使用就可以了,下面介绍一下线程池的实现原理:

线程池的组成主要分为3个部分,这三部分配合工作就可以得到一个完整的线程池:

1. 任务队列,存储需要处理的任务,由工作的线程来处理这些任务

  • 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
  • 已处理的任务会被从任务队列中删除
  • 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程

2. 工作的线程(任务队列任务的消费者),N个

  • 线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理
  • 工作的线程相当于是任务队列的消费者角色,
  • 如果任务队列为空,工作的线程将会被阻塞(使用条件变量/信号量阻塞)
  • 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作

3. 管理者线程(不处理任务队列中的任务),1个

  • 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
    • 当任务过多的时候可以适当的创建一些新的工作线程
    • 当任务过少的时候可以适当的销毁一些工作的线程

3>线程池的使用QRunnable

在Qt中使用线程池需要先创建任务,添加到线程池中的每一个任务都需要是一个QRunnable类型,因此在程序中需要创建子类继承QRunnable这个类,然后重写run()方法,在这个函数中编写要在线程池中执行的任务,并将这个子类对象传递给线程池,这样任务就可以被线程池中的某个工作的线程处理掉了。

QRunnable类常用函数不多,主要是设置任务对象传给线程池后,是否需要自动析构。

[pure virtual] void QRunnable::run();

在子类中必须要重写的函数, 里边是任务的处理流程

void QRunnable::setAutoDelete(bool autoDelete);

  • 参数设置为 true: 这个任务对象在线程池中的线程中处理完毕, 这个任务对象就会自动销毁
  • 参数设置为 false: 这个任务对象在线程池中的线程中处理完毕, 对象需要手动销毁

bool QRunnable::autoDelete() const;

获取当前任务对象的析构方式,返回true->自动析构, 返回false->手动析构

创建一个要添加到线程池中的任务类,处理方式如下:

class MyWork : public QObject, public QRunnable
{
    Q_OBJECT
public:
    explicit MyWork(QObject *parent = nullptr)
    {
        // 任务执行完毕,该对象自动销毁
        setAutoDelete(true);
    }
    ~MyWork();

    void run() override{};
}

在上面的示例中MyWork类是一个多重继承,如果需要在这个任务中使用Qt的信号槽机制进行数据的传递就必须继承Qobject这个类,如果不使用信号槽传递数据就可以不继承了,只继承QRunnable即可

class MyWork :public QRunnable
{
    Q_OBJECT
public:
    explicit MyWork()
    {
        // 任务执行完毕,该对象自动销毁
        setAutoDelete(true);
    }
    ~MyWork();

    void run() override{};
}
4>QThreadPool

Qt中的QThreadPool类管理了一组QThreads,里边还维护了一个任务队列。QThreadPool管理和回收各个QThread对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局QThreadPool对象,可以通过调用globalInstance()来访问它。也可以单独创建一个QThreadPool对象使用。

线程池常用的API函数如下:

int maxThreadCount() const;

void setMaxThreadCount(int maxThreadCount);

获取和设置线程中的最大线程个数

void QThreadPool::start(QRunnable * runnable, int priority = 0);

给线程池添加任务, 任务是一个 QRunnable 类型的对象,如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理

bool QThreadPool::tryStart(QRunnable * runnable);

如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中

int QThreadPool::activeThreadCount() const;

线程池中被激活的线程的个数(正在工作的线程个数)

bool QThreadPool::tryTake(QRunnable *runnable);

尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了

void QThreadPool::clear();

将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了

static QThreadPool * QThreadPool::globalInstance();

在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象

一般情况下,我们不需要在Qt程序中创建线程池对象,直接使用Qt为每个应用程序提供的线程池全局对象即可。得到线程池对象之后,调用start()方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。

使用示例:

class MyWork :public QRunnable
{
    Q_OBJECT
public:
    explicit MyWork();
    ~MyWork();

    void run() override;
}
MyWork::MyWork() : QRunnable()
{
    // 任务执行完毕,该对象自动销毁
    setAutoDelete(true);
}
void MyWork::run()
{
    // 业务处理代码
    ......
}
MyWork::MyWork() : QRunnable()
{
    // 任务执行完毕,该对象自动销毁
    setAutoDelete(true);
}
void MyWork::run()
{
    // 业务处理代码
    ......
}
示例:生成随机数,然后使用冒泡排序和快速排序对其进行处理

这个示例再用线程池实现一次

mythread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include<QVector>
#include<QRunnable>

//生成随机数,将构造类的名字直接改成Generate更明确一些
class Generate :public QObject, public QRunnable
{
    Q_OBJECT
public:
    explicit Generate(QObject *parent = nullptr);

    //将主线程传递过来的数保存到m_num
    void recvNum(int num);
    void run() override;

signals:
    void sendArray(QVector<int>);

private:
    int m_num;

};


class BubbleSort :public QObject, public QRunnable
{
    Q_OBJECT
public:
    explicit BubbleSort(QObject *parent = nullptr);

    //将主线程传递过来的数保存到m_num
    void recvArray(QVector<int> list);
    void run() override;

signals:
    void finish(QVector<int>);

private:
    QVector<int> m_list;

};

class QuickSort :public QObject, public QRunnable
{
    Q_OBJECT
public:
    explicit QuickSort(QObject *parent = nullptr);

    //将主线程传递过来的数保存到m_num
    void recvArray(QVector<int> list);
    void run() override;

private:
    void quickSort(QVector<int> &list,int l, int r);

signals:
    void finish(QVector<int>);

private:
    QVector<int> m_list;

};

#endif // MYTHREAD_H

 mythread.cpp

#include "mythread.h"
#include<QElapsedTimer>
#include<QDebug>
#include<QThread>

Generate::Generate(QObject *parent) :QObject(parent),QRunnable()
{
    //设置自动析构
    setAutoDelete(true);
}

void Generate::recvNum(int num)
{
    m_num = num;
}

void Generate::run()
{
    qDebug() << "生成随机数的线程的线程地址:" << QThread::currentThread();
    QVector<int> list;
    //计时
    QElapsedTimer time;
    time.start();

    for(int i=0; i<m_num; ++i)
    {
        list.push_back(qrand() % 100000);
    }
    int milsec = time.elapsed();
    qDebug() << "生成" << m_num << "随机数总用时:" << milsec << "毫秒";
    //发送给主线程
    emit sendArray(list);
}

BubbleSort::BubbleSort(QObject *parent):QObject(parent),QRunnable()
{
    //设置自动析构
    setAutoDelete(true);
}

void BubbleSort::recvArray(QVector<int> list)
{
    m_list = list;
}

void BubbleSort::run()
{
    qDebug() << "冒泡排序的线程的线程地址:" << QThread::currentThread();
    //计时
    QElapsedTimer time;
    time.start();

    //冒泡排序
    int temp;
    for(int i=0;i<m_list.size();++i)
    {
        for(int j=0;j<m_list.size()-i-1;++j)
        {
            if(m_list[j] > m_list[j+1])
            {
                temp = m_list[j];
                m_list[j] = m_list[j+1];
                m_list[j+1] = temp;
            }
        }
    }
    int milsec = time.elapsed();
    qDebug() << "冒泡排序用时:" << milsec << "毫秒";
    emit finish(m_list);
}

QuickSort::QuickSort(QObject *parent):QObject(parent),QRunnable()
{
    //设置自动析构
    setAutoDelete(true);
}

void QuickSort::recvArray(QVector<int> list)
{
    m_list = list;
}

void QuickSort::run()
{
    qDebug() << "快速排序的线程的线程地址:" << QThread::currentThread();
    //计时
    QElapsedTimer time;
    time.start();

    //快速排序
    quickSort(m_list,0,m_list.size()-1);


    int milsec = time.elapsed();
    qDebug() << "快速排序用时:" << milsec << "毫秒";
    emit finish(m_list);
}

void QuickSort::quickSort(QVector<int> &s, int l, int r)
{
    if(l<r)
    {
        int i = l,j = r;

        int x = s[l];
        while(i < j)
        {
            while(i < j && s[j] >= x)
            {
                j--;
            }
            if(i < j)
            {
                s[i++] = s[j];
            }

            while(i < j && s[i] < x)
            {
                i++;
            }
            if(i < j)
            {
                s[j--] = s[i];
            }
        }
        s[i] = x;
        quickSort(s,l,i-1);
        quickSort(s,i+1,r);
    }
}

 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

signals:
    void starting(int num);

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

 mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<mythread.h>
#include<QThreadPool>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //1.创建任务对象
    Generate* gen = new Generate;
    BubbleSort* bubble = new BubbleSort;
    QuickSort* quick = new QuickSort;


    connect(this,&MainWindow::starting,gen,&Generate::recvNum);

    //2.启动子线程
    connect(ui->start,&QPushButton::clicked,this,[=]()
    {
        emit starting(10000);
        QThreadPool::globalInstance()->start(gen);
    });

    //随机数子线程发送来的数据触发冒泡排序和快速排序接收数据
    connect(gen,&Generate::sendArray,bubble,&BubbleSort::recvArray);
    connect(gen,&Generate::sendArray,quick,&QuickSort::recvArray);

    connect(gen,&Generate::sendArray,this,[=](QVector<int> list)
    {
        //启动两个任务
        QThreadPool::globalInstance()->start(bubble);
        QThreadPool::globalInstance()->start(quick);
        for (int i=0; i<list.size(); ++i) {
            ui->randlist->addItem(QString::number(list.at(i)));
        }
    });

    //两个排序处理数据
    connect(bubble,&BubbleSort::finish,this,[=](QVector<int> list)
    {
        for (int i=0; i<list.size(); ++i) {
            ui->bubblelist->addItem(QString::number(list.at(i)));
        }
    });
    connect(quick,&QuickSort::finish,this,[=](QVector<int> list)
    {
        for (int i=0; i<list.size(); ++i) {
            ui->quicklist->addItem(QString::number(list.at(i)));
        }
    });
}

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

main.cpp 

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qRegisterMetaType<QVector<int>>("QVector<int>");
    MainWindow w;
    w.show();
    return a.exec();
}

④线程安全

实现线程互斥和同步常用的类有:

  • 互斥锁:QMutex、QMutexLocker
  • 条件变量:QWaitCondition
  • 信号量:QSemaphore
  • 读写锁:QReadLocker、QWriteLocker、QReadWriteLock

多个线程加锁的对象,得是同一个对象,不同对象,此时不会产生锁的互斥,也就无法把并发执行变为串行执行

1>互斥锁

互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在Qt中,互斥锁主要是通过QMutex类来处理。

  • OMutex

        特点:QMutex是Qt框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。

        用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全

QMutex mutex;

mutex.lock();//上锁
//访问共享资源
//……
mutex.unlock();//解锁
  • OMutexLocker

        特点:OMutexLockerQMutex的辅助类,使用RAII(Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作。

        用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题

QMutex mutex;
{
    QMutexLocker locker(&mutex);//在作用域内自动上锁
    //访问共享资源
    //...
}//在作用域结束时自动解锁
  1. QMutex mutex; 创建了一个 QMutex 对象。
  2. 在大括号 {} 内,创建了 QMutexLocker 的实例 locker,它接受 mutex 的地址作为参数。这将锁定 mutex
  3. 在大括号内,可以安全地访问共享资源,因为互斥量已经被锁定,防止了其他线程同时访问。
  4. 当离开大括号的作用域时,locker 的实例被销毁,它的析构函数会自动调用 mutex.unlock(),从而释放互斥量。
  • QReadWriteLockerQReadLockerQWriteLocker

特点:

  • QReadWriteLock读写锁类,用于控制读和写的并发访问。
  • QReadLocker用于读操作上锁,允许多个线程同时读取共享资源。
  • QWriteLocker用于写操作上锁,只允许一个线程写入共享资源。

用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。

OReadWriteLock rwLock;
//在读操作中使用读锁
{
    QReadLocker locker(&rwLock);//在作用域内自动上读锁
    //读取共享资源
    //……
}//在作用域结束时自动解读锁

//在写操作中使用写锁
{
    QWriteLocker locker(&rwLock);//在作用域内自动上写锁
    //修改共享资源
    //……
}//在作用域结束时自动解写锁
2>条件变量

在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。

在Qt中,专门提供了QWaitCondition类来解决像上述这样的问题。

特点:QWaitCondition是Qt框架提供的条件变量类,用于线程之间的消息通信和同步。

用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。

3>信号量

有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量

特点:QSemaphore是Qt框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。

用途:限制并发线程数量,用于解决一些资源有限的问题。

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

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

相关文章

Java:Thread类以及线程状态

文章目录 Thread类等待一个线程 - join()获取当前线程的引用sleep 线程状态 Thread类 等待一个线程 - join() 操作系统,针对多个线程的执行,是一个"随机调度,抢占式执行“的过程. 线程等待就是在确定两个线程的"结束顺序”. 我们无法确定两个线程调度执行的顺序,但…

找工作,如何写一份好的简历? 附简洁大方的简历模板

一份精心制作的简历对给潜在雇主留下积极的第一印象至关重要。这是你展示技能、经验和成就的第一机会&#xff0c;因此制作一份出色的简历至关重要。下面是一个指南&#xff0c;帮助你创建一份出色的简历&#xff0c;参考一个专业的模板。 1. 联系信息 在简历顶部提供你的联系…

设计模式 - Singleton pattern 单例模式

文章目录 定义单例模式的实现构成构成UML图 单例模式的六种实现懒汉式-线程不安全懒汉式-线程安全饿汉式-线程安全双重校验锁-线程安全静态内部类实现枚举实现 总结其他设计模式文章&#xff1a;最后 定义 单例模式是一种创建型设计模式&#xff0c;它用来保证一个类只有一个实…

MATLAB优化模型(4)

一、前言 在MATLAB中&#xff0c;你可以使用内置的遗传算法(Genetic Algorithm)、模拟退火(Simulated Annealing)等优化工具箱函数&#xff0c;或者编写自定义代码来实现(Ant Colony Optimization, ACO) 蚁群算法和粒子群算法(Particle Swarm Optimization, PSO)。以下是一些基…

日撸Java三百行(day12:顺序表二)

目录 一、关于昨天的补充 1.final关键字 2.toString()方法 二、今日代码实现 1.顺序表的查找操作 2.顺序表的插入操作 3.顺序表的删除操作 4.数据测试 总结 一、关于昨天的补充 1.final关键字 public static final int MAX_LENGTH 10; 在昨天的这行代码中&#xf…

OpenCV||超详细的灰度变换和直方图修正

一、点运算 概念&#xff1a;点运算&#xff08;也称为像素级运算或单像素操作&#xff09;是指对图像中每一个像素点进行独立、相同的操作&#xff0c;而这些操作不会考虑像素点之间的空间关系。点处理优势也称对比度拉伸、对比度增强或灰度变换等。 目的&#xff1a;点运算…

操作系统|day3.锁、I/O多路复用、中断

协程 概念 协程是微线程&#xff0c;在子程序内部执行&#xff0c;可在子程序内部中断&#xff0c;转而执行别的子程序&#xff0c;在适当的时候再返回来接着执行。 优势 协程调用跟切换比线程效率高&#xff1a;协程执行效率极高。协程不需要多线程的锁机制&#xff0c;可…

项目经验分享:用4G路由器CPE接海康NVR采用国标GB28181协议TCP被动取流一段时间后设备就掉线了

最近我们在做一个生态化养殖的项目时&#xff0c;发现一个奇怪的现象&#xff1a; 项目现场由于没有有线网络&#xff0c;所以&#xff0c;我们在现场IPC接入到海康NVR之后&#xff0c;再通过一款4G的CPE接入到天翼云的国标GB28181视频平台&#xff1b;我们采用UDP协议播放NVR…

BERT模型

BERT模型是由谷歌团队于2019年提出的 Encoder-only 的 语言模型&#xff0c;发表于NLP顶会ACL上。原文题目为&#xff1a;《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》链接 在前大模型时代&#xff0c;BERT模型可以算是一个参数量比…

杂记123

(前提已安装了Beyond Compare4)在Everything的文件-右键菜单项里没有Beyond Compare的"选择左边文件进行比较"的现象 进过调查,LZ本机是X64位的,但是安装了x86(32位)的Everything, 切换成X64位的Everything版本就好了

数据结构之《二叉树》(中)

在数据结构之《二叉树》(上)中学习了树的相关概念&#xff0c;还了解的树中的二叉树的顺序结构和链式结构&#xff0c;在本篇中我们将重点学习二叉树中的堆的相关概念与性质&#xff0c;同时试着实现堆中的相关方法&#xff0c;一起加油吧&#xff01; 1.实现顺序结构二叉树 在…

详细测评下搬瓦工香港CN2 GIA VPS

搬瓦工香港VPS分移动CMI和电信CN2 GIA两个大类&#xff0c;一个属于骨干网&#xff0c;一个属于轻负载。搬瓦工的香港CN2 GIA根据测试来看实际上是CN2 GIABGP&#xff0c;并非三网纯CN2 GIA。详细测评数据如下&#xff1a; 用FIO再给测试一下硬盘I/O&#xff0c;可以仔细看看数…

全网最适合入门的面向对象编程教程:31 Python的内置数据类型-对象Object和类型Type

全网最适合入门的面向对象编程教程&#xff1a;31 Python 的内置数据类型-对象 Object 和类型 Type 摘要&#xff1a; Python 中的对象和类型是一个非常重要的概念。在 Python 中,一切都是对象,包括数字、字符串、列表等,每个对象都有自己的类型。 原文链接&#xff1a; Fre…

WebSocket 协议介绍

前言 一.通用协议设计 参考链接 /* --------------------------------------------------------------- | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | --------------------------------------------------------------- | 状态 1byte | …

前端HTML+CSS查漏补缺——仿制百度搜索首页的一些思考

在像素模仿百度搜索首页的时候&#xff0c;在实现的时候&#xff0c;遇到了一些值得记录的点。 在这个过程中&#xff0c;也顺便看了看百度的源码&#xff0c;感觉很有意思。 对了&#xff0c;QQ截屏里面获取到的颜色&#xff0c;是不大正确的&#xff0c;会有点误差。 这是我…

TypeError: ‘float’ object is not iterable 深度解析

TypeError: ‘float’ object is not iterable 深度解析与实战指南 在Python编程中&#xff0c;TypeError: float object is not iterable是一个常见的错误&#xff0c;通常发生在尝试对浮点数&#xff08;float&#xff09;进行迭代操作时。这个错误表明代码中存在类型使用不…

Study--Oracle-08-ORACLE数据备份与恢复(一)

一、ORACLE数据保护方案 1、oracle数据保护方案 2、数据库物理保护方案 oracle数据库备份可以备份到本地集群存储&#xff0c;也可以备份到云存储。 3、数据库逻辑数据保护方案 二、ORACLE数据体系 1、ORACLE 数据库的存储结构 2、oracle物理和逻辑存储结构 3、数据库进程 4…

OpenCV||超简略的Numpy小tip

一、基本类型 二、数组属性 三、数组迭代&#xff08;了解&#xff09; import numpy as np # 创建一个数组 a np.arange(6).reshape(2, 3) # 使用np.nditer遍历数组 for x in np.nditer(a): print(x) np.nditer有多个参数&#xff0c;用于控制迭代器的行为&#xff…

一层5x1神经网络绘制训练100轮后权重变化的图像

要完成这个任务&#xff0c;我们可以使用Python中的PyTorch库来建立一个简单的神经网络&#xff0c;网络结构只有一个输入层和一个输出层&#xff0c;输入层有5个节点&#xff0c;输出层有1个节点。训练过程中&#xff0c;我们将记录权重的变化&#xff0c;并在训练100轮后绘制…

显示学习5(基于树莓派Pico) -- 彩色LCD的驱动

和这篇也算是姊妹篇&#xff0c;只是一个侧重SPI协议&#xff0c;一个侧重显示驱动。 总线学习3--SPI-CSDN博客 驱动来自&#xff1a;https://github.com/boochow/MicroPython-ST7735 所以这里主要还是学习。 代码Init def __init__( self, spi, aDC, aReset, aCS) :"&…