Qt 线程同步机制 互斥锁 信号量 条件变量 读写锁

qt线程同步

Qt提供了丰富的线程同步机制来帮助开发者更高效和安全地进行多线程编程。其主要包括:

  • QMutex:为共享数据提供互斥访问能力,避免同时写入导致的数据冲突。利用lock()/unlock()方法实现锁定和解锁。

  • QReadWriteLock:读写锁,允许多个读线程同时访问,但写操作需要独占锁。适用于读操作频繁,写操作较少的场景。

  • QSemaphore:信号量,用来限制特定资源的访问量。例如一次最多N个线程可以访问缓冲区。通过acquire()/release()实现获取和释放许可。

  • QWaitCondition:条件变量,可以让线程等待一个条件满足后被唤醒。配合QMutex完成更为复杂的同步需求。

  • Qfutures/QtConcurrent:提供了基于线程池的并行计算能力。将耗时任务放入线程池,自动进行并行执行与结果汇总。

  • QEventLoop/Signals&Slots:事件循环与信号槽机制可以实现复杂的线程间通信,例如定时器、Progress Reporting等。

  • QThread:Qt原生的线程类,通过moveToThread()实现在线程间安全地传递QObject。

互斥锁

QMutex / QMutexLocker 

    QMutex 是 Qt 中用于实现互斥锁的类,用于保证在多线程程序中访问共享资源的互斥性。它提供了两个基本操作:lock() 和 unlock(),分别用于加锁和解锁。

        QMutexLocker 是 QMutex 的 RAII 风格封装,可以自动释放锁资源,避免忘记解锁而导致的死锁情况。QMutexLocker 在创建时会自动调用 QMutex 的 lock() 方法,析构时会自动调用 QMutex 的 unlock() 方法。因此使用 QMutexLocker 可以大大减少忘记解锁的情况。

QMutex mutex;

void func() {
   
{
    QMutexLocker locker(&mutex);
    //临界区域

}
  
    mutex.lock();
    //临界区域
    mutex.unlock();
}
互斥锁实现买票同步程序

此处实现多线程重写版完美退出机制

#ifndef SELLER_H
#define SELLER_H


#include<QThread>
#include<QMutex>
#include<QDebug>
class seller :public QThread
{
    Q_OBJECT
public:
    seller()=default;
    seller(int* data,QMutex *m){
        tickets=data;
        mtx=m;
    }
signals:
    void ticketsFinsh();
public:
    void run() override{
        while ((*tickets) > 0) { //多线程经典问题 双锁机制
            mtx->lock();
            if((*tickets) > 0){
                mtx->unlock();
                mtx->lock();
                qDebug()<<this->objectName()<<" : " <<--(*tickets);

                mtx->unlock();
                QThread::usleep(100);
            }
        }

        ticketsFinsh();
    }
private:
    int* tickets; // 票
    QMutex *mtx;
};

#endif // SELLER_H


#include <QCoreApplication>

#include"seller.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int tickets =100;
    QMutex mtx;
    seller* t1 =new seller(&tickets,&mtx);
    seller* t2 =new seller(&tickets,&mtx);
    seller* t3 =new seller(&tickets,&mtx);

    t1->setObjectName("Thread 1");
    t2->setObjectName("Thread 2");
    t3->setObjectName("Thread 3");

    //设置线程完成信号与主线程进行连接,主线程知道子线程任务结束。
    QObject::connect(t1, &seller::finished, t1, &seller::quit);
    QObject::connect(t2, &seller::finished, t2, &seller::quit);
    QObject::connect(t3, &seller::finished, t3, &seller::quit);
   //线程对象在离开线程后删除,但不代表线程任务真正完成。
    QObject::connect(t1, &seller::finished, t1, &seller::deleteLater);
    QObject::connect(t2, &seller::finished, t2, &seller::deleteLater);
    QObject::connect(t3, &seller::finished, t3, &seller::deleteLater);
    //线程对象在离开线程 结束应用的通知
    QObject::connect(t1, &seller::finished, &a, &QCoreApplication::quit);
    QObject::connect(t2, &seller::finished, &a, &QCoreApplication::quit);
    QObject::connect(t3, &seller::finished, &a, &QCoreApplication::quit);

    //start与run的区别:调用run()直接在当前线程执行任务代码。 start()用于启动一个线程,将任务移动到新线程运行。它会调用run()函数,但run()此时会在新线程的上下文中执行。
    t1->start();
    t2->start();
    t3->start();

    t1->wait();
    t2->wait();
    t3->wait();
    return a.exec();
}

