Qt之条件变量QWaitCondition详解

QWaitCondition内部实现结构图: 

相关系列文章

C++之Pimpl惯用法

目录

1.简介

2.示例

2.1.全局配置

2.2.生产者Producer

2.3.消费者Consumer

2.4.测试例子

3.原理分析

3.1.辅助函数CreateEvent

3.2.辅助函数WaitForSingleObject 

3.3.QWaitConditionEvent

3.4.QWaitConditionEventPrivate

3.5.流程分析

4.总结


1.简介

QWaitCondition是用来同步线程的条件变量,头文件<QWaitCondition>,类中的所有函数都是线程安全的。主要的公共函数(以Qt5.12.12为例)如下表:

返回类型函数名称含义
QWaitCondition ()构造函数
~QWaitCondition ()析构函数
boolwait ( QMutex * mutex, unsigned long time = ULONG_MAX )mutex将被解锁,并且调用线程将会阻塞,直到下列条件之一满足才想来:
(1)另一个线程使用wakeOne()或wakeAll()传输给它;
(2)time毫秒过去。
boolwait(QMutex *lockedMutex, QDeadlineTimer deadline)同上
boolwait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX )readWriteLock将被解锁,并且调用线程将会阻塞,直到下列条件之一满足才想来:
(1)另一个线程使用wakeOne()或wakeAll()传输给它;
(2)time毫秒过去。
boolwait(QReadWriteLock *lockedReadWriteLock, QDeadlineTimer deadline)同上
voidwakeAll ()唤醒所有等待的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定
voidwakeOne()唤醒等待QWaitCondition的线程中的一个线程,线程唤醒的顺序不确定,由操作系统的调度策略决定
voidnotify_all()同wakeAll()
voidnotify_one()同wakeOne()

QWaitCondition允许线程告诉其他线程某种条件已经满足。一个或多个线程可以阻止等待QWaitCondition使用wakeOne()或wakeAll()设置条件。使用wakeOne()唤醒一个随机选择的线程,或使用wakeAll()唤醒所有线程。

2.示例

生产者/消费者模型为例,看一下具体实现

2.1.全局配置

//! [0]
const int DataSize = 127;

const int BufferSize = 8192;
char buffer[BufferSize];

QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
//! [0]

        主要有缓冲区buffer, 循环缓冲区大小BufferSize及生产的数量,小于 DataSize,这意味着在某一时刻生产者将达到缓冲区的末尾,并从开始位置重新启动。

        要同步生产者和消费者,需要两个 wait 条件和一个 mutex。当生产者生成一些数据时,bufferNotEmpty 条件被发射,告诉消费者可以读取它了;当消费者读取一些数据时,bufferNotFull 条件被发射,告诉生产者生成更多的数据。numUsedBytes 为缓冲区中所包含数据的字节数。

        总之,wait 条件、mutex、和 numUsedBytes 计数器确保生产者不会先于消费者超过 BufferSize 的大小,而消费者永远不会读取生产者尚未生成的数据。

2.2.生产者Producer

生产者的代码如下:

//! [1]
class Producer : public QThread
//! [1] //! [2]
{
public:
    Producer(QObject *parent = NULL) : QThread(parent)
    {
    }

    void run() override
    {
        for (int i = 0; i < DataSize; ++i) {
            mutex.lock();
            if (numUsedBytes == BufferSize)
                bufferNotFull.wait(&mutex);
            mutex.unlock();

            buffer[i % BufferSize] = i;"ACGT"[QRandomGenerator::global()->bounded(4)];

            mutex.lock();
            ++numUsedBytes;
            bufferNotEmpty.wakeAll();
            mutex.unlock();
        }
    }
};
//! [2]

        生产者根据DataSize的大小循环生产数据。在往循环缓冲区写入一个字母之前,它必须检查缓冲区是否已满(即满足numUsedBytes等于BufferSize条件),如果缓冲区满了,现成就会在bufferNotFull条件上等待。

        满足条件后,生产者增加 numUsedBytes,并且标志 bufferNotEmpty 条件为 true,从而唤醒消费者线程去消费。

2.3.消费者Consumer

消费者的代码如下:

//! [3]
class Consumer : public QThread
//! [3] //! [4]
{
    Q_OBJECT
public:
    Consumer(QObject *parent = NULL) : QThread(parent)
    {
    }

    void run() override
    {
        for (int i = 0; i < DataSize; ++i) {
            mutex.lock();
            if (numUsedBytes == 0)
                bufferNotEmpty.wait(&mutex);
            mutex.unlock();

            fprintf(stderr, "%d\n", buffer[i % BufferSize]);

            mutex.lock();
            --numUsedBytes;
            bufferNotFull.wakeAll();
            mutex.unlock();
        }
        fprintf(stderr, "\n");
    }

signals:
    void stringConsumed(const QString &text);
};
//! [4]

代码非常类似于生产者,在读取字节之前,需要先检查缓冲区是否为空(numUsedBytes 为 0),而非它是否为已满。并且,当它为空时,等待 bufferNotEmpty 条件。在读取字节后,减小 numUsedBytes (而非增加),并标志 bufferNotFull 条件(而非 bufferNotEmpty 条件)。

2.4.测试例子

代码如下:

//! [5]
int main(int argc, char *argv[])
//! [5] //! [6]
{
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}
//! [6]

#include "waitconditions.moc"

        上面的测试代码是一个生产者对一个消费者,生产一个消费一个,所以看到的结果是按照顺序输出,结果如下:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

        此示例中提出的“生产者 - 消费者”模式,适用于编写高并发多线程应用。在多处理器计算机中,程序可能比基于 mutex 的方案快达两倍之多,因为两个线程可以同一时间在缓冲区的不同部分处于激活状态。

        上面我们讲解了QWaitCondition的用法,如果还有兴趣继续探究它的实现原理的话,则可以继续往下看。

3.原理分析

在Qt5.12.12版本上,以windows端来讲解它的实现原理。

3.1.辅助函数CreateEvent

CreateEvent的定义如下:

HANDLE CreateEvent(
  [in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
  [in]           BOOL                  bManualReset,
  [in]           BOOL                  bInitialState,
  [in, optional] LPCSTR                lpName
);

创建或打开一个命名或未命名的事件对象。

lpEventAttributes: 指向 SECURITY_ATTRIBUTES 结构的指针。 如果此参数为 NULL,则子进程无法继承句柄。结构的 lpSecurityDescriptor 成员为新事件指定 安全描述符 。 如果 lpEventAttributes 为 NULL,则事件将获取默认的安全描述符。 事件的默认安全描述符中的 ACL 来自创建者的主要令牌或模拟令牌。

bManualReset: 如果此参数为 TRUE,则函数将创建手动重置事件对象,该对象需要使用 ResetEvent 函数将事件状态设置为非签名。 如果此参数为 FALSE,则函数将创建一个自动重置事件对象,在释放单个等待线程后,系统会自动将事件状态重置为未签名。

bInitialState: 如果此参数为 TRUE,则会向事件对象发出初始状态信号;否则,它将不进行签名。

lpName: 可选项,事件对象的名称。 名称限制为 MAX_PATH 个字符。 名称比较区分大小写。

如果 lpName 与现有命名事件对象的名称匹配,则此函数请求 EVENT_ALL_ACCESS 访问权限。 在这种情况下, bManualReset 和 bInitialState 参数将被忽略,因为它们已由创建过程设置。 如果 lpEventAttributes 参数不是 NULL,它将确定是否可以继承句柄,但忽略其安全描述符成员。

如果 lpName 为 NULL,则创建不带名称的事件对象。

如果 lpName 与同一命名空间中另一种对象的名称匹配, (例如现有信号灯、互斥体、可等待计时器、作业或文件映射对象) ,则函数将失败, GetLastError 函数将返回 ERROR_INVALID_HANDLE。 发生这种情况的原因是这些对象共享相同的命名空间。

名称可以具有“Global”或“Local”前缀,以在全局命名空间或会话命名空间中显式创建对象。 名称的其余部分可以包含除反斜杠字符 (\) 以外的任何字符。 有关详细信息,请参阅 内核对象命名空间。 使用终端服务会话实现快速用户切换。 内核对象名称必须遵循终端服务概述的准则,以便应用程序可以支持多个用户。

可以在专用命名空间中创建 对象。 有关详细信息,请参阅 对象命名空间。

返回值:如果函数成功,则返回值是事件对象的句柄。 如果命名事件对象在函数调用之前存在,则函数将返回现有对象的句柄, GetLastError 将返回 ERROR_ALREADY_EXISTS

如果函数失败,则返回值为 NULL。 要获得更多的错误信息,请调用 GetLastError。

3.2.辅助函数WaitForSingleObject 

这个是windows系统多线程,进程中用的最多的一个函数,它的定义如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

这个函数的作用是等待一个内核对象,在Windows系统上一个内核对象通常使用其句柄来操作,参数hHandle即需要等待的内核对象,参数dwMilliseconds是等待这个内核对象的最大时间,时间单位是毫秒,其类型是DWORD,这是一个unsigned long类型。如果我们需要无限等待下去,可以将这个参数值设置为INFINITE宏。

在Windows上可以调用WaitForSingleObject等待的常见对象如下表所示:

可以被等待的对象等待对象成功的含义对象类型
线程等待线程结束HANDLE
Process等待进程结束HANDLE
Event (事件)等待 Event 有信号HANDLE
Mutex (互斥体)等待持有 Mutex 的线程释放该 Mutex,等待成功,拥有该MutexHANDLE
Semaphore(信号量)等待该 Semaphore 对象有信号HANDLE

上面介绍的等待线程对象上文中已经详细介绍过了,这里不再重复了,等待进程退出与等待线程退出类似,也不再赘述。下文中我们将详细介绍 Event、Mutex、Semaphore 这三种类型的资源同步对象,这里我们先接着介绍WaitForSingleObject函数的用法,该函数的返回值一般有以下类型:

  • WAIT_FAILED,表示WaitForSingleObject函数调用失败了,调用失败时,可以通过GetLastError 函数得到具体的错误码;
  • WAIT_OBJECT_0,表示WaitForSingleObject成功“等待”到设置的对象;
  • WAIT_TIMEOUT,等待超时;
  • WAIT_ABANDONED,当等待的对象是Mutex类型时,如果持有该Mutex对象的线程已经结束,但是没有在结束前释放该Mutex,此时该Mutex已经处于废弃状态,其行为是未知的,不建议再使用该Mutex。

上面我们讲解了CreateEvent和WaitForSingleObject函数,下面看一个示例:

#include <windows.h>
#include <stdio.h>

#define THREADCOUNT 4 

HANDLE ghWriteEvent; 
HANDLE ghThreads[THREADCOUNT];

DWORD WINAPI ThreadProc(LPVOID);

void CreateEventsAndThreads(void) 
{
    int i; 
    DWORD dwThreadID; 

    // Create a manual-reset event object. The write thread sets this
    // object to the signaled state when it finishes writing to a 
    // shared buffer. 

    ghWriteEvent = CreateEvent( 
        NULL,               // default security attributes
        TRUE,               // manual-reset event
        FALSE,              // initial state is nonsignaled
        TEXT("WriteEvent")  // object name
        ); 

    if (ghWriteEvent == NULL) 
    { 
        printf("CreateEvent failed (%d)\n", GetLastError());
        return;
    }

    // Create multiple threads to read from the buffer.

    for(i = 0; i < THREADCOUNT; i++) 
    {
        // TODO: More complex scenarios may require use of a parameter
        //   to the thread procedure, such as an event per thread to  
        //   be used for synchronization.
        ghThreads[i] = CreateThread(
            NULL,              // default security
            0,                 // default stack size
            ThreadProc,        // name of the thread function
            NULL,              // no thread parameters
            0,                 // default startup flags
            &dwThreadID); 

        if (ghThreads[i] == NULL) 
        {
            printf("CreateThread failed (%d)\n", GetLastError());
            return;
        }
    }
}

void WriteToBuffer(VOID) 
{
    // TODO: Write to the shared buffer.
    
    printf("Main thread writing to the shared buffer...\n");

    // Set ghWriteEvent to signaled

    if (! SetEvent(ghWriteEvent) ) 
    {
        printf("SetEvent failed (%d)\n", GetLastError());
        return;
    }
}

void CloseEvents()
{
    // Close all event handles (currently, only one global handle).
    
    CloseHandle(ghWriteEvent);
}

int main( void )
{
    DWORD dwWaitResult;

    // TODO: Create the shared buffer

    // Create events and THREADCOUNT threads to read from the buffer

    CreateEventsAndThreads();

    // At this point, the reader threads have started and are most
    // likely waiting for the global event to be signaled. However, 
    // it is safe to write to the buffer because the event is a 
    // manual-reset event.
    
    WriteToBuffer();

    printf("Main thread waiting for threads to exit...\n");

    // The handle for each thread is signaled when the thread is
    // terminated.
    dwWaitResult = WaitForMultipleObjects(
        THREADCOUNT,   // number of handles in array
        ghThreads,     // array of thread handles
        TRUE,          // wait until all are signaled
        INFINITE);

    switch (dwWaitResult) 
    {
        // All thread objects were signaled
        case WAIT_OBJECT_0: 
            printf("All threads ended, cleaning up for application exit...\n");
            break;

        // An error occurred
        default: 
            printf("WaitForMultipleObjects failed (%d)\n", GetLastError());
            return 1;
    } 
            
    // Close the events to clean up

    CloseEvents();

    return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParam) 
{
    // lpParam not used in this example.
    UNREFERENCED_PARAMETER(lpParam);

    DWORD dwWaitResult;

    printf("Thread %d waiting for write event...\n", GetCurrentThreadId());
    
    dwWaitResult = WaitForSingleObject( 
        ghWriteEvent, // event handle
        INFINITE);    // indefinite wait

    switch (dwWaitResult) 
    {
        // Event object was signaled
        case WAIT_OBJECT_0: 
            //
            // TODO: Read from the shared buffer
            //
            printf("Thread %d reading from buffer\n", 
                   GetCurrentThreadId());
            break; 

        // An error occurred
        default: 
            printf("Wait error (%d)\n", GetLastError()); 
            return 0; 
    }

    // Now that we are done reading the buffer, we could use another
    // event to signal that this thread is no longer reading. This
    // example simply uses the thread handle for synchronization (the
    // handle is signaled when the thread terminates.)

    printf("Thread %d exiting\n", GetCurrentThreadId());
    return 1;
}

上面示例使用事件对象来防止在主线程写入该缓冲区时从共享内存缓冲区读取多个线程。 首先,主线程使用 CreateEvent 函数创建初始状态为非签名的手动重置事件对象。 然后,它会创建多个读取器线程。 主线程执行写入操作,然后在完成写入后将事件对象设置为信号状态。

在开始读取操作之前,每个读取器线程都使用 WaitForSingleObject 等待手动重置事件对象发出信号。 当 WaitForSingleObject 返回时,这表示main线程已准备好开始其读取操作。

3.3.QWaitConditionEvent

QWaitConditionEvent实际是对CreateEvent的封装,代码如下:

class QWaitConditionEvent
{
public:
    inline QWaitConditionEvent() : priority(0), wokenUp(false)
    {
        event = CreateEvent(NULL, TRUE, FALSE, NULL);
    }
    inline ~QWaitConditionEvent() { CloseHandle(event); }
    int priority;
    bool wokenUp;
    HANDLE event;
};

这个定义源码在.\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\thread\qwaitcondition_win.cpp中,从中可以看出没生成一个QWaitConditionEvent就会创建一个手动重置事件对象。

3.4.QWaitConditionEventPrivate

QWaitConditionEventPrivate的定义如下:

typedef QList<QWaitConditionEvent *> EventQueue;

class QWaitConditionPrivate
{
public:
    QMutex mtx;
    EventQueue queue;
    EventQueue freeQueue;

    QWaitConditionEvent *pre();
    bool wait(QWaitConditionEvent *wce, unsigned long time);
    void post(QWaitConditionEvent *wce, bool ret);
};

上面代码定义了两个事件队列,一个是等待事件队列,一个空闲时间队列;还定义了3个对事件队列操作的接口,下面说明各接口的用法:

1)  pre() :  从空闲队列中freeQueue取出一个事件对象 QWaitConditionEvent放入queue。

