统信UOS应用开发进阶教程

978-7-115-58010-8
作者: 统信软件技术有限公司
译者:
编辑: 赵祥妮

图书目录:

详情

统信UOS是一款界面美观、安全稳定的操作系统,可为用户提供丰富的应用生态。统信软件技术有限公司组织编写了两本统信UOS应用开发教程,分别面向初级和中级开发者。本书基于Qt 5.11.3,讲解统信UOS应用开发的进阶知识,涵盖了应用开发中级工程师必须掌握的大部分核心知识点,如多线程、通信机制、进程间通信、数据库操作、Qt的高级应用、调试与调优、桌面文件等。此外,根据统信UOS的特点,本书还介绍了统信开发套件DTK,以及从Windows向Linux迁移应用的方法。本书实战导向性强,精心设计了十余个项目案例,并在每章开头点明目标任务和通过项目可掌握的知识点,便于读者快速投入实战。

图书摘要

版权信息

书名:统信UOS应用开发进阶教程

ISBN:978-7-115-58010-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

版  权

著    统信软件技术有限公司

责任编辑 赵祥妮

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内容提要

统信UOS是一款界面美观、安全稳定的操作系统,可为用户提供丰富的应用生态。统信软件技术有限公司组织编写了两本统信UOS应用开发教程,分别侧重于实战和进阶。本书基于Qt 5.11.3,讲解统信UOS应用开发的进阶知识,涵盖了应用开发中级工程师必须掌握的大部分核心知识点,如多线程、通信机制、进程间通信、数据库操作、Qt的高级应用、调试与调优、桌面文件等。此外,根据统信UOS的特点,本书还介绍了DTK开发框架,以及从Windows向Linux迁移应用的方法。本书实战导向性强,精心设计了十余个项目案例,并在每章开头点明目标任务和通过项目可掌握的知识点,便于读者快速投入实战。

本书适合统信UOS的应用开发人员、信创领域公司以及个人开发者学习使用,也适合Qt开发人员阅读参考。

《统信UOS应用开发进阶教程》编委会

主 编:

  刘闻欢

副主编:

  张 磊 秦 冰

参编人员:

  王明栋 王 波 王耀华 史维星 邢 健 苏 雷 李 望 

  杨建民 张 宪 吴 丹 吴博文 邸国良 张文斌 张 松 

  张 亮 张海东 张继德 张 爽 陆 洲 金 业 金奇才 

  郑幼戈 赵 越 崔丽华 崔 湛 彭 浩 韩亚飞 湛忠祥

  郑 光 赵 耀

推荐序

拿到《统信UOS应用开发进阶教程》的样书,便被封面设计吸引,简洁、典雅,有大道至简之风。我想,如果内容也如封面这般赏心悦目,那就不失为一本“精品”。

这本教程延续了《统信UOS应用开发实战教程》的风格,保留了学练结合的特点,即理论与实践有机结合,理论部分的讲解仍然是深入浅出、循序渐进,实践案例仍然很有代表性,很接地气。

通读全书,不难发现这本教程极具统信UOS特色,将统信软件技术有限公司自主研发的DTK开发框架列入其中。这对于众多统信UOS开发爱好者来说是一个好消息,因为他们很早就在期盼统信DTK的亮相。这本教程的出版无疑是给与DTK开发者最好的礼物。

除了DTK开发框架之外,相比《统信UOS应用开发实战教程》,这本教程提供了更丰富的项目案例,在内容上也更具深度,更适合技术上有一定积累的开发者学习参考。教程涉及进程间通信、多线程同步、网络编程、数据库操作、插件的开发、Qt程序的调试与调优、程序迁移等内容,满足基于统信UOS的应用开发者技术精进的需要。值得称道的是,书中提供的很多资料是通过其他书籍或网络检索不易得到的,目前来看还算是独一份。总之,对于有一定Qt技术基础的开发者来说,这是一本值得借鉴的优质教程。

王耀华

统信软件技术有限公司 桌面操作系统产线总经理

2022年3月

前  言

统信软件技术有限公司(简称统信软件)于2019年成立,总部位于北京经开区信创园,在全国共设立了6个研发中心、7个区域服务中心、3地生态适配认证中心,公司规模和研发力量在国内操作系统领域处于第一梯队,技术服务能力辐射全国。