QSemaphore 信号量

成员函数

acquire(int n = 1):从信号量中获取n个信号量(如果当前信号量可用)。如果数量不足,则阻塞等待其可用。成功返回后信号量减少n个。
release(int n = 1):向信号量中增加n个信号量。其他正在等待的线程可能因此被唤醒。
tryAcquire(int n = 1):尝试从信号量中获取n个信号量,,但不阻塞等待。如果成功返回true并减去n,否则返回false。
available():返回当前信号量的数量。
cancelAcquire(n):取消acquire操作的等待,释放阻塞的线程。
tryAcquire(int n,int timeout):尝试获取n个信号量,等待最长timeout毫秒,成功或超时均返回。
tryAcquire(QSemaphore::Time timeout):tryAcquire的重载版本,等待时间采用Time类型。

这些方法提供了完整的获取/释放机制,开发者可以根据不同的同步需求灵活选择:
acquire()用于需要阻塞等待的情况;
tryAcquire()用于非阻塞获取,如有限资源池;
设置超时tryAcquire()可以防止死锁。

 基于信号量实现一个家庭买票

#ifndef SELLER_H
#define SELLER_H

#include<QThread>
#include<QSemaphore>
#include<QtDebug>
#include<QMutex>
class seller : public QObject
{
    Q_OBJECT

public:
    seller()=default;
    seller(QSemaphore* sem) : sem(sem) {i=0;}

signals:
    void emptyTicket();
public slots:
    void display(){

        while(sem->tryAcquire(3)) {
            qDebug() << this->objectName() << "售出:"<<++i<<"个家庭"<<" 剩余票数:"<<sem->available();
            QThread::yieldCurrentThread(); //让出当前时间片
        }

        emit emptyTicket();
    }

private:
    int i;
    QSemaphore *sem;

};

#endif // SELLER_H
#include <QCoreApplication>
#include"seller.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QThread t1,t2,t3; //3个工作人员售票
    /*
     * 空串原因
    t1.setObjectName("Thread1");
    t2.setObjectName("Thread 2");
    t3.setObjectName("Thread 3");
*/
    QSemaphore sem(99); // 33个家庭,每个家庭3个人 99张票 以家庭为单位售卖


    seller* s1 =new seller(&sem);
    s1->moveToThread(&t1);  //开辟新的线程去执行  槽函数也是在新的线程执行

    seller* s2 =new seller(&sem);
    s2->moveToThread(&t2);

    seller* s3 =new seller(&sem);
    s3->moveToThread(&t3);
    //因为class类this 是s对象
    s1->setObjectName("Seller1");
    s2->setObjectName("Seller2");
    s3->setObjectName("Seller3");

    QObject::connect(&t1,&QThread::started,s1,&seller::display);
    QObject::connect(&t2,&QThread::started,s2,&seller::display);
    QObject::connect(&t3,&QThread::started,s3,&seller::display);


    QObject::connect(s1,&seller::emptyTicket,&t1,&QThread::quit);
    QObject::connect(s2,&seller::emptyTicket,&t2,&QThread::quit);
    QObject::connect(s3,&seller::emptyTicket,&t3,&QThread::quit);


    QObject::connect(s1,&seller::emptyTicket,s1,&seller::deleteLater);
    QObject::connect(s2,&seller::emptyTicket,s2,&seller::deleteLater);
    QObject::connect(s3,&seller::emptyTicket,s3,&seller::deleteLater);

    QObject::connect(&t1,&QThread::finished,&t1,&QThread::deleteLater);
    QObject::connect(&t2,&QThread::finished,&t2,&QThread::deleteLater);
    QObject::connect(&t3,&QThread::finished,&t3,&QThread::deleteLater);

    t1.start();
    t2.start();
    t3.start();


    // 等待所有线程完成
    t1.wait();
    t2.wait();
    t3.wait();

    return a.exec();
}

 QWaitCondition 条件变量

