QThread多线程详解

news2025/1/10 15:59:43

本文结构如下

文章目录

    • 本文结构如下
  • 1.概述
  • 2.开始多线程之旅
    • 2.1应该把耗时代码放在哪里?
    • 2.2再谈moveToThread()
  • 3.启动线程前的准备工作
    • 3.1开多少个线程比较合适?
    • 3.2设置栈大小
  • 4.启动线程/退出线程
    • 4.1启动线程
    • 4.2优雅的退出线程
  • 5.操作运行中的线程
    • 5.1获取状态
    • 5.2操作线程
  • 6. 为每个线程提供独立数据
  • 附:所有函数


1.概述

在阅读本文之前,你需要了解进程和线程相关的知识,详情参考《Qt 中的多线程技术》
在很多文章中,人们倾向于把 QThread 当成线程的实体,区区创建一个 QThread 类对象就被认为是开了一个新线程,当然这种讨巧的看法似乎能快速的让我们入门但是只要深入多线程编程领域后就会发现这种看法越来越站不住脚,甚至编写的代码脱离我们的控制,代码越写越复杂。最典型的问题就是"明明把耗时操作代码放入了新线程,可实际仍在旧线程中运行"。造成这种情况的根源在于继承 QThread 类,并在 run()函数中塞入耗时操作代码
追溯历史,在 Qt4.4版本以前的 QThread 类是个抽象类,要想编写多线程代码唯一的做法就是继承 QThread 类。但是之后的版本中,Qt库完善了线程的亲和性以及信号槽机制,我们有了更为优雅的使用线程的方式,即 QObiect:moveToThread()这也是官方推荐的做法,遗憾的是网上大部分教程没有跟上技术的进步,依然采用 run() 这种腐朽的方式来编写多线程程序。

2.开始多线程之旅

在 Qt4.4版本后,之所以 Qt官方对 QThread 类进行了大刀阔斧地改革,我认为这是想让多线程编程更加符合 C++ 语言的[面向对象]特性,继承的本章是扩展基类的功能,所以继承 QThread 并把耗时操作代码塞入 run() 函数中的做法怎么看都感觉不伦不类。

2.1应该把耗时代码放在哪里?

暂时不考虑多线程,先思考这样一个问题:想想我们平时会把耗时操作代码放在哪里?一个类中。那么有了多线程后,难道我们要把这段代码从类中剥离出来单独放到某个地方吗?显然这是很糟糕的做法。QObject 中的 moveToThread() 函数可以在不破坏类结构的前提下依然可以在新线程中运行(方法实现直接写在QThread中通过事件循环调用见另一篇博客《QThread与事件循环》)。
在这里插入图片描述
假设现在我们有个 QObject 的子类 Worker,这个类有个成员函数 doSomething(),该函数中运行的代码非常耗时。此时我要做的就是将这个类对象“移动”到新线程里,这样 Worker 的所有成员函数就可以在新线程中运行了。那么如何触发这些函数的运行呢?信号槽。在主线程里需要有个 signal 信号来关联并触发 Worker 的成员函数,与此同时 Worker 类中也应该有个 signal 信号用于向外界发送运行的结果。这样思路就清晰了,Worker 类需要有个槽函数用于执行外界的命令,还需要有个信号来向外界发送结果。如下列代码:

Worker.h:

// Worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>

class Worker : public QObject
{
  Q_OBJECT
public:
  explicit Worker(QObject *parent = nullptr);

signals:
  void resultReady(const QString &str); // 向外界发送结果

public slots:
  void on_doSomething(); // 耗时操作
};
#endif // WORKER_H

Worker.cpp:

// Worker.cpp
#include "worker.h"
#include <QDebug>
#include <QThread>

Worker::Worker(QObject *parent) : QObject(parent)
{
}

void Worker::on_doSomething()
{
  qDebug() << "I'm working in thread:" << QThread::currentThreadId();
  emit resultReady("Hello");
}

Controller.h:

// Controller.h
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include <QThread>
#include "worker.h"

class Controller : public QObject
{
  Q_OBJECT
public:
  explicit Controller(QObject *parent = nullptr);
  ~Controller();

