创建线程与互斥锁

范例类:

#include <QWidget>
#include <QMutex>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
// f1和f2用于两个线程,用于修改x,y并显示
    void f1();
    void f2();


private:
    int x{1};
    int y{2};
    QMutex loc;
};
// 构造函数
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
//    QtConcurrent::run(this,&Widget::f1);
//    QtConcurrent::run(this,&Widget::f2);
}
// f1()和f2()的实现
void Widget::f1()
{
//  loc.lock(); 加锁
    cc<<"begin:f1x:"<<x<<"\t"<<"f1y:"<<y;
    ++x;
    ++y;
    cc<<"end:f1x:"<<x<<"\t"<<"f1y:"<<y;
//  loc.unlock(); 解锁
}

void Widget::f2()
{
//  loc.lock();
    cc<<"begin:f2x:"<<x<<"\t"<<"f2y:"<<y;
    ++x;
    ++y;
    cc<<"end:f2x:"<<x<<"\t"<<"f2y:"<<y;
//  loc.unlock();
}

创建线程

​ 这里主要是为了讲解互斥锁,创建线程将会以最简单的一种方式创建:

使用QtConcurrent创建线程:

#include <QtConcurrent/QtConcurrent>
QtConcurrent::run(this,&Widget::f1); // 将f1移交到新线程中运行

互斥锁

​ 互斥锁(Mutex)再Qt里实现为QMutex,基类为QBasicMutex。Mutex可以确保多个线程按次序进入临界区

互斥限制机制

QMutex无需指定临界资源。对QMutexlockunlock只能由一个线程进行,其他进程在尝试lock QMutex时,若QMutex已被加锁,则等待至QMutex解锁之后再运行后续代码。

QMutex可以指定为Recursion(迭代),这使QMutex在一个线程里可以被多次加锁,要解锁的数量要与加锁的次数相同。

代码运行

在没加QMutex时,代码运行结果如下:

1559213426324

对f1加锁而对f2不加锁:

1559213561419

对f1和f2都加锁:

1559213623206

​ 可以看到,仅仅对一个函数加锁没有任何作用,要想实现对临界资源访问的限制,所有访问临界资源的函数都应当在进入临界区时加锁,退出临界区时解锁

QMutex成员函数和属性

QMutex的属性如下:

1559214210299

QMutex的所有函数成员如下(包含继承成员):

1559214165583

​ 该类所有成员函数都是线程安全的。

lock()

​ lock()函数用于对QMutex进行加锁,若QMutex已被加锁,则该线程将被阻塞直至QMutex解锁。

RecursionMode=Recursive时,QMutex可被一个线程多次lock,要解锁QMutex,必须调用相同次数的unlock

tryLock(int timeout=0)

​ 若加锁失败将会返回false并等待timeout毫秒。

try_lock()

​ 若加锁失败将会返回false,该函数与tryLock相同,用于兼容标准库。

QMutex::try_lock_for(std::chrono::duration<Rep, Period> duration)

​ 若加锁失败或QMutex是一个NonRecursive将会返回false

QMutexLocker

QMutexLocker为lockunlock提供了便利。

其构造函数接受一个QMutex*,并将会在构造函数中对QMutex进行lock,在析构函数中进行unlock

成员函数

其所有成员函数如下:

1559215058503

例:

void Widget::f1()
{
    QMutexLocker locker(&loc); // 构造加锁
    cc<<"begin:f1x:"<<x<<"\t"<<"f1y:"<<y;
    ++x;
    ++y;
    cc<<"end:f1x:"<<x<<"\t"<<"f1y:"<<y;
}// 析构解锁

多线程的常用术语

一、原语

​ 用于操作系统计算机网络术语。此处指操作系统中的原语:

原语用以指代一组不可分割、必须连续执行的一段程序代码。在多线程里,原语在执行过程中不允许被中断。原语实现由CPU实现,由操作系统进行封装,由库作者提供给程序员进行使用。

原语的分类

1. 请求原语(Req)

​ 用于高层向底层请求某种业务。

2. 证实原语(Cfm)

​ 业务提供层证实某个动作已经完成。

3. 指示原语(Ind)

​ 业务提供层向高层报告一个与特定业务相关的动作。

4. 响应原语(Res)

​ 用于应答,表示来自高层的指示原语已收到。

二、进程控制原语

进程控制原语包括:

  1. 进程的建立

  2. 进程的撤销

  3. 进程的等待

  4. 进程的唤醒