统信软件以“打造操作系统创新生态,给世界更好的选择”为愿景,致力于研发安全稳定、智能易用的操作系统产品,在操作系统研发、行业定制、国际化、迁移适配、交互设计等方面拥有深厚的技术积淀,现已形成桌面、服务器、智能终端等操作系统产品线。

统信软件通过了CMMI 3级国际评估认证及等保2.0安全操作系统四级认证,拥有ISO27001信息安全管理体系认证、ISO9001质量管理体系认证等资质,在产品研发实力、信息安全和质量管理上均达到行业领先标准。

统信软件积极开展国家适配认证中心的建设和运营工作,已与4000多个生态伙伴达成深度合作,完成20多万款软硬件兼容组合适配,并发起成立了“同心生态联盟”。同心生态联盟涵盖了产业链上下游厂商、科研院所等600余家成员单位,有效推动了操作系统生态的创新发展。(上述数据截至2022年3月,相关数据仍在持续更新中,详见统信UOS生态社区网站www.chinauos.com)

第1章 多线程和多线程同步

在一个程序中,独立运行的程序片段称为线程(Thread),多线程(Multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。需要有硬件支持,计算机才能够具有多线程能力,同时执行多个线程,从而提升整体的处理性能。具有这种能力的系统包括对称多处理机、多核处理器、芯片级多处理器以及同时多线程处理器。对线程进行编程处理的过程称为多线程处理。

【目标任务】

掌握多线程的状态和线程调度等概念、多线程的创建和管理、线程同步互斥量、死锁以及解决方案、线程同步读写锁、线程同步条件变量概念和具体的使用方法。

【知识点】

多线程的状态和线程调度。

多线程的创建和管理。

线程同步互斥量的使用方法。

死锁以及解决方案。

线程同步读写锁的使用方法。

线程同步条件变量的使用方法。

【项目实践】

项目案例1:通过条件变量实现生产者消费者模型,生产者只负责 生产数据,而消费者只负责消费数据。

项目案例2:通过信号量实现生产者消费者模型。

项目案例3:文件管理器多文件复制任务同步。

1.1 多线程的状态和线程调度

线程是操作系统能够进行运算调度的最小单位。大部分情况下,线程包含在进程中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发执行多个线程,每个线程执行不同的任务。

同一进程中的多个线程共享该进程中的全部系统资源,如虚拟地址空间、文件描述符、信号处理等。但同一进程中的多个线程有各自的调用栈(Call Stack)、寄存器环境(Register Context),以及独立的线程本地存储(Thread-local Storage)。

在多核或多路处理器以及支持超线程(Hyper-threading)的处理器上使用多线程程序设计的好处是显而易见的,即这样的设计可提高程序的执行吞吐率。即使是在单核处理器的计算机上,使用多线程技术也可以把进程中负责输入/输出(I/O)处理、人机交互而常被阻塞的部分与密集计算的部分分开执行,编写专门的工作线程执行密集计算,从而提高程序的执行效率。

线程从创建、运行到结束包括下面5个状态:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。线程状态之间的关系如图1-1所示。

图1-1 线程状态之间的关系

一旦线程进入可执行状态,就会在就绪状态与运行状态下转换,同时也有可能进入等待状态或死亡状态(线程执行完毕即进入死亡状态)。如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态;在其等待输入/输出结束时,线程进入就绪状态。对于阻塞线程来说,即使系统资源空闲,线程依然不能回到运行状态。当run函数执行完毕时,线程进入死亡状态。

计算机通常只有一个CPU,如果是单核CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,从宏观上来看,指各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU。线程调度是指按照特定机制为多个线程分配CPU的使用权。

线程调度有下面两种方式。

分时调度(系统级别):所有线程轮流拥有CPU的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度(语言级别):优先级高的线程先使用CPU,如果可运行线程池中的线程优先级相同,就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至其放弃CPU。

1.2 多线程的创建和管理

Qt通过3种形式提供对线程的支持,分别是平台无关的线程类、线程安全的事件投递、跨线程的信号槽连接。

Qt中主要的线程类如下。

QThread:提供跨平台的多线程解决方案。

QThreadStorage:提供逐线程数据存储。

QMutex:提供相互排斥的锁或互斥量。

QMutexLocker:辅助类,可自动对 QMutex 加锁与解锁。

QReadWriteLock:提供可以同时读写操作的锁。

QReadLocker与QWriteLocker:自动对QReadWriteLock 加锁与解锁。

QSemaphore:提供整型信号量,是互斥量的泛化。

QWaitCondition:线程在被其他线程唤醒之前一直休眠。

QThread是Qt线程中的一个公共的抽象类,所有的线程类都是从QThread抽象类中派生的。需要实现QThread中的虚函数run,可通过start函数来调用run函数。在默认情况下,QThread::run会启动一个事件循环(QEventLoop::exec)。线程相关的函数如下。

void run:线程体函数,用于定义线程的功能。

void start:启动函数,用于将线程入口地址设置为run函数。

void terminate:用于强制结束线程,不保证数据完整性和资源释放。

QCoreApplication::exec总是在主线程(执行main的线程)中被调用,不能从一个QThread中调用。在图形用户界面(GUI)程序中,主线程也称为GUI线程,是唯一允许执行GUI相关操作的线程。另外,必须在创建一个QThread前创建QApplication(或QCoreApplication)对象。

当线程启动和结束时,QThread会发送信号started和finished,可以使用isFinished和isRunning来查询线程的状态。

通过静态函数currentThreadId和currentThread返回当前正在执行的线程的标识,前者返回线程的ID,后者返回一个线程指针。

如果要设置线程的名称,可以在启动线程之前调用setObjectName。如果不调用setObjectName函数,那么线程的名称将是线程对象的运行时类型(QThread子类的类名)。

在新的线程中使用QThread类执行代码可通过以下几种方式。

1.继承QThread类

重写QThread的void run函数,在重写的函数中添加需要执行的代码。在以下代码中,NewThread类通过继承QThread类,重写run函数,实现一个新的线程。

  #include <QThread>
 
  class NewThread : public QThread
  {
  public:
      explicit NewThread(QObject *parent = 0);
  protected:
      void run()
  {
      // 具体语句      
  }

2.使用 QThread::create

使用 QThread::create(要求Qt版本至少为Qt 5.10)直接创建一个QThread对象,它可接收一个函数类型的参数,当调用QThread::start时会在新的线程中执行此函数。以下代码为使用QThread::create函数的示例。

  void function()
  {
      //具体语句
  }
  
  QThread *new_thread = QThread::create(function);
  new_thread->start();

3.直接创建一个QThread对象

直接创建一个QThread对象,将一个QObject对象移动到此线程,则使用QMetaObject :: invokeMethod调用此对象的槽函数就会在新的线程中执行。以下代码为使用moveToThread的示例。

  class Object : public QObject
  {
      Q_OBJECT
  public:
      explicit Object(QObject *parent = nullptr);
  
  public slots:
      void function() {
          //具体语句
      }
  };
  
  QThread *new_thread = new QThread();
  Object *object = new Object();
  object->moveToThread(new_thread);//将对象交给线程
  new_thread->start();
  QMetaObject::invokeMethod(object, "function");

QRunnable类可以和QThreadPool(线程池)配合使用。和QThread的第一种使用方法类似,开发者也需要通过继承并重写QRunnable::run函数,从而在新的线程中执行代码。以下代码为使用QRunnable的示例。

  class Runnable : public QRunnable
  {
  public:
      void run() override
      {
          //具体语句
      }
  };
  QRunnable *runnable = new Runnable();
  runnable->setAutoDelete(runnable);
  QThreadPool::globalInstance()->start(runnable);

QtConcurrent类是基于QRunnable封装的上层接口,可以很方便地在一个新的线程中执行一个函数,也可以和QThreadPool配合使用,以满足更灵活的功能需求。以下代码为使用QtConcurrent的示例。

  void function()
  {
      //具体语句
  }
  QtConcurrent::run(function);

1.3 线程同步

线程同步即当有一个线程在对内存地址进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他处于等待的线程才能对该内存地址进行操作,而别的线程又处于等待状态。

1.3.1 互斥量

线程锁能够保证临界资源的安全性,通常,每个临界资源需要一个线程锁进行保护。下面介绍两个概念。

临界资源:每次只允许一个线程访问的资源。

线程间互斥:多个线程在同一时刻都需要访问临界资源。

QMutex、QReadWriteLock、QSemaphore和QWaitCondition可提供线程同步的手段。使用线程主要是希望它们可以尽可能并发执行,而在一些关键点上线程之间需要停止或等待。例如,如果两个线程要同时访问同一个全局变量,则无法实现。

QMutex 类提供相互排斥的锁,或称为互斥量。在一个时刻至多一个线程拥有QMutex的某对象m_Mutex。假如一个线程试图访问已经被锁定的m_Mutex,那么该线程将休眠,直到拥有m_Mutex对象的线程对此m_Mutex解锁。QMutex常用来保护共享数据访问。QMutex类的所有成员函数是线程安全的。

在程序中使用QMutex 时需要声明头文件,在程序开始之前声明QMutex m_Mutex,在只能一个线程访问的代码之前加锁,代码之后解锁。相关的代码如下。

头文件声明: #include <QMutex>

互斥量声明: QMutex m_Mutex;

互斥量加锁: m_Mutex.lock();

互斥量解锁: m_Mutex.unlock();

如果对没有加锁的互斥量进行解锁,那么执行的结果是可能造成死锁。互斥量的加锁(Lock)和解锁(Unlock)必须在同一线程中成对出现。

QMutex有两种模式:Recursive和NonRecursive。

Recursive:一个线程可以对mutex对象多次加锁,直到相应次数的解锁调用后,mutex对象才真正被解锁。

NonRecursive:默认模式,mutex对象只能被加锁一次。

如果使用了m_Mutex.lock加锁而没有使用对应的m_Mutex.unlock解锁,就会造成死锁,其他线程将永远也得不到接触m_Mutex锁住的共享资源的机会。尽管可以不使用lock而使用tryLock(timeout)来避免“死等”造成的死锁[tryLock(负值)==lock()],但是还是可能造成错误。两个函数的具体情况如下。

bool tryLock:如果当前其他线程已对QMutex对象加锁,则该调用会立即返回,而不被阻塞。

bool tryLock(int timeout):如果当前其他线程已对该QMutex对象加锁,则该调用会等待一段时间,直到超时。

下面通过一个多线程售票的例子来看一下QMutex的使用。在这个例子中,首先通过继承QObject类,创建TicketSeller类,并创建两个对象——seller1和seller2,然后通过创建线程t1和t2,再将对象交给线程。在具体售票过程中,售票前先对互斥对象票的总数加锁,售票后再解锁释放。

#include <QCoreApplication>
#include <QObject>
#include <QThread>
#include <QMutex>
#include <string>
#include <iostream>
class TicketSeller : public QObject
{
public:
    TicketSeller();
    ~TicketSeller();
public slots:
    void sale();
    public:
    int* tickets;
    QMutex* mutex;
    std::string name;
};
TicketSeller::TicketSeller()
{
    tickets = 0;
    mutex = NULL;
}
TicketSeller::~TicketSeller()
{
}
void TicketSeller::sale()
{
    while((*tickets) > 0)
    {
        mutex->lock();//加锁
        std::cout << name << " : " << (*tickets)-- << std::endl;
        mutex->unlock();//解锁
    }
}
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    int ticket = 100;
    QMutex mutex;
    /*创建、设置线程1*/
    //创建线程1
    QThread t1;
    TicketSeller seller1;
    //设置线程1
    seller1.tickets = &ticket;
    seller1.mutex = &mutex;
    seller1.name = "seller1";
    //将对象移动到线程
    seller1.moveToThread(&t1);
    /*创建、设置线程2*/
    //创建线程2
    QThread t2;
    TicketSeller seller2;
    seller2.tickets = &ticket;
    seller2.mutex = &mutex;
    seller2.name = "seller2";
    //将对象移动到线程
    seller2.moveToThread(&t2);
    QObject::connect(&t1, &QThread::started, &seller1, &TicketSeller::sale);
    QObject::connect(&t2, &QThread::started, &seller2, &TicketSeller::sale);
    t1.start();
    t2.start();
    return a.exec();
}

编译运行后,可以看到票的总数通过两个线程从100依次递减到0。

另外还有一个QMutexLocker 类,主要用来管理 QMutex。使用 QMutexLocker 的好处是可以防止线程死锁。QMutexLocker在构造的时候加锁,析构的时候解锁。

1.3.2 死锁以及解决方案

多线程以及多进程可改善系统资源的利用率,并提高系统的处理能力。然而,运行过程中因争夺资源可能会造成一种僵局(Deadly-embrace),若无外力作用,这些进程(线程)都将无法向前推进。

产生死锁的条件,一是系统中存在多个临界资源且临界资源不可抢占,二是线程需要多个临界资源才能继续执行。死锁可采用的解决方案如下:对使用的每个临界资源都分配一个唯一的序号,对每个临界资源对应的线程锁分配相应的序号,系统中的每个线程按照严格递增的次序请求临界资源。

1.3.3 读写锁

QReadWriteLock 与QMutex相似,但对读写操作区别对待,可以允许多个读者同时读数据,但只能有一个写,并且读写操作不能同时进行。使用QReadWriteLock而不是QMutex,可以使多线程程序更具有并发性。 QReadWriteLock的默认模式是NonRecursive。

QReadWriteLock类成员函数如下。

QReadWriteLock:读写锁构造函数。

QReadWriteLock ( RecursionMode recursionMode):递归模式。在这种模式下,一个线程可以加多次相同的读写锁,直到相应数量的unlock被调用才能被解锁。

void lockForRead:加读锁。

void lockForWrite:加写锁。

QReadWriteLock ( RecursionMode NonRecursive):非递归模式。在这种模式下,一个线程仅可以加读写锁一次,不可递归。

bool tryLockForRead:尝试读锁定。如果读锁定成功,则返回true,否则它立即返回false。

bool tryLockForRead ( int timeout ):尝试读锁定。如果读锁定成功,则返回true;如果不成功,则等待timeout时间,等待其他线程解锁,当timeout为负数时,一直等待。

bool tryLockForWrite:尝试写锁定。如果写锁定成功,则返回true,否则它立即返回false。

bool tryLockForWrite ( int timeout ):尝试写锁定。如果写锁定成功,则返回true;如果不成功,则等待timeout时间,等待其他线程解锁,当timeout为负数时,一直等待。

void unlock:解锁。

下面给出了一个QReadWriteLock的使用实例,代码如下。

QReadWriteLock lock;
 void ReaderThread::run()
 {
     lock.lockForRead();
     read_file();
     lock.unlock();
 }
 
 void WriterThread::run()
 {
     lock.lockForWrite();
     write_file();
     lock.unlock();
 }

1.3.4 条件变量

QWaitCondition条件变量允许一个线程通知其他线程,如果所等待的某个条件已经满足,可以继续运行。一个或多个线程可以在同一个条件变量上等待。当条件满足时,可以调用wakeOne从所有等待在该条件变量上的线程中随机唤醒一个线程继续运行,也可以使用wakeAll同时唤醒所有等待在该条件变量上的线程。

QWaitCondition和QSemaphore一样,因为要访问共享资源,所以要和QMutex配合使用。

1.4 项目案例1:通过条件变量实现生产者消费者模型

工作过程中,可能会使用生产者消费者模型来处理各种应用场景,而生产者消费者模型指的是生产者只负责生产数据,而消费者只负责消费数据。例如网络通信的过程中,采用生产者来接收网络数据,而消费者负责处理网络数据,这样既能各司其职,又提高了网络的通信速度。下面通过使用常见的生产者消费者模型来说明一下条件变量的使用方法。

新建一个Qt控制台程序,再新建两个线程类Producer和Consumer,这两个类继承自QThread类。

先在main.cpp中声明要用到的全局变量。代码如下。

#include <QCoreApplication>
#include <QWaitCondition>
#include <QMutex>
#include <QQueue>
 
QQueue<int> buffer;
QMutex mutex;
QWaitCondition fullCond;// 缓冲区变满(有数据)
QWaitCondition emptyCond;// 缓冲区变空
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    return a.exec();
}