  void start();

signals:
  void startRunning(); // 用于触发新线程中的耗时操作函数

public slots:
  void on_receivResult(const QString &str); // 接收新线程中的结果

private:
  QThread m_workThread;
  Worker *m_worker;
};
#endif // CONTROLLER_H

在作为“外界”的 Controller 类中,由于要发送命令与接收结果,因此同样是有两个成员:startRunning() 信号用于启动 Worker 类的耗时函数运行,on_receivResult() 槽函数用于接收新线程的运行结果。注意别和 Worker 类的两个成员搞混了,在本例中信号对应着槽,即“外界”的信号触发“新线程”的槽,“外界”的槽接收“新线程”的信号结果。

Controller.cpp:

// Controller.cpp
#include "controller.h"
#include <QThread>
#include <QDebug>

Controller::Controller(QObject *parent) : QObject(parent)
{
  qDebug() << "Controller's thread is :" << QThread::currentThreadId();

  m_worker = new Worker();
  m_worker->moveToThread(&m_workThread);

  connect(this, &Controller::startRunning, m_worker, &Worker::on_doSomething);
  connect(&m_workThread, &QThread::finished, m_worker, &QObject::deleteLater);
  connect(m_worker, &Worker::resultReady, this, &Controller::on_receivResult);

  m_workThread.start();
}

Controller::~Controller()
{
  m_worker = nullptr;
  m_workThread.quit();
  m_workThread.wait();
}

void Controller::start()
{
  emit startRunning();
}

void Controller::on_receivResult(const QString &str)
{
  qDebug() << str;
}

在 Controller 类的实现里,首先实例化一个 Worker 对象并把它“移动”到新线程中,然后就是在新线程启动前将双方的信号槽连接起来。同 Worker 类一样,为了体现是在不同线程中执行的,我们在构造函数中打印当前线程 ID。

main.cpp:

// main.cpp
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include "controller.h"

int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);

  qDebug() << "The main threadID is :" << QThread::currentThreadId();
  Controller controller;
  controller.start();

  return a.exec();
}

在 main.cpp 中我们实例化一个 Controller 对象,并运行 start() 成员函数发射出信号来触发 Worker 类的耗时操作函数。来看看运行结果:
在这里插入图片描述
从结果可以看出,Worker 类对象的成员函数是在新线程中运行的。而 Controller 对象是在主线程中被创建,因此它就隶属于主线程。

2.2再谈moveToThread()

“移动到新线程”是一个很形象的描述,作为入门的认知是可以的,但是它的本质是改变线程亲和性(也叫关联性)。为什么要强调这一点?这是因为如果你天真的认为 Worker 类对象整体都移动到新线程中去了,那么你就会本能的认为 Worker 类对象的控制权是由新线程所属,然而事实并不是如此。「在哪创建就属于哪」这句话放在任何地方都是适用的。比如上一节的例子中,Worker 类对象是在 Controller 类中创建并初始化,因此该对象是属于主线程的。而 moveToThread() 函数的作用是将槽函数在指定的线程中被调用。当然,在新线程中调用函数的前提是该线程已经启动处于就绪状态,所以在上一节的 Controller 构造函数中,我们把各种信号槽连接起来后就可以启动新线程了。

使用 moveToThread() 有一些需要注意的地方,首先就是类对象不能有父对象否则无法将该对象“移动”到新线程。如果类对象保存在栈上,自然销毁由操作系统自动完成;如果是保存在堆上,没有父对象的指针要想正常销毁,需要将线程的 finished() 信号关联到 QObject 的deleteLater() 让其在正确的时机被销毁。其次是该对象一旦“移动”到新线程,那么该对象中的计时器(如果有 QTimer 等计时器成员变量)将重新启动。不是所有的场景都会遇到这两种情况,但是记住这两个行为特征可以避免踩坑。