三、同步原语

​ 同步原语是建立在操作系统上的,是操作系统对多线程中的线程协同工作、线程间通信、线程同步、临界资源的竞争等问题提出的解决方案。

​ 主要的抽象同步原语抽象模型有:

  • 互斥锁(Mutual)

  • 条件变量(Condition Variable)

  • 信号量(Semaphores)

  • 信箱(MailBox)

  • 事件标志(Event Flag)

  • 自旋锁(SpinLock)

1. 互斥锁(Mutual)

​ 通过设置互斥锁可以控制对临界资源的访问,互斥锁保证了任意时刻最多有一个线程可访问临界资源

2.条件变量

​ 条件变量需要和互斥锁配合使用。是一种等待同步机制。用于条件变量的线程首先锁定互斥锁,然后循环判断某个变量是否满足。若条件不满足,阻塞线程并释放互斥锁。当其他线程处理条件并使其满足时,由其他线程调用通知函数唤醒该线程。

3. 信号量

​ 信号量用于管理资源。每个信号量都拥有一个计数器用于储存资源的数量(非负整数)。进入临界区的线程会消耗一个信号量,退出临界区的线程会归还一个信号量。

​ 当信号量为零时,线程会被阻塞,进入等待队列,等待信号量大于零后由其他线程唤醒。

4.信箱

​ 信箱由两条消息队列组成,一条用于发送消息,一条用于接收消息。消息具有有限的容量,广泛被用于线程和进程间通讯。

5. 事件标志

​ 事件标志用于表示一个或几个不同类型的事件的状态。物理结构类似与标志寄存器,使用位(bit)来描述事件的状态,事件标志允许进程:

  1. 阻塞到所有事件发生为止。

  2. 至少有一个事件发生。

  3. 直到所有事件发生为止并清除事件标志。

  4. 至少有一个事件发生并清除事件标志。

6. 自旋锁(SpinLock)

​ 自旋锁的运行级别低于其他同步原语,用于中断处理硬件资源共享。自旋锁时一种等待的锁机制,当发生资源冲突时,线程会不断尝试获取锁以进入临界区,而不是进入阻塞。

读写锁

​ 读写锁时一种特殊的自旋锁,其将资源的访问划分为读、写和读写,因而产生了读锁(ReadLock)写锁(WriteLock)读写锁(ReadWriteLock)

​ 读锁保证读,写锁保证写,读写锁保证读写。

简答题

  1. 什么是低级通信原语和高级通信原语

    利用信号量实现进程间信息交换的通信原语,即P.V操作.

  2. P原语和V原语是解决进程间同步和互斥的一对低级通信原语。

参考

操作系统概念—原语

同步原语

C++ STL条件变量

百度百科:信号量

消息邮箱和消息队列

百度百科:消息队列

应用消息队列设计可以解决哪些实际问题?

【uTenux实验】事件标志

STM32学习之:事件标志组

蜗窝科技 spin lock (讲的非常不错)

百度百科:读写锁# 可运行对象与线程池

可运行对象

​ QT使用QRunnable来提供可运行对象的接口 。可运行对象代表了一个需要被运行任务(task)一段代码(piece of code)

​ QRunable是一个抽象基类,你必须将重写派生类的run接口。线程池可以通过run()接口运行QRunable对象。

QRunable的属性

​ QRunable只有一个隐含属性:autoDelete(bool),当该值为true时,则对象可以被线程池自动删除。

QRunable成员函数

1559282649295

线程池

​ 线程的频繁创建与销毁会带来很大的性能问题,为了解决线程的调度问题,计算机科学家提出了线程池(Thread Pool)的概念。线程池本身是一个独立的后台线程,其使用队列的方式维护着多个线程。当线程任务执行完毕后,不再进行销毁,而是进入空闲状态,等待线程池为其分配任务。

​ 线程池有固定的大小,若排队的任务过多,则多余任务将会被挂起,等待线程池调度。

线程池保证了内核的充分利用,防止了过度调度。

影响线程池的因素

  • 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。

  • 销毁太多线程,将导致之后浪费时间再次创建它们。

  • 创建线程太慢,将会导致长时间的等待,性能变差。

  • 销毁线程太慢,导致其它线程资源饥饿。

线程池的组成

​ 下面介绍服务器利用线程池的一个实现:

  1. 线程池管理器(Thread Pool Manager):用于创建并管理线程池

  2. 工作线程(Work Thread): 线程池中线程

  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。

  4. 任务队列:用于存放没有处理的任务。提供一种缓冲机制。

