【C++并发入门】摄像头帧率计算和多线程相机读取(上):并发基础概念和代码实现

news2024/12/25 12:34:58

前言

  • 高帧率摄像头往往应用在很多opencv项目中,今天就来通过简单计算摄像头帧率,抛出一个单线程读取摄像头会遇到的问题,同时提出一种解决方案,使用多线程对摄像头进行读取。
  • 同时本文介绍了线程入门的基础知识,讲解了线程进程的概念,std::thread的使用,std::mutex锁的概念。
  • 本教程使用的环境:
    • opencv C++ 4.5
    • C++11
    • KS1A293黑白240fps摄像头

1 摄像头帧率计算

1-1 概念
  • 摄像头帧率通常指的是视频摄像头每秒钟能够捕捉到的图像数量,单位是帧每秒(fps)。经常打游戏的朋友应该不陌生FPS吧(乐)请添加图片描述
1-2 代码实现
  • 那以我手上的这个240fps的摄像头为例子请添加图片描述

  • 我们简单使用opencv-C++根据摄像机帧率进行简单的FPS计算,并画在图上

#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/core/utils/logger.hpp>
#include <chrono>
#include <thread>


int main() {
    cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_VERBOSE);
    cv::VideoCapture cap(0);

    if (!cap.isOpened()) {
        std::cerr << "open camera failed!" << std::endl;
        return -1;
    }

    cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
    cap.set(cv::CAP_PROP_FRAME_HEIGHT, 400);
    cap.set(cv::CAP_PROP_FPS, 240);
    cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));

    std::chrono::time_point<std::chrono::steady_clock> startTime=std::chrono::steady_clock::now();
    std::chrono::time_point<std::chrono::steady_clock> endTime;
    double fps = 0.0;
    int frame_count = 0;
    cv::Mat frame;
    while (true) {
        bool ret = cap.read(frame);
        if (!ret) {
            break;
        }
        frame_count++;


        endTime = std::chrono::steady_clock::now();
        double timeTaken = std::chrono::duration<double, std::milli>(endTime - startTime).count();
        if (timeTaken >= 1000)
        {
            fps = frame_count;
            startTime = std::chrono::steady_clock::now();
            frame_count = 0;
        }

        cv::putText(frame, std::to_string(int(fps)) + " FPS", cv::Point(frame.cols / 4, frame.rows / 3), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 0, 0), 2);

        
        cv::imshow("Frame", frame);

        if (cv::waitKey(1) == 'q') 
             break;
       

    }

    cap.release();

    return 0;
}

  • 代码很简单,根据FPS的定义使用C++通用时间库chrono可以计算出每1000ms(1s)读到的帧数,得到如下的窗口显示,可以看到有正常使用由于部分硬件限制是可以达到180fps左右的实时帧率的。请添加图片描述
1-3 问题抛出
  • 那我们的cv代码对捕获的图像进行处理就理所应当顺其自然的写在获取到图像的循环中了,那么我拿下述代码模拟我在主循环执行的耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 10ms
  • 我们把上述模拟耗时的代码放入主循环的任意位置,可以看到,摄像头的FPS明显下降请添加图片描述

  • 那我们再做一个实验,模拟平常没有使用高帧率摄像头的情况,我把摄像头的帧率降低到30帧

 cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
 cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
 cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));
 cap.set(cv::CAP_PROP_FPS, 30);
  • 然后我们重复和上述一样的操作增减模拟耗时的代码,发现帧率基本是还是在30fps左右,相信这样一对比大家肯定就明白原因了。如果把相机的读取和处理放在同一个线程下,且摄像头帧率又过快时,图像处理的代码会影响摄像头的读取,那么我们就引入了今天的主题—多线程读取相机。

2 基础概念

  • 在踏入正式的多线程多进程的道路之前,你需要知道点概念。