在使用 moveToThread 函数时,需要注意以下几点:

  • 只有QObject对象可以使用moveToThread函数,其他对象不能使用。
  • 一旦调用了moveToThread函数,这个对象的线程上下文就会改变,因此在调用该函数之后,这个对象所属的线程上下文不能再使用。
  • 如果对象正在执行某个函数,而该函数又没有使用线程锁,那么在移动对象之后,该函数仍然会在原来的线程中执行。因此,在移动对象之前,需要确保该对象不处于执行状态。
  • 如果一个QObject对象的子对象也需要移动到新线程中,那么这些子对象也必须调用moveToThread函数进行移动。

3.启动线程前的准备工作

3.1开多少个线程比较合适?

说“开线程”其实是不准确的,这种事儿只有操作系统才能做,我们所能做的是管理其中一个线程。无论是 QThread thread 还是 QThread *thread,创建出来的对象仅仅是作为操作系统线程的接口,用这个接口可以对线程进行一些操作。虽然这样说不准确,但下文我们仍以“开线程”的说法,只是为了表述方便。作为入门教程,能在主线程之外“开”一个线程就已经够了,那么讲解“开”多个线程的内容实在没有必要。本节的目的是想在叩开多线程大门的同时能向里望一望多线程领域的世界,就当是抛砖引玉吧。
在这里插入图片描述
我们来思考这样一个问题:“线程数是不是越大越好”?显然不是,“开”一千个线程是没有意义的。根据《 Qt 中的多线程技术》中所讲的,线程的切换是要消耗系统资源的,频繁的切换线程会使性能降低。线程太少的话又不能完全发挥 CPU 的性能。一般后端服务器都会设置最大工作线程数,不同的架构师有着不同的经验,有些业务设置为 CPU 逻辑核心数的4倍,有的甚至达到32倍。如上图所示,Chrome 浏览器运行时就开了36个线程。

So, the minimum number of threads is equal to the number of available cores. If all tasks are computation intensive, then this is all we need. Having more threads will actually hurt in this case because cores would be context switching between threads when there is still work to do. If tasks are IO intensive, then we should have more threads.

When a task performs an IO operation, its thread gets blocked. The processor immediately context switches to run other eligible threads. If we had only as many threads as the number of available cores, even though we have tasks to perform, they can’t run because we haven’t scheduled them on threads for the processors to pick up.

If tasks spend 50 percent of the time being blocked, then the number of threads should be twice the number of available cores. If they spend less time being blocked—that is, they’re computation intensive—then we should have fewer threads but no less than the number of cores. If they spend more time being blocked—that is, they’re IO intensive—then we should have more threads, specifically, several multiples of the number of cores.

So, we can compute the total number of threads we’d need as follows:
Number of threads = Number of Available Cores / (1 - Blocking Coefficient)

在 Venkat Subramaniam 博士的《 Programming Concurrency on the JVM》这本书中提到关于最优线程数的计算,即线程数量 = 可用核心数/(1 - 阻塞系数)。可用核心数就是所有逻辑 CPU 的总数,这可以用 QThread::idealThreadCount() 静态函数获取,比如双核四线程的 CPU 的返回值就是4。但是阻塞系数比较难计算,这需要用一些性能分析工具来辅助计算。如果只是粗浅的计算下线程数,最简单的办法就是 CPU 核心数 * 2 + 2。更为精细的找到最优线程数需要不断的调整线程数量来观察系统的负载情况。

3.2设置栈大小

根据《 Qt 中的多线程技术》所述,线程“与进程内的其他线程一起共享这片地址空间,基本上就可以利用进程所拥有的资源而无需调用新的资源”,这里所指的资源之一就是堆栈空间。每个线程都有自己的栈,彼此独立,由编译器分配。一般在 Windows 的栈大小为2M,在 Linux 下是8M。

Qt 提供了获取以及设置栈空间大小的函数:stackSize()、setStackSize(uint stackSize)。其中 stackSize() 函数不是返回当前所在线程的栈大小,而是获取用 stackSize() 函数手动设置的栈大小。如果是用编译器默认的栈大小,该函数返回0,这一点需要注意。为什么要设置栈的大小?这是因为有时候我们的局部变量很大(常见于数组),当超过编译器默认大小时程序就会因为栈溢出而报错,这时候就需要手动设置栈大小了。

在这里插入图片描述