2)wait() : 在事件对象wce上等待time时间,该函数会阻塞当前线程的运行,直到time到或SetEvent。

3)  post() : 把使用后的QWaitConditionEvent归还到空事件队列freeQueue里面。

3.5.流程分析

QWaitCondition的d指针是QWaitConditionEventPrivate,对QWaitCondition的操作转换为对QWaitConditionEventPrivate的操作。关键步骤流程如下:

1)wait函数执行流程

2)wakeOne函数执行流程

3) wakeAll函数执行流程

wakeAll的流程同wakeOne的流程相似,只是wakeOne是把事件队列的第一个事件对象SetEvent,而wakeAll是把事件队列中的所有事件对象SetEvent。

QWaitCondition类的设计思想也遵循Qt大部分类的设计思想Pimpl技法,关于Pimpl技法的一些详细介绍,可参考我的博客C++之Pimpl惯用法-CSDN博客

4.总结

QMutex 和 QWaitCondition 联合使用是多线程中的一个常用的习惯用法,不仅是 Qt,对于 C++ 的 std::condition_variable 和 std::mutex ,以及 java 的 synchronized / wait / notify 也都适用。

参考:

createEventA 函数 (synchapi.h) - Win32 apps | Microsoft Learn

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++笔记1:操纵符输入输出