2-1 线程和进程的概念
  • 线程(Thread)和进程(Process)是操作系统中用于执行程序的基本单位,它们之间有着密切的关系,但也有明显的区别。

    • *进程(Process):进程是操作系统进行资源分配和调度的基本单位。它是程序的一次执行过程,包含了程序运行所需的全部资源,如内存空间、文件描述符、环境变量等。每个进程都有自己的地址空间,一个进程中的数据对其他进程是不可见的。进程之间是相互独立的,一个进程的崩溃不会影响到其他进程。
    • *线程(Thread):线程是进程中的一个实体,被系统独立调度和分派的基本单位。它是进程的执行流,一个进程可以有多个线程,而同一个进程中的所有线程共享进程的资源。线程的切换通常比进程的切换要快,因为线程之间的切换不需要重新加载进程的上下文。
  • 说人话版本就是

    • 进程就像是电脑上运行的独立程序。比如,当你打开一个浏览器、一个文本编辑器或者一个游戏,每个这样的程序在操作系统中都是一个独立的进程。每个进程都有自己的内存空间、数据和其他资源,它们彼此之间是隔离的,一个进程的崩溃通常不会影响到其他进程。最简单的例子就是打开你电脑的任务管理器,每一栏就是一个独立的进程,他们彼此之间不会干架,进程统一由操作系统进行维护。操作系统负责管理和协调这些进程,确保它们可以有效地使用计算机的资源,比如CPU、内存和硬盘空间。请添加图片描述

    • 线程,举个餐厅的例子,我们假定你开了一家餐厅,这个餐厅是一个进程。你的餐厅需要同时处理多个任务,比如迎接顾客、点菜、烹饪和清洁。每个任务可以看作是一个线程。

      • 迎接顾客的员工是一个线程,他们负责接待客人、带位和解释菜单。
      • 点菜的员工是另一个线程,他们记录顾客的点菜信息并传递给厨房。
      • 厨房里的厨师是第三个线程,他们根据点菜信息准备食物。
      • 清洁工是第四个线程,他们负责保持餐厅的清洁。
    • 每个线程都在执行不同的任务,但它们都共享餐厅的资源,比如厨房、餐具和收银台。如果餐厅的生意很好,这些线程可以同时工作,提高效率。如果某个线程(比如厨师)因为某些原因无法工作(比如生病),其他线程(比如迎接顾客和点菜)的工作可能会受到影响,因为顾客点的菜无法及时准备好。请添加图片描述

  • *关系与区别

    • 资源占用:进程是资源分配的单位,每个进程都有自己的资源,如内存空间。而线程共享进程的资源,线程之间共享进程的内存空间、文件描述符等。
    • 执行控制:线程是独立调度的基本单位,一个进程中的线程可以并发执行。而进程是操作系统进行保护和资源分配的基本单位。
    • 上下文切换:线程的上下文切换通常比进程的上下文切换要快,因为线程共享进程的上下文,而进程的上下文切换需要保存和恢复整个进程的状态。
    • 通信方式:进程间的通信通常需要通过操作系统提供的机制,如管道、消息队列、共享内存等。而线程间的通信通常更加直接,因为它们共享进程的内存空间。

2-2 并行和并发
  • 说完线程和进程,不得不提到 并行和并发,并行(Parallelism)并发(Concurrency)是计算机科学中经常讨论的两个概念,它们描述了程序或任务在多处理器或多核系统中的执行方式。请添加图片描述

    • 并发(Concurrency)是指在同一时间间隔内,多个任务可以“同时”开始或结束,但不一定是真正的同时。并发通常发生在单核处理器上,通过操作系统的任务调度器(scheduler)在多个任务之间快速切换,使得每个任务都能得到执行时间。对于用户来说,似乎这些任务是在同时进行的,但实际上是轮流执行。并发是一种逻辑上的同时执行,它允许多个任务交替使用CPU资源。
    • 并行(Parallelism)是指多个任务在同一时刻真正地同时执行。这通常发生在多核处理器或多个处理器上,每个核或处理器可以同时处理不同的任务。并行是一种物理上的同时执行,它能够显著提高计算速度和效率。
  • 还是举个说人话的例子

    • 并发:你可以在一个炉灶上煮汤,同时在另一个炉灶上炒菜,同时电饭锅里头还在煮米饭。虽然你一次只能操作一个炉灶,但你可以快速地在两个炉灶之间切换,使得几个菜看起来像是同时烹饪的。这就像是单核处理器上的并发执行,处理器快速地在多个任务之间切换。
    • 并行:但是假如你有女朋友(不是),那她可以帮助你做饭,每个人操作一个炉灶,那么几个菜肴就可以真正地同时烹饪。这就像是多核处理器上的并行执行,每个核可以独立地处理一个任务。