以上文「2.1 我们该把耗时代码放在哪里?」中的代码为例,在 Linux 操作系统环境下,假如我们在 on_doSomething() 函数中添加一个9M大小的数组 array,可以看出在程序运行时会由于栈溢出而导致异常退出,因为 Linux 默认的栈空间仅为8M。
在这里插入图片描述

如果我们设置了栈大小为10m,那么程序会正常运行,不会出现栈溢出的问题。

4.启动线程/退出线程

4.1启动线程

调用 start() 函数就可以启动函数在新线程中运行,运行后会发出 started() 信号。

在「1.概述」中我们知道将耗时函数放入新线程有 moveToThread() 和继承 QThread 且重新实现 run() 函数两种方式。有这么一种情况:此时我有 fun1() 和 fun2() 两个耗时函数,将 fun1() 中的代码放入 run() 函数,而将 fun2() 以 moveToThread() 的方式也放到这个线程中。那新线程该运行哪个函数呢?**其实调用 start() 函数后,新线程会优先执行 run() 中的代码,即先执行 fun1() 函数,其次才会运行 fun2() 函数。**这种情况不常见,但了解这种先后顺序有助于我们理解 start() 函数。

说到 run() 函数就不得不提 exec() 函数。这是个 protected 函数,因此只能在类内使用。默认 run() 函数会调用 exec() 函数,即启用一个局部的不占 CPU 的事件循环。为什么要默认启动个事件循环呢?这是因为没有事件循环的话,耗时代码只要执行完线程就会退出,频繁的开销线程显然很浪费资源。因此,如果使用上述第二种“开线程”的方式,别忘了在 run() 函数中调用 exec() 函数。

4.2优雅的退出线程

退出线程可是个技术活,不是随随便便就可以退出。比如我们关闭主进程的同时,里面的线程可能还处在运行状态,尤其线程上跑着耗时操作。这时候你可以用 terminate() 函数强制终止线程,调用该函数后所有处于等待状态的线程都会被唤醒。该函数是异步的,也就是说调用该函数后虽然获得了返回值,但此时线程依然可能在运行。因此,一般是在后面跟上 wait() 函数来保证线程已退出。当然强制是很暴力的行为,有可能会造成局部变量得不到清理,或者无法解锁互斥关系,种种行为都是很危险的,除非必要时才会使用该函数。

上文「4.1 启动线程」结尾提到“默认 run() 函数会调用 exec() 函数”,耗时操作代码执行完后,线程由于启动了事件循环是不退出的。所以,正常的退出线程其实质是退出事件循环,即执行exit(int returnCode = 0) 函数。返回0代表成功,其他非零值代表异常。quit() 函数等价于 exit(0)。线程退出后会发出 finished() 信号。

5.操作运行中的线程

5.1获取状态

(1)运行状态
根据《Qt 中的多线程技术》中的「1.3 线程的生命周期」所述,线程的状态有很多种,而往往我们只关心一个线程是运行中还是已经结束。QThread 提供了 isRunning()、isFinished() 两个函数来判断当前线程的运行状态。

(2)线程标识

Returns the thread handle of the currently executing thread.

Warning: The handle returned by this function is used for internal purposes and should not be used in any application code.

Note: On Windows, this function returns the DWORD (Windows-Thread ID) returned by the Win32 function GetCurrentThreadId(), not the pseudo-HANDLE (Windows-Thread HANDLE) returned by the Win32 function GetCurrentThread().

关于 currentThreadId() 函数,很多人将该函数用于输出线程ID,这是错误的用法。该函数主要用于 Qt 内部,不应该出现在我们的代码中。那为什么还要开放这个接口?这是因为我们有时候想和系统线程进行交互,而不同平台下的线程 ID 表示方式不同。因此调用该函数返回的 Qt::HANDLE 类型数据并转化成对应平台的线程 ID 号数据类型(例如 Windows 下是 DWORD 类型),利用这个转化后的 ID 号就可以与系统开放出来的线程进行交互了。当然,这就破坏了移植性了。

需要注意的是,这个 Qt::HANDLE 是 ID 号而不是句柄。句柄相当于对象指针,一个线程可以被多个对象所操控,而每个线程只有一个全局线程 ID 号。正确的获取线程 ID 做法是:调用操作系统的线程接口来获取。以下是不同平台下获取线程 ID 的代码:

#include <QCoreApplication>
#include <QDebug>

#ifdef Q_OS_LINUX
#include <pthread.h>
#endif

#ifdef Q_OS_WIN
#include <windows.h>
#endif

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

#ifdef Q_OS_LINUX
    qDebug() << pthread_self();
#endif

#ifdef Q_OS_WIN
    qDebug() << GetCurrentThreadId();
#endif

    return a.exec();
}

我们自己的程序内部可以调用 currentThread() 函数来获取 QThread 指针,有了线程指针就可以对线程进行一些操作了。

(3)更为精细的事件处理
在《Qt 中的事件系统 - 有什么难的呢?》一文中我们提到事件的整个运行流程,文中所提及的 QCoreApplication::processEvents() 等传递事件方法其实是很简单的,但如果再深入下去就无能为力了。Qt 提供了 QAbstractEventDispatcher 类用于更为精细的事件处理,该类精细到可以管理 Qt 事件队列,即接收到事件(来自操作系统或者 Qt 写的程序)后负责发送到 QCoreApplication 或者 QApplication 实例以进行处理。而文中讲的是从 QCoreApplication 接收到事件开始,再往后的事情了。

线程既然可以开启事件循环,那么就可以调用 eventDispatcher()、setEventDispatcher() 函数来设置和获取事件调度对象,然后对事件进行更为精细的操作。

除此以外,loopLevel() 函数可以获取有多少个事件循环在线程中运行。正如下文所说,这个函数本来在 Qt 4 中被删除了,但是对于那些想知道有多少事件循环的人来说该函数还是有用的。所以在 Qt 5 中又加了进来。

This function used to reside in QEventLoop in Qt 3 and was deprecated in Qt 4. However this is useful for those who want to know how many event loops are running within the thread so we just make it possible to get at the already available variable.

5.2操作线程

(1)安全退出线程必备函数:wait(unsigned long time = ULONG_MAX)

在本文「4.2 优雅的退出线程」中已经提到“一般是在后面跟上 wait() 函数来保证线程已退出”,线程退出的时候不要那么暴力,告诉操作系统要退出的线程后,给点时间(即阻塞)让线程处理完。也可以设置超时时间 time,时间一到就强制退出线程。一般在类的析构函数中调用,正如本文开头「2.1 我们该把耗时代码放在哪里?」的示例代码那样:

Controller::~Controller()
{
    m_workThread.quit();
    m_workThread.wait();
}

(2)线程间的礼让行为

这是个很有意思的话题,一般我们都希望每个线程都能最大限度的榨干系统资源,何来礼让之说呢?有时候我们采用多线程并不只是运行耗时代码,而是和主 GUI 线程分开,避免主界面卡死的情况发生。那么有些线程上跑的任务可能对实时性要求不高,这时候适当的缩短被 CPU 选中的机会可以节约出系统资源
在这里插入图片描述
除了调用 setPriority()、priority() 优先级相关的函数以外,QThread 类还提供了 yieldCurrentThread() 静态函数,该函数是在通知操作系统“我这个线程不重要,优先处理其他线程吧”。当然,调用该函数后不会立马将 CPU 计算资源交出去,而是由操作系统决定。

QThread 类还提供了 sleep()、msleep()、usleep() 这三个函数,这三个函数也是在通知操作系统“在未来 time 时间内我不参与 CPU 计算”。从我们直观的角度看,就好像当前线程“沉睡”了一段时间。

(3)线程的中断标志位

Qt 为每一个线程都设置了一个布尔变量用来标记当前线程的终端状态,用 isInterruptionRequested() 函数来获取,用 requestInterruption() 函数来设置中断标记。这个标记不是给操作系统看的,而是给用户写的代码中进行判断。也就是说调用 requestInterruption() 函数并不能中断线程,需要我们自己的代码去判断。这有什么用处呢?

while (ture) {
    if (!isInterruptionRequested()) {
        // 耗时操作
        ......
    }
}