其中,buffer是缓冲区,mutex是保护缓存区的互斥量,条件变量fullCond用来等待缓冲区变满(有数据),条件变量emptyCond用来等待缓冲区变空。

下面来实现生产者和消费者线程。生产者线程代码如下。

void Producer::run()
{
    while(true)
    {
        mutex.lock();
        while(buffer.size() >= 10)//缓冲区变满
        {
            emptyCond.wait(&mutex);//等待缓冲区变空
        }
        int num = rand();//产生一个随机数
        buffer.enqueue(num);// 在队列尾部添加一个元素
        qDebug() << "enqueue: " << num;
        mutex.unlock();
        fullCond.wakeAll();//唤醒其他等待线程
    }
}

此处,假定缓冲区最多存储10个元素。先判断缓冲区是否已满,如果已满,则等待其变为空;否则,产生一个随机数放入队列中,最后通知消费者线程可以进行消费。

消费者线程代码如下。

void Consumer::run()
{
    while(true)
    {
        mutex.lock();
        while(buffer.size() <= 0)
        {
            fullCond.wait(&mutex);//没有数据,等待缓存区有数据
        }
        //如果有数据,则从队首取出一个元素
        qDebug() << "dequeue: " << buffer.dequeue();
        mutex.unlock();
        emptyCond.wakeAll();//唤醒其他等待线程
    }
}

