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

前言

  • 高帧率摄像头往往应用在很多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_potmtx_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_stovemtx_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_potmtx_pot.lock();std::cout << "厨师A拿起了锅\n" << std::endl;// 线程A尝试锁定mtx_stovemtx_stove.lock();std::cout << "厨师A拿起了炉子\n" << std::endl;// 释放锁mtx_stove.unlock();mtx_pot.unlock();
}void cookBfun() {// 线程B首先尝试锁定mtx_potmtx_pot.lock();std::cout << "厨师B拿起了锅\n" << std::endl;// 线程B尝试锁定mtx_stovemtx_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_potstd::lock_guard<std::mutex> guard_pot(mtx_pot);std::cout << "厨师A拿起了锅\n" << std::endl;// 使用std::lock_guard自动锁定mtx_stovestd::lock_guard<std::mutex> guard_stove(mtx_stove);std::cout << "厨师A拿起了炉子\n" << std::endl;// 释放锁// std::lock_guard在作用域结束时自动释放锁
}void cookBfun() {// 使用std::lock_guard自动锁定mtx_potstd::lock_guard<std::mutex> guard_pot(mtx_pot);std::cout << "厨师B拿起了锅\n" << std::endl;// 使用std::lock_guard自动锁定mtx_stovestd::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.rhkb.cn/news/435173.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【OS】计算机系统概述|操作系统基本概念|并发|并行|虚拟异步

✨ Blog’s 主页: 白乐天_ξ( ✿&#xff1e;◡❛) &#x1f308; 个人Motto&#xff1a;他强任他强&#xff0c;清风拂山冈&#xff01; &#x1f525; 所属专栏&#xff1a;C深入学习笔记 &#x1f4ab; 欢迎来到我的学习笔记&#xff01; 前言 一、操作系统的概念 操作系统…

如何使用ssm实现基于Java的高校物业工程报修系统

TOC ssm736基于Java的高校物业工程报修系统jsp 绪论 1.1研究背景与意义 信息化管理模式是将行业中的工作流程由人工服务&#xff0c;逐渐转换为使用计算机技术的信息化管理服务。这种管理模式发展迅速&#xff0c;使用起来非常简单容易&#xff0c;用户甚至不用掌握相关的专…

2. 将GitHub上的开源项目导入(clone)到(Linux)服务器上——深度学习·科研实践·从0到1

目录 1. 在github上搜项目 (以OpenOcc为例&#xff09; 2. 转移到码云Gitee上 3. 进入Linux服务器终端 (jupyter lab) 4. 常用Linux命令 5. 进入对应文件夹中导入项目(代码) 注意&#xff1a;系统盘和数据盘 1. 在github上搜项目 (以OpenOcc为例&#xff09; 把链接复制下…

llamafactory0.9.0微调qwen2.5

llama_factory微调QWen1.5_llama factory qwen-CSDN博客文章浏览阅读2.9k次,点赞36次,收藏10次。本文介绍了如何使用LLaMA-Factory微调Qwen1.5模型,包括1.8B和0.5B版本的训练细节。在数据、训练、LORA融合及推理等方面进行了探讨,同时也分享了微调后模型在不同任务上的表现…

Linux快速安装ClickHouse(附官方文档)

在线安装 1.安装yum-utils yum-utils是一个与 yum 集成的实用程序集合&#xff0c;可以通过多种方式扩展其本机功能 yum install -y yum-utils 2.增加ClickHouse官方镜像源 yum-config-manager --add-repo https://packages.clickhouse.com/rpm/clickhouse.repo 3.安装Cl…

【JavaEE初阶】网络原理

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 ⽹络互连 IP地址 端口号 协议 协议分层 优势 TCP/IP 五层网络模型 数据在网络通信中的整体流程 封装和分用 封装 分用 ⽹络互连 随着时代的发展&#xff0c;越来越需…

828华为云征文 | 云服务器Flexus X实例:向量数据库 pgvector 部署,实现向量检索

目录 一、什么是向量数据库 pgvector &#xff1f; 二、pgvector 部署 2.1 安装 Docker 2.2 拉取镜像 2.3 添加规则 三、pgvector 运行 3.1 运行 pgvector 3.2 连接 pgvector 3.3 pgvector 常见操作 四、总结 本篇文章通过 云服务器Flexus X实例 部署向量数据库 pgve…

第十四章:html和css做一个心在跳动,为你而动的表白动画

💖 让心跳加速,传递爱意 💖 在这个特别的时刻,让爱在跳动中绽放!🌟 无论是初次相遇的心动,还是陪伴多年的默契,我们的心总在为彼此跳动。就像这颗炙热的爱心,随着每一次的跳动,传递着满满的温暖与期待。 在这个浪漫的季节,让我们一同感受爱的律动!无论你是在…

手搓 HTTP服务器 手把手带你实现高并发HTTP服务器 C++ HTTP服务器 服务器项目实战 高性能服务器实战 服务器项目 服务器

手搓 HTTP服务器 高并发HTTP服务器 C HTTP服务器 1、什么是socket Socket是一种用于网络通信的编程接口&#xff0c;允许不同计算机之间通过网络发送和接收数据。它在客户端和服务器之间创建连接&#xff0c;支持多种协议&#xff0c;如TCP和UDP。2、实现socket 通讯 1、soc…

Milvus - 架构设计详解

Milvus 是一个专为在大规模密集向量数据集上进行相似性搜索而设计的开源向量数据库系统。其架构建立在流行的向量搜索库之上&#xff0c;如 Faiss、HNSW、DiskANN 和 SCANN&#xff0c;能够处理数百万、数十亿甚至数万亿的向量数据。为了全面了解 Milvus 架构&#xff0c;我们首…

苏州 数字化科技展厅展馆-「世岩科技」一站式服务商

数字化科技展厅展馆设计施工是一个综合性强、技术要求高的项目&#xff0c;涉及到众多方面的要点。以下是对数字化科技展厅展馆设计施工要点的详细分析&#xff1a; 一、明确目标与定位 在设计之初&#xff0c;必须明确展厅的目标和定位。这包括确定展厅的主题、目标受众、展…

实用SQL小总结

WHERE 条件 column 为纯英文字符 或 不包含任何字符 语法&#xff1a; SELECT * FROM your_table WHERE REGEXP(your_column,^[A-Za-z]$); SELECT * FROM your_table WHERE NOT REGEXP(your_column,^[A-Za-z]$);例&#xff1a; SELECT DISTINCT t.pldlibho FROM kibb_pldlyw…

Naive UI 选择器 Select 的:render-label 怎么使用(Vue3 + TS)

项目场景&#xff1a; 在Naive UI 的 选择器 Select组件中 &#xff0c;如何实现下面的效果 &#xff0c;在下拉列表中&#xff0c;左边展示色块&#xff0c;右边展示文字。 Naive UI 的官网中提到过这个实现方法&#xff0c;有一个render-label的api&#xff0c;即&#xff…

数据库操作:数据类型

0. 铺垫 1.数值类型 注&#xff1a;此图的最大值都要减1&#xff1b;因为我的错误&#xff0c;后面会改正&#xff1b; 1.0、tinyint 类型大小——1字节 create table tt1(num tinyint); insert into tt1 values(1); insert into tt1 values(128); -- 越界插入&#xff0c;…

【Android】 IconFont的使用

SVG 的特点&#xff1a; 矢量图形&#xff1a;SVG 使用基于路径的矢量图形&#xff0c;这意味着图形可以无限放大而不失真&#xff0c;非常适合需要多种分辨率的应用。 可伸缩性&#xff1a;SVG 文件的大小通常比位图小&#xff0c;这使得它们在网页上加载更快。 编辑和创作&…

Text-to-SQL方法研究

有关Text-to-SQL实现细节&#xff0c;可以查阅我的另一篇文章text-to-sql将自然语言转换为数据库查询语句 1、面临的挑战 自然语言问题往往包含复杂的语言结构,如嵌套语句、倒装句和省略等,很难准确映射到SQL查询上。此外,自然语言本身就存在歧义,一个问题可能有多种解读。消除…

11.C++程序中的常用函数

我们将程序中反复执行的代码封装到一个代码块中&#xff0c;这个代码块就被称为函数&#xff0c;它类似于数学中的函数&#xff0c;在C程序中&#xff0c;有许多由编译器定义好的函数&#xff0c;供大家使用。下面就简单说一下&#xff0c;C中常用的函数。 1.sizeof sizeof函…

spring boot 项目中redis的使用,key=value值 如何用命令行来查询并设置值。

1、有一个老项目&#xff0c;用到了网易云信&#xff0c;然后这里面有一个AppKey&#xff0c;然后调用的时候要在header中加入这些标识&#xff0c;进行与服务器进行交互。 2、开发将其存在了redis中&#xff0c;一开始的时候&#xff0c;我们测试用的老的key&#xff0c;然后提…

ROS学习笔记(二):鱼香ROS — 超便捷的一键安装/配置/换源指令(Ubuntu/ROS/ROS2/IDE等)

文章目录 前言鱼香ROS1 一键安装&#xff1a;快速搭建开发环境2 具体使用2.1 如何开始&#xff1f;2.2 我的常用配置方案2.3 安装示例 3 总结相关链接 前言 关于Ubuntu与ROS的常规安装&#xff0c;可以看这几篇。 SLAM实操入门&#xff08;一&#xff09;&#xff1a;在已有…

【若依RuoYi-Vue | 项目实战】帝可得后台管理系统(三)

文章目录 一、商品管理1、需求说明2、生成基础代码&#xff08;1&#xff09;创建目录菜单&#xff08;2&#xff09;配置代码生成信息&#xff08;3&#xff09;下载代码并导入项目 3、商品类型改造&#xff08;1&#xff09;基础页面 4、商品管理改造&#xff08;1&#xff0…