这种设计可以让我们自助的中断线程,而不是由操作系统强制中断。经常我们会在新线程上运行无限循环的代码,在代码中加上判断中断标志位可以让我们随时跳出循环。好处就是给了我们程序更大的灵活性。

6. 为每个线程提供独立数据

思考这样一个问题,如果线程本身存在全局变量,那么修改一处后另一个线程会不会受影响?我们以一段代码为例:

// main.cpp
#include <QCoreApplication>
#include "workthread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    WorkThread thread1;
    WorkThread thread2;

    thread1.start();
    thread2.start();

    return a.exec();
}

// WorkThread.h
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>

class WorkThread : public QThread
{
public:
    WorkThread();

protected:
    virtual void run() override;
};
#endif

// WorkThread.cpp
#include "workthread.h"
#include <QDebug>
#include <QThreadStorage>

WorkThread::WorkThread()
{
}

quint64 g_value1 = 0;
void WorkThread::run()
{
    g_value1 = quint64(currentThreadId());
    qDebug() << g_value1;
}

我们继承 QThread 类并重写 run() 函数,函数中的全局变量 g_value1 由线程 ID 赋值。实例化出两个线程对象并均启动。其结果输出如下:
在这里插入图片描述
可以看到两个输出的结果是一样的,这并非是同一个线程输出两次,而是线程 thread1 对全局变量的修改影响了线程 thread2。造成这个现象的原因也很好理解,根据《Qt 中的多线程技术》中的「1.2 多核CPU」所述,“线程隶属于某一个进程,与进程内的其他线程一起共享这片地址空间”。也就是说全局变量属于公共资源,被所有线程所共享,只要一个线程修改了这个全局变量自然就会影响其他线程对该全局变量的访问。

而 QThreadStorage 类为每个线程提供了独立的数据存储功能,即使在线程中用到全局变量,只要存在 QThreadStorage 中,也不会影响到其他线程。我们对上面的 workthread.cpp 进行稍加修改,从结果来看,每个线程都有属于各自的全局变量,而互不影响。如下图所示:
在这里插入图片描述
需要注意的是,QThreadStorage 的析构函数并不会删除所储存的数据,只有线程退出才会被删除。

附:所有函数

启动前的准备工作
	构造函数:QThread(QObject *parent = nullptr)
	系统理想的线程数量:[static]int idealThreadCount()
	堆栈大小
		获取:uint stackSize() const
		设置:void setStackSize(uint stackSize)
启动/退出
	启动
		执行:[slots]void start(QThread::Priority priority = InheritPriority)
		信号:void started()
		事件循环:[protected]int exec()
	退出
		执行
			void exit(int returnCode = 0)
			[slots]void quit()
		信号:void finished()
		强制中止[不常用]
			[slots]void terminate()
			[static protected]void setTerminationEnabled(bool enabled = true)
运行中
	状态获取
		是否运行
			bool isRunning() const
			bool isFinished() const
		标识:
			当前线程:[static]QThread * currentThread()
			线程ID:[static]Qt::HANDLE currentThreadId()
		更为精细的事件管理
			获取:QAbstractEventDispatcher * eventDispatcher() const
			设置:void setEventDispatcher(QAbstractEventDispatcher *eventDispatcher)
			循环级别:int loopLevel() const
	行为
		阻塞:bool wait(unsigned long time = ULONG_MAX)
		线程之间的礼让
	优先级
		获取:QThread::Priority priority() const
		设置:void setPriority(QThread::Priority priority)
	切换线程:[static]void yieldCurrentThread()
	睡眠
		[static]void sleep(unsigned long secs) const
		[static]void msleep(unsigned long msecs)
		[static]void usleep(unsigned long usecs)
	请求中断
		判断:bool isInterruptionRequested() const
		执行:void requestInterruption()
其他(仅支持C++17)
	[static]QThread * create(Function &&f, Args &&... args)
	[static]QThread * create(Function &&f)

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

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

相关文章

深度学习数据集有没有规范或指导意见,数据集的建立都需要做哪些研究工作?