对于消费者来说,他要先判断缓冲区是否有数据可消费。如果没有,则等待生产者生产出新的数据;如果有,则消费数据。最后,通知生产者继续生产。

1.5 项目案例2:通过信号量实现生产者消费者模型

信号量(Semaphore)有时被称为信号灯,是在多线程环境下使用的一种设施,可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其他想进入该关键代码段的线程必须等待,直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

QSemaphore 是QMutex的一般化,是特殊的线程锁,允许多个线程同时访问临界资源。信号量可以理解为对互斥量功能的扩展,互斥量只能锁定一次,而信号量可以获取多次,且可以用来保护一定数量的同种资源。acquire(n)用于获取n个资源,当没有足够的资源时,调用者将被阻塞直到有足够的可用资源。release(n)用于释放n个资源。

QSemaphore 类成员主要函数介绍如下。

QSemaphore (int n = 0) :建立对象时可以给它n个资源,默认为0。

void acquire (int n = 1) :获取1个资源。

int available () const :返回在任意时刻可用的资源数目。

void release (int n = 1) :释放1个资源。

bool tryAcquire (int n = 1) :如果得不到足够的资源会立即返回。

bool tryAcquire (int n, int timeout) :如果得不到足够的资源会等待timeout时间返回。

下面给出实现生产者消费者模型的代码。