QT中的线程池

​ QT使用QThreadPool类表示一个线程池实例。其基类为QObject。所有QThreadPool成员函数都是线程安全的。

QThreadPool属性

  • activeThreadCount : const int

  • expiryTimeout : int

  • maxThreadCount : int

  • stackSize : uint

  • objectName : QString (继承自QObject)

| 属性名 | 属性作用 | | :—————: | :————————–: | | activeThreadCount | 保存了线程池中活动的线程数量 | | expiryTimeout | 储存了等待线程的过期时间 | | maxThreadCount | 储存了最大线程数目 | | stackSize | 储存了线程池中线程的栈大小 |

公有成员函数

1559284204992

继承公有函数

1559284367552

静态成员函数

1559284274431

继承静态成员函数

1559284411499

继承的槽函数和信号函数

1559284458985

继承的保护成员函数

1559284495295

使用线程池

​ 要使用QThreadPool中的线程,需要将一个QRunbale对象的拥有权移交给QThreadPool对象。向QThreadPool::start()成员函数传入QRunable对象可达到这一目的:

class HelloWorldTask : public QRunnable
{
    void run() {
        qDebug() << "Hello world from thread " << QThread::currentThread();
    }
}

HelloWorldTask *hello = new HelloWorldTask();

// QThreadPool取得所有权,并自动删除 hello
QThreadPool::globalInstance()->start(hello);

参考资料

百度百科:线程池

Qt 之 QThreadPool 和 QRunnable# 同步类

​ 以下类用于同步线程:

  1. QWaitCondition(线程安全)

  2. QSemaphore

  3. QSystemSemaphore

QWaitCondition

QWaitCondition所有的函数都是线程安全的。

​ QWaitCondition是QT对同步原语中条件变量的实现。当条件满足时进入临界区。

QWaitCondition同一时刻只能由一个线程访问,一个线程的访问会导致其他线程的QWaitCondition无法访问,除非调用wakeOne()wakeAll()唤醒其他线程中的QWaitCondition对象。

例:

#include <QWidget>
#include <QMutex>
#include <QWaitCondition>
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();

private:
    QWaitCondition contion1;
    QMutex *mmutex1=new QMutex;
};
//
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    mmutex1->lock();
    QThread *thd1=QThread::create([&](){
       contion1.wait(mmutex1); //等待其他contion1无其他线程wake
       cc<<"contiion aa";
       contion1.wakeAll();
    });

    QThread *thd2=QThread::create([&](){
        cc<<"bb";
        contion1.wakeAll();// 唤醒contion1
    });

    thd1->start();
    thd2->start();
}

​ wait函数需要一个QMutex对象指针作为参数,wait会阻塞当前线程并等待其他线程调用QWaitCondition对象的wakeOnw或wakeAll函数。当一个QWaitCondition被wake后,被阻塞的线程得以继续运行。

​ QWaitConditiond::wait的参数是一个已被加锁的QMutex,在wait期间,QMutex会被自动解锁。

使用不同线程输出1到17

​ 利用wait会将互斥锁解锁的特点,我们可以使用两个线程来输出连续结果:实现设定临界资源i=0,然后在设定两个线程,一个输出0~3,6~17;另一个输出3~6。

​ 对两个线程访问i时设定互斥锁,然后一个输出到3,wait。而另一个线程将获得锁,输出3~6,唤醒WaitContion后第一个线程输出6~17:

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();

private:
    QWaitCondition contion1;
    QMutex *mmutex1=new QMutex;
    int i=0;
};
//
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{

    QThread *thd1=QThread::create([&](){
       mmutex1->lock();
       for (;i<17;++i) {
            cc<<QThread::currentThreadId()<<i;

            if(i==3){
                contion1.wait(mmutex1);
            }
       }
       mmutex1->unlock();
    });

    QThread *thd2=QThread::create([&](){

        mmutex1->lock();
        for (;i<6;++i) {
            cc<<QThread::currentThreadId()<<i;
        }
        mmutex1->unlock();
        contion1.wakeAll();
    });

    thd1->start();
    thd2->start();

}

输出结果如下:

0x208c 0
0x208c 1
0x208c 2
0x208c 3
0x17cc 3
0x17cc 4
0x17cc 5
0x208c 7
0x208c 8
0x208c 9
0x208c 10
0x208c 11
0x208c 12
0x208c 13
0x208c 14
0x208c 15
0x208c 16