一、数据集的核心原则是什么&#xff1f; 数据集的目标&#xff1a;它需要回答“你要解决什么问题&#xff1f;” 在构建数据集之前&#xff0c;最重要的不是去采集数据&#xff0c;而是明确目标&#xff1a; 你的模型是要做图像分类&#xff0c;还是目标检测&#xff1f;是要…

前端for循环遍历——foreach、map使用

title: 前端不同类型的for循环遍历——foreach、map date: 2025-01-04 11:02:17 tags: javascript 前端不同类型的for循环遍历 场景&#xff1a;很多时候后端发来的数据是不能够完全契合前端的需求的&#xff0c;比如你要一个数据对象中的值&#xff0c;但是这个值却作为了ke…

MR30分布式 IO 在物流分拣线的卓越应用

在当今物流行业高速发展的时代&#xff0c;物流分拣线的高效与精准运作至关重要&#xff0c;而其中对于货物点数较多情况下的有效控制更是一大关键环节。明达技术MR30分布式 IO 系统凭借其独特的优势&#xff0c;在物流分拣线中大放异彩&#xff0c;为实现精准的点数控制提供了…

5 分布式ID

这里讲一个比较常用的分布式防重复的ID生成策略&#xff0c;雪花算法 一个用户体量比较大的分布式系统必然伴随着分表分库&#xff0c;分机房部署&#xff0c;单体的部署方式肯定是承载不了这么大的体量。 雪花算法的结构说明 如下图所示: 雪花算法组成 从上图我们可以看…

Android wifi常见问题及分析

参考 Android Network/WiFi 那些事儿 前言 本文将讨论几个有意思的网络问题&#xff0c;同时介绍 Android 上常见WiFi 问题的分析思路。 网络基础Q & A 一. 网络分层缘由 分层想必大家很熟悉&#xff0c;是否想过为何需要这样分层&#xff1f; 网上大多都是介绍每一层…

音视频入门基础:MPEG2-PS专题(6)——FFmpeg源码中,获取PS流的视频信息的实现

音视频入门基础&#xff1a;MPEG2-PS专题系列文章&#xff1a; 音视频入门基础&#xff1a;MPEG2-PS专题&#xff08;1&#xff09;——MPEG2-PS官方文档下载 音视频入门基础&#xff1a;MPEG2-PS专题&#xff08;2&#xff09;——使用FFmpeg命令生成ps文件 音视频入门基础…

读书笔记:分布式系统原理介绍

写在前面 已经大概三个月左右没有更新博客了&#xff0c;哈哈哈哈&#xff1b; 此博客是笔者在对《分布式系统原理介绍》进行概述&#xff0c;对于整个分布式系统协议的理解基于一些量化的指标考虑了数据的分布副本协议&#xff08;中心化/去中心化&#xff09;进行了总结&…

Dexcap复现代码数据预处理全流程(四)——demo_clipping_3d.py

此脚本的主要功能是可视化点云数据文件&#xff08;.pcd 文件&#xff09;&#xff0c;并通过键盘交互选择演示数据的起始帧和结束帧&#xff0c;生成片段标记文件 (clip_marks.json) 主要流程包括&#xff1a; 用户指定数据目录&#xff1a;检查目录是否存在并处理标记文件 -…

MBM指尖六维力触觉传感器:高灵敏度、低漂移,精准掌控力学世界

MBM指尖六维力触觉传感器是一种专为机器人设计的高性能传感器。它通过集成三轴力和三轴力矩的感知能力&#xff0c;能够精准捕捉复杂的力学信息。传感器采用MEMS与应变体复合测量技术&#xff0c;具备数字输出功能&#xff0c;显著降低漂移并减少安装偏移的影响。其紧凑轻便的设…

C#,图论与图算法,任意一对节点之间最短距离的弗洛伊德·沃肖尔(Floyd Warshall)算法与源程序

一、弗洛伊德沃肖尔算法 Floyd-Warshall算法是图的最短路径算法。与Bellman-Ford算法或Dijkstra算法一样&#xff0c;它计算图中的最短路径。然而&#xff0c;Bellman Ford和Dijkstra都是单源最短路径算法。这意味着他们只计算来自单个源的最短路径。另一方面&#xff0c;Floy…

为答疑机器人扩展问题分类与路由功能