#include <QtCore/QCoreApplication>
#include <QSemaphore>//信号量头文件
#include <QThread>
#include <cstdlib>
#include <cstdio>
 
const int DataSize = 100000; //数据大小
const int BufferSize = 8192; //缓存区大小
char buffer[BufferSize];
 
QSemaphore  production(BufferSize);
QSemaphore  consumption;
 
class Producer:public QThread
{
public:
    void run();
};
 
void Producer::run()
{
    for(int i = 0; i < DataSize; i++)
    {
        production.acquire();//获取信号量
        buffer[i%BufferSize] = "ACGT"[(int)qrand()%4];//随机获取一个字母
        consumption.release();//释放信号量
    }
}
 
class Consumer:public QThread
{
public:
    void run();
 
};
 
void Consumer::run()
{
    for(int i = 0; i < DataSize; i++)
    {
        consumption.acquire();//获取信号量
        fprintf(stderr, "%c", buffer[i%BufferSize]);
        production.release();//释放信号量
    }
    fprintf(stderr, "%c", "\n");
}
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Producer productor;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    
    return a.exec();
}

1.6 项目案例3:文件管理器多文件复制任务同步

在文件管理系统中,使用多线程进行多文件同时复制可以节省复制时间。是否使用多线程进行多文件复制,首先得明确瓶颈在于磁盘I/O还是在于调度。如果单线程文件复制已经占满磁盘I/O,那么多线程复制速度可能低于单线程文件复制。经验证,在统信UOS中进行单线程文件复制并不能占满磁盘I/O,所以通过多线程调度多文件并发复制,可以有效地加快复制速度。