3 C++线程入门std::thread

  • std::thread 是 C++11 引入的标准库功能,用于创建和管理线程。使用 std::thread,我们可以创建新的执行线程,并在线程中执行函数或 lambda 表达式。
3-1 创建线程
  • std::thread支持你传递一个函数指针、函数对象或 lambda 表达式来创建一个新线程。
#include <iostream>
#include <thread>

void printHello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(printHello); 
    t.join(); // 等待线程完成
    return 0;
}
  • 你也可以直接在 std::thread 构造函数中使用 lambda 表达式:
std::thread t([]() {
    std::cout << "Hello from lambda thread!\n";
});
  • 你可以向线程函数传递参数,这些参数可以是普通变量、引用或 std::move 的对象。
void printMessage(const std::string& msg) {
    std::cout << msg << std::endl;
}

int main() {
    std::string msg = "Hello from thread!";
    std::thread t(printMessage, std::move(msg)); // 传递 msg 到线程函数
    t.join();
    return 0;
}

3-2 join 和 detach
  • joindetachstd::thread 类的两个成员函数,用于管理线程的生命周期。它们的主要作用是控制线程何时结束以及如何与主线程(创建线程的线程)交互。
3-2-1 join
  • 当线程开始执行后,主线程默认会继续执行而不会等待线程完成。如果你希望主线程等待一个线程完成,可以使用 join 方法。这会阻塞当前线程(通常是主线程)直到另一个线程完成执行。
std::thread t(printHello);
t.join(); // 主线程会等待直到 t 线程完成
  • 还是说人话,假设你是一名厨师,你正在准备一顿晚餐。你有一个煎锅,需要同时煎牛排和炒蔬菜。你可以将煎牛排的任务交给一个你滴助手,然后开始炒蔬菜。在这个过程中,你希望确保牛排煎好后,再继续炒蔬菜。这时,你可以告诉你滴助手,在他煎好牛排后,通知你。这个通知的过程就像是 join,你(主线程)会等待你滴助手(线程)完成煎牛排的任务后,再继续炒蔬菜。
std::thread t(cookSteak); // 你滴助手开始煎牛排
t.join(); // 你等待你滴助手煎好牛排
cookVegetables(); // 你开始炒蔬菜
3-2-2 detach
  • 如果你想创建一个独立于主线程的线程,你可以使用 detach 方法。这会告诉操作系统,当线程函数返回时,回收线程资源,而不是等待主线程来回收。这意味着线程和主线程将独立运行,主线程不会等待线程完成。
std::thread t(printHello);
t.detach(); // t 线程将独立运行
  • 同样说人话,假设你是一名厨师,但这次你需要同时处理多个任务。除了煎牛排,你还需要煮米饭和准备沙拉。这些任务可以同时进行,不需要互相等待。在这种情况下,你可以让你滴助手开始煎牛排,然后让他独立完成,不需要通知你。这时候你滴助手会自己处理煎牛排的所有事情,包括清洗煎锅和调味。一旦你滴助手开始煎牛排,你就让他自己处理,你(主线程)可以继续做其他事情,比如煮米饭和准备沙拉。
std::thread t(cookSteak); // 你滴助手开始煎牛排
t.detach(); // 你滴助手独立完成煎牛排,不需要通知你
cookRice(); // 你开始煮米饭
prepareSalad(); // 你准备沙拉
  • 这下懂了吧?

4 竞态条件(race condition)和数据不一致

  • 竞态条件(Race Condition)数据不一致问题多线程编程中常见的问题,它们通常发生在多个线程访问和修改共享数据时。
4-1 竞态条件(Race Condition)
  • 竞态条件是指程序执行的结果依赖于线程调度的顺序,即多个线程以不同的顺序执行时,程序可能会产生不同的结果。竞态条件通常发生在以下几种情况:
    1. 共享数据访问:当多个线程访问和修改同一块共享数据时,如果没有适当的同步机制,就可能导致数据的不一致。
    2. 条件检查与操作:在多线程环境中,一个线程可能会在另一个线程修改共享数据之前检查某个条件,这可能导致条件判断错误。
    3. 信号量或锁的错误使用:不当使用信号量或锁,比如死锁、忘记释放锁等,也可能导致竞态条件。
  • 我们来举个例子