信号量

​ Qt中信号量类一共有三个:

  1. QSemaphore

  2. QSystemSemaphore

  3. QSemaphoreReleaser(异常安全)

QSemaphore

​ QSemaphore提供了一个资源计数系统,每个QSemaphore对象可以在初始化时指定资源数目,并使用acquire(int n=0)来请求n个资源,使用release(int n=0)归还n的资源。当请求的资源不足时,线程会被阻塞直至有充足的资源。

例:(来自Qt Doc)

QSemaphore sem(5);      // sem.available() == 5

sem.acquire(3);         // sem.available() == 2
sem.acquire(2);         // sem.available() == 0
sem.release(5);         // sem.available() == 5
sem.release(5);         // sem.available() == 10

sem.tryAcquire(1);      // sem.available() == 9, returns true
sem.tryAcquire(250);    // sem.available() == 9, returns false

QSemaphoreReleaser

​ QSemaphoreReleaser为 QSemaphore::release()的调用提供了一个异常安全(exception-safe)

你可以构造一个QSemaphoreReleaser(QSemaphore &sem, int n = 1)对象,当其析构时将自动会自动从sem中释放n个资源。

​ QSemaphoreReleaser主要是为了防止由于代码执行中出现异常而导致的资源未释放问题。

例如:(Qt Doc)

const QSemaphoreReleaser releaser(sem);
// ... do something that may throw or early return
// implicitly calls sem.release() here and at every other return in between

Future

Future的基本概念

​ Future,我借用数学术语,将其译为期望

​ 在多线程编程中,若主线程需要子线程运行的结果,但是主线程又无需等待子线程结束,那么,就可以使用Futrue来代表子线程“未来的结果”。主线程通过检查子线程的进度,来决定对其结果(也就是Future)的使用。

​ 由于子线程与主线程的进度是不同步的,因此称为两者是异步的。Future代表了一个异步计算的结果

先看一个伪代码示例:

main thread{
    Future fu=SubThread(); // fu代表了SubThread()未来计算的结果

    do_something(); // 主线程与子线程异步进行

    if(SubThread().isFinished()){ // 若子线程任务完成
        cout<<fu<<endl; // 打印子线程的结果
    }

}

由于Future代表了子线程未来的结果,而子线程的结果应当是我们期望得到的,因此,我将其译为 “期望”。

QFuture

QFuture状态

​ QFuture不仅代表了一个状态,而且还可以通过QFuture对子线程进行一定控制,QFuture代表的异步子线程有状态,其操作与相应说明如下:

状态名 进入状态函数 确定状态函数
开始 bool isStarted()
运行 bool isRunning()
暂停 void pause() bool isPaused()
void setPaused(bool paused)
void togglePaused()
继续 void resume()
完成 void waitForFinished() bool isFinished()
取消 void cancel() bool isCanceled()
结果可被读取 bool isResultReadyAt(int index)

异步暂停

​ QFuture的暂停并不是无条件的,只有满足条件的异步计算才可以被暂停,这一点将会在QtConcurrent中说明。

即使isPauseed()返回true,计算可能仍在进行

QFuture类型

​ QFuture是一个模板类,根据子线程运行函数的不同,QFuture的类型也不同,其类型选择标准如下:

  1. QFutrue<void>用于不包含任何结果的函数。用于没有返回值或不需要结果(只是用来监控异步进度)的函数。

  2. QFuture<T>用于获取返回值类型为T的异步计算结果。

异步计算结果

要访问QFuture的结果,可以使用resutAt()进行索引,或者使用迭代器进行访问。

对于拥有多个结果的QFuture对象,resultCount()返回结果的数量。

QFuture将在下一节进行演示# QtConcurrent并行

​ QtConcurrent是Qt中的一个命名空间,位于concurrent模块中。QtConcurrent提供可一系列高级API,可以在不使用低级线程原语(互斥锁、自旋锁、信号量和条件变量)的情况下编写多线程程序。

开辟线程

​ 与QThread::create()创建一个线程并返回线程指针不同。QtConcurrent::run()在独立线程中运行函数并返回异步计算结果。

Run()

  1. QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, …)

    在线程池pool中运行函数并返回QFuture<T>。T与函数的返回值相同。

  2. QFuture<T> QtConcurrent::run(Function function, …)

    在QThreadPool::globalInstance()线程池内运行函数,并返回结果。

  • run函数的function参数传入函数指针或函数对象,后续的参数传入函数参数。