C操纵符用来控制输出控制&#xff0c;一是输出的形式&#xff0c;二是控制补白的数量和位置。本文记录一下&#xff0c;在一些笔试的ACM模式可能有用。其中1-4节的部分是关于格式化输入输出操作&#xff0c;5-6节的部分是关于未格式化输入输出操作。 1. 控制布尔值的格式 一般…

uniapp API文档地址 以及 HBuilder安装

uniapp API文档地址 以及 HBuilder安装 一、进入 当前网站 uni-app 官网 [uni-app](https://zh.uniapp.dcloud.io/quickstart-hx.html)二、点击截图下载文件 三、 进入 当前网站 &#xff08;https://www.dcloud.io/hbuilderx.html&#xff09; 浏览器会识别 也可以自行选择…

基于GIS、RS、VORS模型、CCDM模型、geodetecto、GWR模型集成的生态系统健康的耦合协调分析

详情点击公众号&#xff1a;技术科研吧 链接&#xff1a;基于GIS、RS、VORS模型、CCDM模型、geodetecto、GWR模型集成的生态系统健康的耦合协调分析 前沿 当空间大数据、云计算与人工智能发生碰撞,地理服务产业也不断发生变革与进步。ArcGIS Pro 是一个专业的桌面 GIS 应用程…

17 外排序

排序分为内排序和外排序&#xff0c;内排序是在内存中的排序。外排序指在磁盘中文件的排序&#xff0c;因为在磁盘中&#xff0c;不能进行下标访问&#xff0c;归并排序经常用于磁盘中文件的排序 假如有10亿个整形数据在磁盘中&#xff0c;要对它排序&#xff0c;内存中只有1G…

水果FL Studio21.2最新中文版功能特点介绍

FL Studio 21的特点和优势包括&#xff1a; 丰富的主题换肤&#xff1a;用户可以通过调整色调、饱和度、亮度、文本、仪表和步进序列器的颜色&#xff0c;来个性化定制FL Studio 21的外观&#xff0c;使其更符合个人审美或工作风格。更快的音频编辑&#xff1a;FL Studio 21集…

AI少女/HS2甜心选择2 仿剑三剑灵人物卡全合集打包

AI少女/HS2甜心选择2 仿剑三剑灵人物卡全合集打包 内含&#xff1a;菩提禅音[剑网3]明教晓天喵姐[剑3]明教晓天喵姐无帽版[剑3]茱莉亚[剑灵] 下载地址&#xff1a; https://www.changyouzuhao.cn/12492.html

软件价值12-射箭游戏

射箭游戏&#xff0c;按空格键发射&#xff0c;打击移动靶&#xff0c;左上角显示成绩状态。 代码&#xff1a; import pygame import sys import random# 初始化Pygame pygame.init()# 设置窗口大小 SCREEN_WIDTH 800 SCREEN_HEIGHT 600 screen pygame.display.set_mode((…

【开源】基于JAVA+Vue+SpringBoot的就医保险管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 科室档案模块2.2 医生档案模块2.3 预约挂号模块2.4 我的挂号模块 三、系统展示四、核心代码4.1 用户查询全部医生4.2 新增医生4.3 查询科室4.4 新增号源4.5 预约号源 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVue…

DS:树及二叉树的相关概念

创作不易&#xff0c;兄弟们来波三连吧&#xff01;&#xff01; 一、树的概念及结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c…

利用Cloudflare Workers实现网页状态监控

首先 Fork cf-workers-status-page 浏览器地址栏输入 https://deploy.workers.cloudflare.com/?urlhttps://github.com/$YourUserName/cf-workers-status-page 获取 Cloudflare 账户内的 Account ID 和 API Token 授权的 token 需要 workes 的编辑权限 在 Github actio…

LeetCode二叉树的垂序遍历

题目描述 给你二叉树的根结点 root &#xff0c;请你设计算法计算二叉树的 垂序遍历 序列。 对位于 (row, col) 的每个结点而言&#xff0c;其左右子结点分别位于 (row 1, col - 1) 和 (row 1, col 1) 。树的根结点位于 (0, 0) 。 二叉树的 垂序遍历 从最左边的列开始直到…

DolphinScheduler安装与配置

DolphinScheduler概述 Apache DolphinScheduler是一个分布式、易扩展的可视化DAG工作流任务调度平台。致力于解决数据处理流程中错综复杂的依赖关系&#xff0c;使调度系统在数据处理流程中开箱即用。 DolphinScheduler的主要角色如下&#xff1a; MasterServer采用分布式无…

CPython:表达式的求值顺序(evaluation order)

相关阅读 Pythonhttps://blog.csdn.net/weixin_45791458/category_12403403.html?spm1001.2014.3001.5482 C中表达式的求值 C语言针对表达式的计算&#xff0c;设置了操作符的优先级和结合性这两个特性&#xff0c;优先级用于解析不同优先级的符号&#xff0c;结合性用于解析…

【基础】第K大与第K小数

说明 给定一个长度为N(0< n< 10000)的序列&#xff0c;保证每一个序列中的数字a[i]是正整数 &#xff0c;编程要求求出整个序列中第k大的数字减去第k小的数字的值m&#xff0c;并判断m是否为质数。(0< k< n) 输入数据 第一行为2个数n&#xff0c;k&#xff08;…

【北邮鲁鹏老师计算机视觉课程笔记】10 Classification 分类

【北邮鲁鹏老师计算机视觉课程笔记】10 Classification 分类 1 图像识别的基本范式 检测问题&#xff1a;不仅要知道有没有&#xff0c;还要知道在哪里 分类是整图级标签&#xff0c;检测是区域级标签&#xff0c;分割是像素级标签 2 检测任务的应用 3 单实例识别与类别识别…

《Linux 简易速速上手小册》第3章: 文件系统与权限(2024 最新版)

文章目录 3.1 Linux 文件系统结构3.1.1 重点基础知识3.1.2 重点案例&#xff1a;设置一个 Web 服务器3.1.3 拓展案例 1&#xff1a;日志文件分析3.1.3 拓展案例 2&#xff1a;备份用户数据 3.2 理解文件权限3.2.1 重点基础知识3.2.2 重点案例&#xff1a;共享项目文件夹3.2.3 拓…

java学习08---面向对象

一 类和对象 1 类和对象的理解 客观存在的事物皆为对象 &#xff0c;所以我们也常常说万物皆对象。 类 简单理解&#xff1a;类就是对现实事物的一种描述 类是对象的数据类型&#xff0c;类是具有相同属性和行为的一组对象的集合 类是对现实生活中一类具有共同属性和行为的事…

Python教程56:海龟画图turtle画kitty猫

---------------turtle源码集合--------------- Python教程91&#xff1a;关于海龟画图&#xff0c;Turtle模块需要学习的知识点 Python教程51&#xff1a;海龟画图turtle画&#xff08;三角形、正方形、五边形、六边形、圆、同心圆、边切圆&#xff0c;五角星&#xff0c;椭…

【python量化交易】qteasy使用教程02 - 获取和管理金融数据

qteasy教程2 - 获取并管理金融数据 qteasy教程2 - 获取并管理金融数据开始前的准备工作获取基础数据以及价格数据下载交易日历和基础数据查看股票和指数的基础数据下载沪市股票数据从本地获取股价数据生成K线图 数据类型的查找定期下载数据到本地回顾总结 qteasy教程2 - 获取并…

【Web】CVE-2021-31805 s2-062漏洞复现学习

目录 Struts2介绍 漏洞概况 OGNL与Struts2 简单原理 漏洞复现 正向rce 反弹shell payload分析 Struts2介绍 Struts 2 是一个流行的用于构建 Java Web 应用程序的开源 Web 应用程序框架。它是 Apache 软件基金会下的一个顶级项目&#xff0c;是 Struts 框架的升级版本。…
最新文章