#include <iostream>
#include <thread>

int counter = 0;

void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t2(incrementCounter);
    std::thread t1(incrementCounter);
   

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter << std::endl;
}

  • 在这个例子中,我们创建了两个线程,每个线程都尝试将 counter 的值增加 1000 次。理想情况下,我们期望 counter 的最终值为 2000。然而,由于线程的调度是不确定的,两个线程可能会同时读取和修改 counter 的值,导致最终的 counter 值小于 2000。

  • 我们在VS2022进行测试,前几次都是理想的2000请添加图片描述

  • 然而在我坚持不懈的尝试下(迫真),得到的如下的情况请添加图片描述

  • 值得一提的是为了复现这个效果,你需要在 Visual Studio 中禁用编译器优化。

    1. 打开项目属性。
    2. 导航到“C/C++” > “代码生成”。
    3. 在“优化”下拉菜单中,选择“无 (/Od)”。
    4. 点击“确定”保存更改。请添加图片描述

4-2 数据不一致问题
  • 数据不一致问题是指由于多个线程同时读写共享数据,导致数据的状态变得不可预测或不符合预期。数据不一致问题通常是由竞态条件引起的,因为多个线程没有正确地同步对共享数据的访问。
  • 简单说这两差不多一个意思,但是区别就是:竞态条件是一种可能导致数据不一致的情况,但数据不一致问题可能有其他原因。竞态条件是导致数据不一致的一个常见原因,但不是唯一原因
  • 为了解决竞态条件和数据不一致问题,通常需要使用互斥锁(如 std::mutex)、原子操作(如 std::atomic)或其他同步机制来确保对共享数据的访问是安全的。

5 std::mutex

  • std::mutex 是 C++11 中引入的一个线程同步机制,用于保护共享数据,防止多个线程同时访问同一资源。当多个线程尝试同时访问共享资源时,std::mutex 可以确保一次只有一个线程能够访问该资源,从而避免了竞态条件(race condition)和数据不一致的问题。
  • std::mutex 提供了基本的锁功能,它有两个主要成员函数:
    1. lock(): 当一个线程调用这个函数时,它会尝试获取锁。如果锁当前没有被其他线程持有,这个调用会立即返回,并且锁会被当前线程持有。如果锁已经被其他线程持有,当前线程会被阻塞,直到锁被释放。
    2. unlock(): 当一个线程完成了对共享资源的访问后,它需要调用这个函数来释放锁。一旦锁被释放,其他等待锁的线程中的一个可以获取锁并继续执行。
  • 还是刚刚那个例子
#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx; // 创建一个互斥锁

void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        mtx.lock(); // 加锁
        ++counter;
        mtx.unlock(); // 解锁
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter << std::endl;
    return 0;
}

  • 在这个修改后的代码中,我们创建了一个std::mutex对象mtx。在incrementCounter函数中,我们使用mtx.lock()来加锁,然后递增counter,最后使用mtx.unlock()来解锁。这样,即使两个线程同时尝试递增counter,也只有一个线程可以进入临界区(即加锁后的代码块)。请添加图片描述

4-3死锁(deadlock)
  • 死锁(deadlock)是指两个或多个线程无限期地等待对方释放锁,导致所有线程都无法继续执行。在多线程程序中,死锁通常是由于不正确的锁管理造成的。
  • 死锁的发生通常需要满足以下四个条件,这被称为死锁的四个必要条件:
    1. 互斥条件:至少有一个资源必须处于非共享模式,即一次只能由一个进程(或线程)使用。
    2. 占有和等待条件:一个进程(或线程)至少持有一个资源,并且正在等待获取一个由其他进程(或线程)持有的资源。
    3. 非抢占条件:资源不能被强制从一个进程(或线程)转移到另一个进程(或线程),进程(或线程)只能释放资源。
    4. 循环等待条件:存在一个由两个或多个进程(或线程)组成的循环链,每个进程(或线程)都在等待下一个进程(或线程)持有的资源。
  • 还是说人话环境,我们还是说厨师 (乐),假设有两个厨师,我们称他们为厨师A厨师B。他们共享两个工具:一个锅和一个炉子。厨师A需要先使用锅,然后使用炉子来烹饪食物;而厨师B则需要先使用炉子,然后使用锅来烹饪食物。
    • 现在,让我们来看看如果他们同时开始工作,会发生什么:
      1. 厨师A首先拿起锅开始烹饪。
      2. 厨师B同时拿起炉子开始烹饪。
    • 到这里为止,一切都很正常。但是,当厨师A烹饪完锅里的食物后,他需要使用炉子来加热,但这时炉子已经被厨师B占用。同样,厨师B在烹饪完炉子上的食物后,他需要使用锅来烹饪,但锅已经被厨师A占用。
    • 由于厨师A和厨师B都在等待对方释放他们需要的工具,他们都无法继续工作。这就是一个死锁的情景。除非有外部干预,否则厨师A和厨师B将永远无法完成他们的烹饪任务。
  • 代码实现起来就是这个样子:
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx_pot, mtx_stove;