1.6.1 线程的使用

复制文件夹这一应用场景符合多线程对多文件同时复制的需求,此处就以复制文件夹作为应用案例展开分析。

复制文件夹首先需要遍历出文件夹结构,文件夹结构类似于数据结构中的树,遍历文件夹如同遍历树。使用Qt库函数QFileInfoList QDir::entryInfoList(const QStringList &nameFilters, QDir::Filters filters = NoFilter, QDir::SortFlags sort = NoSort) const获取指定目录下的文件夹和文件列表。调用entryInfoList只会获取指定目录下的文件夹和文件列表,并不会获取子文件夹目录下的文件夹和文件列表。可以通过递归调用的方式遍历子目录,将子目录下的文件夹和文件列表信息存入队列之中。

主线程函数copy是整个复制的入口。要实现复制一系列文件到目标目录或者文件夹,其中重要的是遍历所有的源文件,即使用函数doCopy一个一个地复制源文件到目标目录或者文件夹,具体代码如下。

bool copyFileMoreThread::copy(const QStringList &source_path_list,
                              const QString dst_path)
{
    //复制
    m_sourcePathList = source_path_list;
    m_dstPath = dst_path;
    InfoPtr dst_info(new QFileInfo(dst_path));
 
    if (source_path_list.count() > 1 && !dst_info->isDir()) {
        qDebug() << source_path_list << " == " << dst_path;
        return false;
    }
    qDebug() << "start copy " << source_path_list << dst_path;
    if (!dst_info->exists()) {
        //获取程序当前运行目录
        QString current_path = QCoreApplication::applicationDirPath();
        dst_info->setFile(current_path);
        dst_info->refresh();
    }
 
    for (auto source_path : m_sourcePathList) {
        InfoPtr info(new QFileInfo(source_path));
        //所有的线程结束
        if (!info->exists()) {
            quitCopy();
            return false;
        }
        bool ok = doCopy(info,dst_info);
                if (!ok) {
            quitCopy();
            return ok;
        }
    }
    quitCopy();
    qDebug() << " copy over " << source_path_list;
    return true;
}