QWaitCondition 是 Qt 框架中用于线程间同步的类之一。它允许一个线程等待另一个线程发出信号,从而实现线程间的协调和同步。 (相当于c11的条件变量)

        在使用 QWaitCondition 时,通常会配合使用 QMutex。QMutex 用于保护共享资源,而 QWaitCondition 则用于在等待某个条件为真时挂起线程,并在条件满足时唤醒线程。

成员函数 

void wait(QMutex *mutex):
将当前线程挂起,并释放指定的 QMutex。当某个其他线程调用 wakeOne() 或 wakeAll() 时,该线程将被唤醒。
当线程被唤醒时,它会重新获取 QMutex。

bool wait(QMutex *mutex, unsigned long time):
与上一个函数类似,但是带有超时时间参数。如果在指定的时间内没有被唤醒,该函数将返回 false。
返回 true 表示正常被唤醒,返回 false 表示超时。

void wakeOne():
唤醒一个正在等待的线程。如果没有线程正在等待,该函数什么也不做。
当有多个线程在等待时,哪个线程被唤醒是不确定的。

void wakeAll():
唤醒所有正在等待的线程。
所有等待的线程都将被唤醒,并重新获取相关的 QMutex。

基于条件遍历实现一个售票店与制票商供应程序

#ifndef PRODUCWORKER_H
#define PRODUCWORKER_H

#include"SellerWorker.h"

//模拟生产
class ProducWorker : public QObject {
    Q_OBJECT
private:
    QQueue<int> *curTicket;
    QMutex* mtx;
    QWaitCondition* cond;
    int *curTicketid ;
public:
    ProducWorker()=default;
    ProducWorker(QQueue<int> *curTicket,
    QMutex* mtx,
    QWaitCondition* cond,
    int *curTicketid){
        this->curTicket=curTicket;
        this->mtx=mtx;
        this->cond=cond;
        this->curTicketid=curTicketid;
    }
    ~ProducWorker()=default;
public slots:
    void work() {
        while (1) {
            {
                QMutexLocker lock(mtx);
                if (curTicket->length() < 20) { //假设最大生产20张票

                    int data=++(*curTicketid);
                    qDebug() << "生产票:" <<data ;
                    curTicket->push_back(data);
                    cond->wakeAll(); //通知商店售票一张

                } else {
                    cond->wait(mtx); //超过则等待销售
                }
                QThread::usleep(1000);
            }
        }
    }
};
#endif // PRODUCWORKER_H
#ifndef SELLERWORKER_H
#define SELLERWORKER_H
#include <QWaitCondition>
#include <QThread>
#include <QMutex>
#include <QQueue>
#include <QDebug>


//模拟售票
class SellerWorker : public QObject {
    Q_OBJECT
private:
    QQueue<int> *curTicket;
    QMutex* mtx;
    QWaitCondition* cond;
    int *curTicketid ;
public:
    SellerWorker()=default;
    SellerWorker(QQueue<int> *curTicket,
    QMutex* mtx,
    QWaitCondition* cond,
    int *curTicketid){
        this->curTicket=curTicket;
        this->mtx=mtx;
        this->cond=cond;
        this->curTicketid=curTicketid;
    }
    ~SellerWorker()=default;
public slots:
    void work() {
        while (1) {
            {
                QMutexLocker lock(mtx);
                if (!curTicket->isEmpty()) {

                    qDebug() << "售票:" << curTicket->front();
                    curTicket->pop_front();
                    cond->wakeOne(); //通知生产者 生产一张
                } else {
                    cond->wait(mtx);
                }
                QThread::usleep(1000);
            }
        }
    }
};
#endif // SELLERWORKER_H
#include <QCoreApplication>
#include"ProducWorker.h"
#include"ProducWorker.h"





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

    QQueue<int> curTicket;

    QMutex mtx;
    QWaitCondition cond;
    int curTicketid =0;
    QThread t1, t2;

    SellerWorker *sellerWorker =new SellerWorker(&curTicket,&mtx,&cond,&curTicketid);
    ProducWorker *producWorker =new ProducWorker(&curTicket,&mtx,&cond,&curTicketid);

    sellerWorker->moveToThread(&t1);
    producWorker->moveToThread(&t2);



    QObject::connect(&t1, &QThread::started, sellerWorker, &SellerWorker::work);
    QObject::connect(&t2, &QThread::started, producWorker, &ProducWorker::work);

    t1.start();
    t2.start();



    return a.exec();
}