void cookAfun() {
    // 线程A首先尝试锁定mtx_pot
    mtx_pot.lock();
    std::cout << "厨师A拿起了锅\n" << std::endl;

    // 线程A尝试锁定mtx_stove,但mtx_stove已被线程B锁定
    mtx_stove.lock();
    std::cout << "厨师A拿起了炉子\n" << std::endl;

    // 释放锁
    mtx_stove.unlock();
    mtx_pot.unlock();
}

void cookBfun() {
    // 线程B首先尝试锁定mtx_stove
    mtx_stove.lock();
    std::cout << "厨师B拿起了锅\n" << std::endl;

    // 线程B尝试锁定mtx_pot,但mtx_pot已被线程A锁定
    mtx_pot.lock();
    std::cout << "厨师B拿起了炉子\n" << std::endl;

    // 释放锁
    mtx_pot.unlock();
    mtx_stove.unlock();
}

int main() {
    std::thread t1(cookAfun);
    std::thread t2(cookBfun);

    t1.join();
    t2.join();

    return 0;
}

  • 如下程序陷入了死锁,卡死了,两个厨师在那干瞪眼请添加图片描述

  • 那为了避免死锁,我们可以让厨师A和厨师B都先尝试锁定锅(mtx_pot),然后再锁定炉子(mtx_stove)。这样,无论哪个线程先开始执行,都不会发生死锁,因为它们都会以相同的顺序获取锁。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx_pot, mtx_stove;

void cookAfun() {
    // 线程A首先尝试锁定mtx_pot
    mtx_pot.lock();
    std::cout << "厨师A拿起了锅\n" << std::endl;

    // 线程A尝试锁定mtx_stove
    mtx_stove.lock();
    std::cout << "厨师A拿起了炉子\n" << std::endl;

    // 释放锁
    mtx_stove.unlock();
    mtx_pot.unlock();
}

void cookBfun() {
    // 线程B首先尝试锁定mtx_pot
    mtx_pot.lock();
    std::cout << "厨师B拿起了锅\n" << std::endl;

    // 线程B尝试锁定mtx_stove
    mtx_stove.lock();
    std::cout << "厨师B拿起了炉子\n" << std::endl;

    // 释放锁
    mtx_stove.unlock();
    mtx_pot.unlock();
}

int main() {
    std::thread t1(cookAfun);
    std::thread t2(cookBfun);

    t1.join();
    t2.join();

    return 0;
}

  • 这样,大家就可以各干各的了请添加图片描述

  • 然而,直接使用 lock()unlock() 可能会导致死锁(deadlock),如果忘记调用 unlock() 或者在持有锁的时候发生异常。


6 std::lock_guard

  • 为了解决忘记调用unlock或者上述问题,C++11 引入了 std::lock_guardstd::unique_lock,它们是 RAII(Resource Acquisition Is Initialization)风格的互斥锁封装器,可以自动管理锁的获取和释放。
  • 咱们直接上例子,还是那俩厨师
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx_pot, mtx_stove;
void cookAfun() {
    // 使用std::lock_guard自动锁定mtx_pot
    std::lock_guard<std::mutex> guard_pot(mtx_pot);
    std::cout << "厨师A拿起了锅\n" << std::endl;

    // 使用std::lock_guard自动锁定mtx_stove
    std::lock_guard<std::mutex> guard_stove(mtx_stove);
    std::cout << "厨师A拿起了炉子\n" << std::endl;

    // 释放锁
    // std::lock_guard在作用域结束时自动释放锁
}