QtConcurrent::run()线程不可被QFuture获取进度、取消、暂停。

只能被用于查询runing / finished状态和函数返回值。

static int sum100(int a,int b){
   	int sum{};
   	for (int i=a;i<b;++i) {
   		sum+=i;
   	}
   	return sum;
   }
QFuture<int> fu=QtConcurrent::run(sum100,0,100);
   fu.waitForFinished();
   if(fu.isFinished()){
   	cc<<"fu is Finished";
   	cc<<fu.result();
   }

运行结果:

1559483245171

Map和Filter

​ Map和Filter分别用于映射和过滤,Map和Filter族函数用于将指定函数作用于一个序列容器,就地修改或返回一个新值。

函数 返回值 作用
map(Sequence &sequence, MapFunctor function) QFuture<void> 就地修改序列容器
map(Iterator begin, Iterator end, MapFunctor function)
mapped(const Sequence &sequence, MapFunctor function) 被修改后的容器 返回一个被修改后的容器,原容器不做改变
mapped(Iterator begin, Iterator end, MapFunctor function)
mappedReduced(const Sequence &sequence,
MapFunctor mapFunction,
ReduceFunctor reduceFunction,
QtConcurrent::ReduceOptions reduceOptions = ...)
QFuture<ResultType> 根据reduceFunction函数对整个序列容器进行运算,返回运算结果
mappedReduced(Iterator begin,
Iterator end,
MapFunctor mapFunction,
ReduceFunctor reduceFunction,
QtConcurrent::ReduceOptions reduceOptions = ...)
filter(Sequence &sequence, KeepFunctor filterFunction) QFuture<void> 就地过滤容器
filtered(const Sequence &sequence, KeepFunctor filterFunction) 被过滤后的容器 返回一个被过滤后的容器,原容器不做改变
filtered(Iterator begin, Iterator end, KeepFunctor filterFunction)
filteredReduced(const Sequence &sequence,
KeepFunctor filterFunction,
ReduceFunctor reduceFunction,
QtConcurrent::ReduceOptions reduceOptions = ...)
QFuture<ResultType> 根据reduceFunction函数对整个序列容器进行运算,返回运算结果
filteredReduced(Iterator begin,
Iterator end,
KeepFunctor filterFunction,
ReduceFunctor reduceFunction,
QtConcurrent::ReduceOptions reduceOptions = ...)

BloackMap和BloackFilter

​ 与map和filter类似,但是运行时会阻塞当前线程至运算结束。函数名数量与map和filter完全相同,只需在函数名前加上blocking并转化为驼峰命名法即可,例:

map–>blockingMap

QThread

​ QThread是线程的实体。每个QThread对象都代表了一个独立的线程。QThread继承自QObject。

例:

// 该段代码处于GUI构造函数中
QThread*thd=new QThread(this);	// 创建QThread对象
QObject *obj=new QObject; 		// 创建QObject对象
obj->moveToThread(thd);			//将QObject从当前线程移交到thd线程中
thd->start();					//打开线程

connect(thd,&QThread::destroyed,[](){
    qDebug()<<"thread destory";
});

thd->exit();					//关闭线程

运行结果为:

1559227682087

QThread属性

​ QThread的Priority属性保存了线程的优先级。在无优先级的操作系统中该属性不起作用,优先级共八级,除了最后一级外,等级越高,优先级越高:

| 常量 | 值 | 描述 | | :—————————: | :–: | :————————————: | | QThread::IdlePriority | 0 | 没有其它线程运行时才调度 | | QThread::LowestPriority | 1 | 比LowPriority调度频率低 | | QThread::LowPriority | 2 | 比NormalPriority调度频率低 | | QThread::NormalPriority | 3 | 操作系统的默认优先级 | | QThread::HighPriority | 4 | 比NormalPriority调度频繁 | | QThread::HighestPriority | 5 | 比HighPriority调度频繁 | | QThread::TimeCriticalPriority | 6 | 尽可能频繁的调度 | | QThread::InheritPriority | 7 | 使用和创建线程同样的优先级. 这是默认值 |

QThread成员函数

共有成员函数

1559231010491

共有槽

1559231059087

静态成员函数

1559231094871

保护函数

1559231133956

静态保护函数

1559231156980

QThread的使用

​ 上面的范例简单地演示了QThread的使用方法,但是QThread的典型使用时使用类继承,通过重写run()函数来实现定制化功能:

例:

QT之QThread(深入理解)

如何正确使用QThread