QReadWriteLock 读写锁

        QReadWriteLock 是 Qt 提供的用于读写操作的锁类,允许多个线程同时读取共享数据,提高了并发性能,但在写操作时会阻止其他的读取和写入,以确保数据的一致性。

void lockForRead():
获取读锁。如果已有写锁被持有,当前线程将被阻塞,直到获得读锁。
可以被多个线程同时调用,只要没有写锁被持有。
void lockForWrite():
获取写锁。如果已有读锁或写锁被持有,当前线程将被阻塞,直到获得写锁。
一次只允许一个线程持有写锁。
void unlock():
释放当前持有的读锁或写锁。
如果有其他线程正在等待锁,它们将被唤醒。
bool tryLockForRead(int timeout = 0):
尝试获取读锁。如果成功获取,返回 true。
如果在指定的超时时间内无法获取读锁,返回 false。
bool tryLockForWrite(int timeout = 0):
尝试获取写锁。如果成功获取,返回 true。
如果在指定的超时时间内无法获取写锁,返回 false。

 基于读写锁实现3个写线程和5个读线程的同步

#ifndef CACHEMANAGER_H
#define CACHEMANAGER_H
#include <QDebug>
#include <QReadWriteLock>
#include <QThread>
#include <QVector>

class CacheManger : public QObject
{
    Q_OBJECT
public:
    CacheManger() {}
    virtual ~CacheManger() {}
    CacheManger( QVector<int> * data,QReadWriteLock* rwLock) :m_data(data),m_rwLock(rwLock){}

    void readData(int index) {
        while(1){
            m_rwLock->lockForRead();
            qDebug() << "Reading data at index:" << index << "- Data:" << m_data->at(index);
            m_rwLock->unlock();

        }
    }

    void writeData(int &sum) { //sum
        int &value = sum;
        while (1) {
            m_rwLock->lockForWrite();
            value+=1;
            int index =value % 5;
            (*m_data)[index] = value;
            qDebug() << "Writing data at index:" << index << "- Data:" << value;
            m_rwLock->unlock();
            QThread::usleep(1000);
        }
    }
private:
    QVector<int> * m_data ;
    QReadWriteLock* m_rwLock;
};
#endif // CACHEMANAGER_H
#include <QCoreApplication>
#include"CacheManager.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QVector<int> m_data = {1, 2, 3, 4, 5};
    QReadWriteLock m_rwLock;


    for(int i=0;i<5;i++){
        QThread* t =new QThread;
        CacheManger* read = new CacheManger(&m_data,&m_rwLock);
        read->moveToThread(t);

        QObject::connect(t,&QThread::started,read,[read,i](){
                read->readData(i);
        });
        t->start();
    }

    int sum=0;
    for(int i=0;i<3;i++){
        QThread* t =new QThread;
        CacheManger* write = new CacheManger(&m_data,&m_rwLock);
        write->moveToThread(t);

        QObject::connect(t,&QThread::started,write,[write,&sum](){  //3个线程对缓冲区循环写入数据

              write->writeData(sum);

        });
        t->start();
    }
    return a.exec();
}

总结

        上述展示了Qt多线程编程的基本知识,与C语言、C++等语言线程大体上一致。线程间的同步和互斥涉及到的知识点大体不差。Qt还可以通过信号和槽连接,在不同线程之间进行通信和同步。Qt多线程编程更需要关注的点是线程的生命周期以及如何更加优雅的退出线程。Qt除了线程还提供了一些模块、线程池等功能和机制来实现并发执行。

 参考文献:
       一文搞定之Qt多线程(QThread、moveToThread)_qthread movetothread-CSDN博客