主线程函数doCopy可复制一个文件或者目录到目标文件夹,根据源文件的类型实现相应的复制操作,具体代码如下。

bool copyFileMoreThread::doCopy(InfoPtr source, InfoPtr dst)
{
    //是否是链接文件
    if (source->isSymLink()) {
        return copyLink(source, dst);
    }
    //是否是目录
    if (source->isDir()) {
        return copyDir(source,dst);
}
    else {
        //是文件就获取新目标的名称,将复制信息放入m_copyFileInfo链表
        //启动线程池去复制
        //目标是一个文件,并且目标文件存在
        if (dst->isFile() && dst->exists()) {
            return false;
        }
        QString new_dst_path = newFileName(source,dst);
        InfoPtr new_dst_info(new QFileInfo(new_dst_path));
        qint8 newcount = 0;
        //如果文件存在错误,则通过异步阻塞处理,此处未实现,读者可以尝试添加实现
        while(new_dst_info->exists()) {
            new_dst_path = newFileName(source,dst,++newcount);
            new_dst_info->setFile(new_dst_path);
            new_dst_info->refresh();
        }
        QMutexLocker lk(&m_copyFileInfoMutex);
        m_copyFileInfo.push_back(QPair<InfoPtr, InfoPtr>(source,new_dst_info));
        //启动线程去复制文件
        threadCopyFile();
    }
    return true;
}

在主线程执行复制时,如果当前是目录,就调用函数copyDir。主线程copyDir实现目标文件夹的创建,遍历源文件夹并且递归调用doCopy来执行每一个文件或者目录的复制,具体代码如下。

bool copyFileMoreThread::copyDir(InfoPtr source, InfoPtr dst)
{
    QString dst_path = newFileName(source, dst);
    InfoPtr dst_info(new QFileInfo(dst_path));
 
    QDir dir(source->absoluteFilePath());
    QFileInfoList file_info_list = dir.entryInfoList(QDir::AllEntries |                      QDir::NoDotAndDotDot | QDir::System | QDir::Hidden);
    //文件存在错误,通过阻塞进行错误处理,此处没有实现(只是强制合并到同一个文件夹)
    if (dst_info->exists() && dst_info->isFile()) {
        return false;
    }
 
    if (!dst_info->exists()) {
        if (!QDir::current().mkdir(dst_path)) {
            //创建目录失败错误处理
            return false;
        }
    }
   
    m_completeDir.push_back(dst_info->absoluteFilePath());
 
    for (auto fileinfo : file_info_list) {
        //排除已经复制过的文件
        if (m_completeDir.contains(fileinfo.absoluteFilePath())) {
            continue;
        }
        InfoPtr info(new QFileInfo(fileinfo.absoluteFilePath()));
        //递归调用doCopy执行文件的复制
        bool ok = doCopy(info,dst_info);
        if (!ok) {
            return false;
        }
    }
 
    return true;
}

1.6.2 线程池的使用