void cookBfun() {
    // 使用std::lock_guard自动锁定mtx_pot
    std::lock_guard<std::mutex> guard_pot(mtx_pot);
    std::cout << "厨师B拿起了锅\n" << std::endl;

    // 使用std::lock_guard自动锁定mtx_stove
    std::lock_guard<std::mutex> guard_stove(mtx_stove);
    std::cout << "厨师B拿起了炉子\n" << std::endl;

    // 释放锁
    // std::lock_guard在作用域结束时自动释放锁
}

int main() {
    std::thread t1(cookAfun);
    std::thread t2(cookBfun);

    t1.join();
    t2.join();

    return 0;
}
  • 效果如下请添加图片描述

7 小结

  • 本文介绍了并发的基础入门知识,讲解了摄像头帧率计算,线程进程,并发和并行,std::thread,std::mutex,死锁,数据竞争问题,以及std::lock_guard
  • 下一节我们将讲讲如何把多线程运用到相机读取上。
  • 如有错误,欢迎指出!感谢大家的支持!!!!

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

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

相关文章

2-107 基于matlab的hsv空间双边滤波去雾图像增强算法

基于matlab的hsv空间双边滤波去雾图像增强算法&#xff0c;原始图像经过光照增强后&#xff0c;将RGB转成hsv&#xff0c;进行图像增强处理&#xff0c;使图像更加清晰。程序已调通&#xff0c;可直接运行。 下载源程序请点链接&#xff1a; 2-107 基于matlab的hsv空间双边滤…

“找不到emp.dll,无法继续执行代码”需要怎么解决呢?分享6个解决方法

在日常使用电脑玩游戏的过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中最常见的就是“emp.dll丢失”。那么&#xff0c;emp.dll到底是什么&#xff1f;它为什么会丢失&#xff1f;丢失后会对我们的电脑产生什么影响&#xff1f;本文将为您详细解析emp.dll的概念…

超详细的华为ICT大赛报名流程

1、访问华为人才在线官网&#xff0c;点击右上角“登录/注册“&#xff0c;登录华为账号。 报名链接&#xff1a; https://e.huawei.com/cn/talent/cert/#/careerCert?navTypeauthNavKey ▲如已有华为Uniportal账号&#xff0c;完成实名认证后方可报名大赛。 ▲如没有华为…

【有啥问啥】具身智能(Embodied AI):人工智能的新前沿

具身智能&#xff08;Embodied AI&#xff09;&#xff1a;人工智能的新前沿 引言 在人工智能&#xff08;AI&#xff09;的进程中&#xff0c;具身智能&#xff08;Embodied AI&#xff09;正逐渐成为研究与应用的焦点。具身智能不仅关注于机器的计算能力&#xff0c;更强调…

需求5:增加一个按钮

在之前的几个需求中&#xff0c;我们逐步从修改字段到新增字段&#xff0c;按部就班地完成了相关工作。通过最近的文章&#xff0c;不难看出我目前正在处理前端的“未完成”和“已完成”按钮。借此机会&#xff0c;我决定趁热打铁&#xff0c;重新梳理一下之前关于按钮实现的需…

4、MapReduce编程实践

目录 1、创建文件2、启动HDFS3、启动eclipse 创建项目并导入jar包file->new->java project导入jar包finish 4、编写Java应用程序5、编译打包应用程序&#xff08;1&#xff09;查看直接运行结果&#xff08;2&#xff09;打包程序&#xff08;3&#xff09;查看 JAR 包是…

软硬协同方案破解IT瓶颈,龙蜥衍生版KOS助力内蒙古大学成功迁移10+业务软件 | 龙蜥案例

2024 云栖大会上&#xff0c;龙蜥社区发布了《龙蜥操作系统生态用户实践精选 V2》&#xff0c;为面临 CentOS 迁移的广大用户提供成熟实践样板。截至目前&#xff0c;阿里云、浪潮信息、中兴通讯 | 新支点、移动、联通、龙芯、统信软件等超 12 家厂商基于龙蜥操作系统发布商业衍…

【在Linux世界中追寻伟大的One Piece】命名管道

目录 1 -> 命名管道 1.1 -> 创建一个命名管道 1.2 -> 匿名管道与命名管道的区别 1.3 -> 命名管道的打开规则 1.4 -> 例子 1 -> 命名管道 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据&…