最后附上源代码链接
对您有帮助的话,帮忙点个star

37-Qmutex-QSempahor · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)

38-QSempahor · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)

39-QWaitCondition · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)

40-QReadWriteLock · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)

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

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

相关文章

Java面试八股之MySQL中int(10)和bigint(10)能存储读的数据大小一样吗

MySQL中int(10)和bigint(10)能存储读的数据大小一样吗 在MySQL中&#xff0c;int(10)和bigint(10)的数据存储能力并不相同&#xff0c;尽管括号内的数字&#xff08;如10&#xff09;看起来似乎暗示着某种关联&#xff0c;但实际上这个数字代表的是显示宽度&#xff0c;而不是…

基于信号量的生产者消费者模型

文章目录 信号量认识概念基于线程分析信号量信号量操作 循环队列下的生产者消费者模型理论认识代码部分 信号量 认识概念 信号量本质: 计数器 它也叫做公共资源 为了线程之间,进程间通信------>多个执行流看到的同一份资源---->多个资源都会并发访问这个资源(此时易出现…

Python OpenCV 教学取得视频资讯

这篇教学会介绍使用OpenCV&#xff0c;取得影像的长宽尺寸、以及读取影像中某些像素的颜色数值。 因为程式中的OpenCV 会需要使用镜头或GPU&#xff0c;所以请使用本机环境( 参考&#xff1a;使用Python 虚拟环境) 或使用Anaconda Jupyter 进行实作( 参考&#xff1a;使用Anaco…

关于.NETCORE站点程序部署到nginx上无法访问静态文件和无法正确生成文件的问题解决过程。

我的netcore6项目&#xff0c;部署到IIS的时候&#xff0c;生成报告时&#xff0c;需要获取公司LOGO图片放到PDF报告文件中&#xff0c;这时候访问静态图片没有问题。 然后还有生成邀请二维码图片&#xff0c;这时候动态创建图片路径和图片也没有问题&#xff0c;可以在站点的…

14-58 剑和诗人32 - 使用矢量数据库增强 LLM 应用程序

GPT-4、Bloom、LaMDA 等大型语言模型 (LLM) 在生成类似人类的文本方面表现出了令人印象深刻的能力。然而,它们在事实准确性和推理能力等方面仍然面临限制。这是因为,虽然它们的基础是从大量文本数据中提取统计模式,但它们缺乏结构化的知识源来为其输出提供依据。 最近,我们…

Python:安装/Mac

之前一直陆陆续续有学python&#xff01;今天开始&#xff01;正式开肝&#xff01;&#xff01;&#xff01; 进入网站&#xff1a;可能会有点慢&#xff0c;多开几个网页 https://www.python.org 点击下载&#xff0c;然后进入新的页面&#xff0c;往下滑 来到File&#xff0…

成为编程大佬!!——数据结构与算法(1)——算法复杂度!!

前言&#xff1a;解决同一个程序问题可以通过多个算法解决&#xff0c;那么要怎样判断一个算法的优劣呢&#xff1f;&#x1f914; 算法复杂度 算法复杂度是对某个程序运行时的时空效率的粗略估算&#xff0c;常用来判断一个算法的好坏。 我们通过两个维度来看算法复杂度——…

c++ 多边形 xyz 数据 获取 中心点方法

有需求需要对。多边形 获取中心点方法&#xff0c;绝大多数都是 puthon和java版本。立体几何学中的知识。 封装函数 point ##########::getCenterOfGravity(std::vector<point> polygon) {if (polygon.size() < 2)return point();auto Area [](point p0, point p1, p…

leetcode--从中序与后序遍历序列构造二叉树

leeocode地址&#xff1a;从中序与后序遍历序列构造二叉树 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder …

Oracle基础以及一些‘方言’(二)

1、Oracle的查询语法结构 Oracle 的单表查询的语法结构&#xff1a; SELECT 1 FROM 2 WHERE 3 GROUP BY 4 HAVING 5 ORDER BY 6 其每个关键词的功能与MySQL中的功能已知&#xff0c;不过分页查询的关键词 limit 并不在Oracle的语法结构中。伪列&#xff1a; 在 Oracle 的表的使…

资料分析笔记整理

提升技巧多做题、少动笔、多分析 资料分析认识 国考一般20题(24~28分钟) 统计材料的类型包括单纯的文字、表格、图形以及由这些元素组成的复合类型材料 文字性材料:(30~60秒) 多段落型文字材料(时间、关键词、结构) 孤立段落文字材料(时间、关键词、标点[。;]) 表…

Linux 利用命名空间创建一个自己的“容器“

Linux 利用命名空间创建一个自己的"容器" 前置条件 创建一个目录存放容器mkdir /myapp准备静态编译busybox&#xff0c;操作系统自带的往往是依赖动态库的(本文使用的debian apt install busybox-static) 开始 使用unshare起一个独立命名空间.# 进入后/myapp目录…

如何理解http与https协议,他们有什么区别?

写在前面的话&#xff0c;关于 HTTP 和 HTTPS 的问题&#xff0c;常常会被很多学习者忽略&#xff0c;HTTP、HTTPS 不就是网址的开头吗&#xff0c;有啥好了解的&#xff0c;浏览器的引擎实现了这个协议&#xff0c;在开发关系不大&#xff0c;但想要深入一些理解数据传输原理&…

《植物大战僵尸杂交版》2.2版本:全新内容与下载指南

《植物大战僵尸杂交版》2.2版本已经火热更新&#xff0c;带来了一系列令人兴奋的新玩法和调整&#xff0c;为这款经典的塔防游戏注入了新的活力。如果你是《植物大战僵尸》系列的忠实粉丝&#xff0c;那么这个版本绝对值得你一探究竟。 2.2版本更新亮点 新增看星星玩法 这个新…

HarmonyOS鸿蒙DevEco Studio无法连接本地模拟器

使用DevEcoStudio 5.0.3.403版本 发现无法选择模拟器 解决方法&#xff1a; 1、打开模拟器 2、关闭DevEco Studio&#xff0c;&#xff08;不要关闭模拟器&#xff09; 3、重新打开DevEco Studio。

效果惊人!LivePortrait开源数字人技术,让静态照片生动起来

不得了了,快手已经不是众人所知的那个短视频娱乐平台了。 可灵AI视频的风口尚未过去,又推出了LivePortrait--开源的数字人项目。LivePortrait让你的照片动起来,合成逼真的动态人像视频,阿里通义EMO不再是唯一选择。 让图像动起来 LivePortrait 主要提供了对眼睛和嘴唇动作的…

Junior.Crypt.2024 CTF Web方向 题解WirteUp 全

Buy a cat 题目描述&#xff1a;Buy a cat 开题 第一思路是抓包改包 Very Secure App 题目描述&#xff1a;All secrets become clear 开题 乱输一个密码就登陆成功了&#xff08;不是弱口令&#xff09; 但是回显Your role is: user 但是有jwt&#xff01;&#xff01;&a…

线程池【开发实践】

文章目录 一、为什么要用线程池1.1 单线程的问题1.2 手动创建多线程的问题1.3 线程池的作用&#xff08;优点&#xff09;1.4 线程池的使用场景 二、线程池的基础知识2.1 线程池的核心组件2.2 JUC中的线程池架构2.3 线程池的配置参数2.4 线程池常见的拒绝策略&#xff08;可自定…

看影视学英语(假如第一季第一集)

in the hour也代表一小时吗&#xff1f;等同于in an hour&#xff1f;

科研绘图系列:R语言小提琴图(Violin Plot)

介绍 小提琴图(Violin Plot)是一种结合了箱线图和密度图的图表,它能够展示数据的分布密度和分布形状。以下是对小提琴图的详细解释: 小提琴图能表达: 数据分布:小提琴图通过在箱线图的两侧绘制曲线来展示数据的分布密度,曲线的宽度表示数据点的密度。集中趋势:箱线图部…