线程过多会增加系统调度开销,从而影响系统整体的性能。而线程池维护多个线程,等待分配可并发执行的任务。线程池可减小线程处理短时间任务时创建和销毁线程付出的代价,通过这种机制不仅能保证内核得到充分利用,还能防止过度的系统调度。线程池的线程数量取决于处理器内核和内存等资源,一般以CPU内核数量加2为宜,因为过多的线程会导致额外的线程切换开销。

当线程池中的线程处于非空闲状态时,新加入的任务会被分配到等待队列。只有当线程空闲下来的时候,才会去等待队列里获取未被执行的任务。如果没有任务,线程将进入休眠状态。

在对象copyFileMoreThread构造时设置线程池的最大线程数量。

m_threadPool.setMaxThreadCount(MAX_THREAD_COUNT);

函数threadCopyFile启动线程池,让线程执行复制。

void copyFileMoreThread::threadCopyFile()
{
    if (m_bThreadPoolStart.load()) {
        return;
    }
    for (int i = 0;i < MAX_THREAD_COUNT;++i) {
        m_threadPool.start(new copyFileTask(this));
    }
    m_bThreadPoolStart.store(true);
}

1.6.3 线程同步

当一个线程对一个内存地址进行操作时,其他线程不能对这个内存地址进行操作,直到该线程完成操作为止。线程同步的方法有很多,本示例选用互斥量来实现资源上锁。资源上锁主要用于防止多个线程同时访问同一个资源,如图1-2所示。

图1-2 资源上锁

注意 假设A线程对E资源加锁访问,如果A线程对E资源未解锁,此时B线程尝试加锁,B线程会被阻塞。C线程如果不加锁,可以直接访问E资源,但会出现数据混乱。

线程任务copyFileTask中的run是线程执行的核心函数,主要实现获取复制信息,根据复制信息复制文件,具体代码如下。

void copyFileTask::run()//根据复制信息复制文件
{
    if (!m_main) {
        qDebug() << "copyFileMoreThread ptr error";
        return;
    }
 
    while (!m_quitFlag.load()) {
        QPair<InfoPtr, InfoPtr> copy_info = m_main->getCopyInfo();
    //获取源文件和目标文件信息的数据
    if (!copy_info.first.data() || !copy_info.second.data()) {
            QThread::msleep(100);//无法获取则休眠
            continue;
        }
        bool ok = copyFile(copy_info.first,copy_info.second);
        if (!ok) {
            return;
        }
    }
    QPair<InfoPtr, InfoPtr> copy_info = m_main->getCopyInfo();
    while (copy_info.first.data() && copy_info.second.data()) {
            bool ok = copyFile(copy_info.first,copy_info.second);
        if (!ok) {
            return;
        }
        copy_info = m_main->getCopyInfo();
    }
 
}

在每个线程获取复制信息m_main->getCopyInfo时,会读取公共资源对象copyFileMoreThread的成员变量复制信息list,所以getCopyInfo通过互斥量来保护公共资源,具体代码如下。

QPair<InfoPtr, InfoPtr> copyFileMoreThread::getCopyInfo()
{
    QMutexLocker lk(&m_copyFileInfoMutex);
    if (m_copyFileInfo.count() > 0)
    {
        QPair<InfoPtr, InfoPtr> copyinfo = m_copyFileInfo.first();
        m_copyFileInfo.pop_front();
        return copyinfo;
    }
    return QPair<InfoPtr, InfoPtr>(InfoPtr(nullptr),InfoPtr(nullptr));
}

在主线程copyFileMoreThread::doCopy中复制文件时,也会读取公共资源,具体代码如下。

/*是文件就获取新目标的名称
将复制信息放入m_copyFileInfo链表
启动线程池去复制 */
else {
    //目标是一个文件,并且目标文件存在
    if (dst->isFile() && dst->exists()) {
        return false;
    }
    QString new_dst_path = newFileName(source,dst);
    InfoPtr new_dst_info(new QFileInfo(new_dst_path));
    qint8 newcount = 0;
    //文件存在错误,则通过异步阻塞处理
    while(new_dst_info->exists()) {
        new_dst_path = newFileName(source,dst,++newcount);
        new_dst_info->setFile(new_dst_path);
        new_dst_info->refresh();
    }
    //写入公共资源复制信息
    QMutexLocker lk(&m_copyFileInfoMutex);
    m_copyFileInfo.push_back(QPair<InfoPtr, InfoPtr>(source,new_dst_info));
    //启动线程池去复制
    threadCopyFile();
}

相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程