1.意图识别 2. 构建路由模块 简单的意图识别 from chatbot import llmfrom config.load_key import load_key load_key()prompt 【角色背景】 你是一个问题分类路由器&#xff0c;需要识别问题的类型。 --- 【任务要求】 问题的类型目前有&#xff1a;公司内部文档查询、内…

spring boot启动源码分析(三)之Environment准备

上一篇《spring-boot启动源码分析&#xff08;二&#xff09;之SpringApplicationRunListener》 环境介绍&#xff1a; spring boot版本&#xff1a;2.7.18 主要starter:spring-boot-starter-web 本篇开始讲启动过程中Environment环境准备&#xff0c;Environment是管理所有…

Pandas-RFM会员价值度模型

文章目录 一. 会员价值度模型介绍二. RFM计算与显示1. 背景2. 技术点3. 数据4. 代码① 导入模块② 读取数据③ 数据预处理Ⅰ. 数据清洗, 即: 删除缺失值, 去掉异常值.Ⅱ. 查看清洗后的数据Ⅲ. 把前四年的数据, 拼接到一起 ④ 计算RFM的原始值⑤ 确定RFM划分区间⑥ RFM计算过程⑦…

【理论】测试框架体系TDD、BDD、ATDD、DDT介绍

一、测试框架是什么 测试框架是一组用于创建和设计测试用例的指南或规则。框架由旨在帮助 QA 专业人员更有效地测试的实践和工具的组合组成。 这些指南可能包括编码标准、测试数据处理方法、对象存储库、存储测试结果的过程或有关如何访问外部资源的信息。 A testing framewo…

FreeU: Free Lunch in Diffusion U-Net 笔记

FreeU: Free Lunch in Diffusion U-Net 摘要 作者研究了 U-Net 架构对去噪过程的关键贡献&#xff0c;并发现其主干部分主要在去噪方面发挥作用&#xff0c;而其跳跃连接主要是向解码器模块引入高频特征&#xff0c;这使得网络忽略了主干部分的语义信息。基于这一发现&#…

JAVA 使用apache poi实现EXCEL文件的输出;apache poi实现标题行的第一个字符为红色;EXCEL设置某几个字符为别的颜色

设置输出文件的列宽&#xff0c;防止文件过于丑陋 Sheet sheet workbook.createSheet(FileConstants.ERROR_FILE_SHEET_NAME); sheet.setColumnWidth(0, 40 * 256); sheet.setColumnWidth(1, 20 * 256); sheet.setColumnWidth(2, 20 * 256); sheet.setColumnWidth(3, 20 * 25…

【STM32】无源蜂鸣器播放音乐《千与千寻》,HAL库

目录 一、工程链接 二、简单介绍 主要特点&#xff1a; 应用&#xff1a; 驱动电路&#xff1a; 三、原理图 四、cubeMX配置 时钟配置 五、keil配置 六、驱动编写 演奏函数 主函数编写 七、效果展示 八、驱动附录 music.h music.c 一、工程链接 STM32无源蜂鸣…

在 Vue 3 集成 e签宝电子合同签署功能

实现 Vue 3 e签宝电子合同签署功能&#xff0c;需要使用 e签宝提供的实际 SDK 或 API。 e签宝通常提供针对不同平台&#xff08;如 Web、Android、iOS&#xff09;的 SDK&#xff0c;而 Web 端一般通过 WebView 或直接使用嵌入式 iframe 来加载合同签署页面。 下面举个 &…

04、Redis深入数据结构

一、简单动态字符串SDS 无论是Redis中的key还是value&#xff0c;其基础数据类型都是字符串。如&#xff0c;Hash型value的field与value的类型&#xff0c;List型&#xff0c;Set型&#xff0c;ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了…

如何用Python编程实现自动整理XML发票文件

传统手工整理发票耗时费力且易出错&#xff0c;而 XML 格式发票因其结构化、标准化的特点&#xff0c;为实现发票的自动化整理与保存提供了可能。本文将详细探讨用python来编程实现对 XML 格式的发票进行自动整理。 一、XML 格式发票的特点 结构化数据&#xff1a;XML 格式发票…