【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.xdnf.cn/news/1550553.html

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

相关文章

【muduo源码分析】「阻塞」「非阻塞」「同步」「异步」

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 文章目录 引言何为「muduo库」安装muduo库阻塞、非阻塞、同步、异步数据准备数据准备 引言 从本篇博客开始&#xff0c;我会陆续发表muduo库源码分析的相关文章。感谢大家的持续关注&#xff01;&#xff01;…

9.29总结

这星期学了概率和组合数学 这是我觉得的一个有趣的题目&#xff0c;每个人身上都有n-1根绳子&#xff0c;如果组不成稳定三角&#xff0c;那么肯定有两个人相邻两根绳子颜色不一样&#xff0c;那么每两个这样的人就会贡献一个不稳定三角形&#xff0c;所以只要所有三角形减去每…

64.【C语言】再议结构体(下)(未完)

本文衔接第63篇 目录 6.复习 7.修改默认对齐数 8.结构体传参 01.传递非指针参数 02.传递指针参数(传递地址) 03.对比 9.结构体实现位段 01.位段的定义 02.格式 03.例题 答案速查 分析 前置知识:位段的内存分配 解析 若按浪费空间处理 验证 6.复习 20.【C语言…

①三菱Modbus主站MELSEC转ModbusRTU/ASCII工业MELSEC网关串口服务

三菱Modbus主站MELSEC转ModbusRTU/ASCII工业MELSEC网关串口服务https://item.taobao.com/item.htm?ftt&id834634632647 MELSEC 通信单元 MELSEC 转 RS485 MS-A1-80X1 系列概述 型号&#xff1a;1路总线MELSEC网关(单网口&#xff09; MS-A1-8011 1路总线MELSEC网关(双…

A Learning-Based Approach to Static Program Slicing —— 论文笔记

A Learning-Based Approach to Static Program Slicing OOPLSA’2024 文章目录 A Learning-Based Approach to Static Program Slicing1. Abstract2. Motivation(1) 为什么需要能处理不完整代码(2) 现有方法局限性(3) 验证局限性: 初步实验研究实验设计何为不完整代码实验结果…

计算机网络基础--认识协议

目录 前言 一、IP地址与端口 二、网络协议 1.网络体系结构框架 2.网络字节序 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 计算机网络涉及非常广泛&#xff0c;这篇文章主要对计算机网络有个认识 提示&#xff1a;以下是本篇文章正文内容&#x…

C++系列-继承

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 继承的概念和定义 继承是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行拓展&#xff0c;增加功能&#xff0c;这样可以…

【PyTorch】生成对抗网络

生成对抗网络是什么 Generative Adversarial Nets&#xff0c;简称GAN GAN&#xff1a;生成对抗网络 —— 一种可以生成特定分布数据的模型 《Recent Progress on Generative Adversarial Networks (GANs): A Survey》 《How Generative Adversarial Networks and Its Varian…

基于Megatron-LM从0到1完成GPT2模型预训练、模型评估及推理

随着 ChatGPT 迅速爆火&#xff0c;引领基于Transformer架构的大模型从幕后走到台前。但 ChatGPT 的成功并不是一蹴而就&#xff0c;而是&#xff0c;经过了从早期的 GPT1 到 GPT2&#xff0c;之后到 GPT3 和 InstructGPT、然后到GPT3.5和ChatGPT&#xff0c;直到如今的多模态大…

一钉多用:自攻螺钉在家居与工业领域的广泛应用

自攻螺钉的结构要素有哪些重要特点&#xff1f; 自攻螺钉适用于非金属或软金属&#xff0c;不需要配合预先开好的孔和攻牙。自攻螺钉的尖头设计使其能够“自我攻入”材料中&#xff1b;而普通螺丝通常是平头&#xff0c;规格一致。自攻螺钉的关键在于&#xff0c;打孔时不需要进…

【JavaEE初阶】网络原理

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

Angular基础学习(入门 --> 入坑)

目录 一、Angular 环境搭建 二、创建Angular新项目 三、数据绑定 四、ngFor循环、ngIf、ngSwitch、[ngClass]、[ngStyle]、管道、事件、双向数据绑定--MVVM 五、DOM 操作 &#xff08;ViewChild&#xff09; 六、组件通讯 七、生命周期 八、Rxjs 异步数据流 九、Http …

51单片机的光照强度检测【proteus仿真+程序+报告+原理图+演示视频】

1、主要功能 该系统由AT89C51/STC89C52单片机LCD1602显示模块光照传感器按键蜂鸣器LED等模块构成。适用于光照强度检测、光照强度测量报警等相似项目。 可实现功能: 1、LCD1602实时显示光照强度信息 2、光照强度传感器&#xff08;电位器模拟&#xff09;采集光照信息 3、可…

X86架构(九)——保护模式的进入

全局描述符表 全局描述符表(Global Descriptor Table,GDT)是保护模式下非常重要的一个数据结构。 在保护模式下&#xff0c;对内存的访问仍然使用段地址和偏移地址&#xff0c;在每个段能够访问之前&#xff0c;必须先行设置好 GDT 的地址&#xff0c;并加载全局描述符表寄存…

emp.dll丢失怎么解决,快来试试这个几个解决方法

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

[BUUCTF从零单排] Web方向 03.Web入门篇之sql注入-1(手工注入详解)

这是作者新开的一个专栏《BUUCTF从零单排》&#xff0c;旨在从零学习CTF知识&#xff0c;方便更多初学者了解各种类型的安全题目&#xff0c;后续分享一定程度会对不同类型的题目进行总结&#xff0c;并结合CTF书籍和真实案例实践&#xff0c;希望对您有所帮助。当然&#xff0…

打造高业绩朋友圈:策略与实践

在数字化时代&#xff0c;朋友圈不仅是个人生活的展示窗口&#xff0c;更是商业变现的有力平台。许多人通过精心经营朋友圈&#xff0c;实现了财富的增长&#xff0c;甚至达到了年入百万的惊人业绩。朋友圈已成为普通人实现逆袭的重要战场。 要打造一个业绩过万的朋友圈&#…

关于武汉芯景科技有限公司的IIC电平转换芯片XJ9509开发指南(兼容PCa9509)

一、芯片引脚介绍 1.芯片引脚 2.引脚描述 二、系统结构图 三、功能描述 1.VCCA1.35V,VCCB5V,A1输入&#xff0c;B1输出 2.VCCA1.35V,VCCB5V,B1输入&#xff0c;A1输出 3.VCCA1.35V,VCCB5V,A2输入&#xff0c;B2输出 4.VCCA1.35V,VCCB5V,B2输入&#xff0c;A2输出

升级 Windows 后如何恢复丢失的文件

升级到 Windows 11 后可以恢复丢失的文件&#xff01;阅读帖子直到最后&#xff0c;了解如何做到这一点。 为了获得安全更新并使用最新的操作系统&#xff0c;人们会升级到最新版本的 Windows。然而&#xff0c;在这样做的过程中&#xff0c;许多人丢失了他们的重要文件&#…

基于SpringBoot+Vue+MySQL的体育商城系统

系统展示 用户前台界面 管理员后台界面 系统背景 随着互联网的飞速发展&#xff0c;电子商务已成为人们日常生活中不可或缺的一部分。体育用品市场作为其中的一个重要分支&#xff0c;也逐渐向线上转移。基于SpringBootVueMySQL的体育商城系统应运而生&#xff0c;旨在通过构建…