串行化执行、并行化执行

文章目录 1、串行化执行2、并行化测试&#xff08;多线程环境&#xff09;3、任务的执行是异步的&#xff0c;但主程序的继续执行是同步的 可以将多个任务编排为并行和串行化执行。 也可以处理编排的多个任务的异常&#xff0c;也可以返回兜底数据。 1、串行化执行 顺序执行、…

C++类和对象(下) 初始化列表 、static成员、友元、内部类等等

1.再探构造函数 之前使用构造函数时都是在函数体内初始化成员变量&#xff0c;还有一种构造函数的用法&#xff0c;叫做初始化列表&#xff1b;那么怎么使用呢&#xff1f; 使用方法用冒号开始(" : ")要写多个就用逗号(" , ")隔开数据成队列每个成员变量后…

DC00023基于jsp+MySQL新生报到管理系统

1、项目功能演示 DC00023基于jsp新生报到管理系统java webMySQL新生管理系统 2、项目功能描述 基于jspMySQL新生报到管理系统项目分为学生、辅导员、财务处和系统管理员四个角色。 2.1 学生功能 1、系统登录 2、校园新闻、报到流程、学校简介、在线留言、校园风光、入校须知…

解决Qt每次修改代码后首次运行崩溃,后几次不崩溃问题

在使用unique_ptr声明成员变量后&#xff0c;我习惯性地在初始化构造列表中进行如下构造&#xff1a; 注意看&#xff0c;我将m_menuBtnGroup的父类指定为ui->center_menu_widget&#xff0c;这便是导致崩溃的根本原因&#xff0c;解决办法便是先用this初始化&#xff0c;后…

pdf页面尺寸裁减

1、编辑pdf 2、点击裁减页面&#xff0c;并在空白区域双击裁减 3、输入裁减数据&#xff1a;

calibre-web浏览器标题icon修改

calibre-web浏览器标题icon修改 Windows安装calibre-web&#xff0c;Python-CSDN博客文章浏览阅读537次&#xff0c;点赞10次&#xff0c;收藏11次。pip install calibreweb报错&#xff1a;error: Microsoft Visual C 14.0 or greater is required. Get it with "Microso…

Springboot中基于注解实现公共字段自动填充

1.使用场景 当我们有大量的表需要管理公共字段&#xff0c;并且希望提高开发效率和确保数据一致性时&#xff0c;使用这种自动填充方式是很有必要的。它可以达到一下作用 统一管理数据库表中的公共字段&#xff1a;如创建时间、修改时间、创建人ID、修改人ID等&#xff0c;这些…

视频剪辑软件哪个好?剪辑更高效用这些

众所周知&#xff0c;视频已经成为我们记录生活、表达自我的重要方式。 无论是制作旅行Vlog&#xff0c;还是剪辑短片分享故事&#xff0c;优秀的视频剪辑软件是让创意变为现实的利器。 那么&#xff0c;如何在众多免费软件中做出明智选择&#xff0c;成为剪辑高手呢&#xf…

通信工程学习:什么是SISO单入单出

SISO&#xff1a;单入单出 SISO&#xff0c;即单输入单输出&#xff08;Single-Input Single-Output&#xff09;系统&#xff0c;也被称为单变量系统。在这种系统中&#xff0c;输入量与输出量各为一个&#xff0c;是控制理论中的一个基本概念。以下是对SISO系统的详细解释&am…

为什么说函数传递参数最好小于四个

有一个说法说是函数传递参数最好不超过四个&#xff0c;原因有一个是参数太多难以维护&#xff0c;另一个重要的原因就是函数传递小于四个参数时候效率会更高&#xff0c;其实这个说法也不全对&#xff0c;在不同的结构下不太一样&#xff0c;也不一定是4 其实那么下面将探究函…

【RocketMQ】消费失败重试与死信消息

&#x1f3af; 导读&#xff1a;本文档详细介绍了RocketMQ中的重试机制与死信消息处理方法。对于生产者而言&#xff0c;文档提供了如何配置重试次数的具体示例&#xff1b;而对于消费者&#xff0c;它解释了默认情况下消息消费失败后的重试策略&#xff0c;并展示了如何通过代…