一:线程的两种实现方式
方法一:
- 创建一个类从QThread类派生
- 在子线程类中重写 run 函数, 将处理操作写入该函数中
- 在主线程中创建子线程对象, 启动子线程, 调用start()函数
方法二:
- 将业务处理抽象成一个业务类, 在该类中创建一个业务处理函数
- 在主线程中创建一QThread类对象
- 在主线程中创建一个业务类对象
- 将业务类对象移动到子线程中
- 在主线程中启动子线程
- 通过信号槽的方式, 执行业务类中的业务处理函数
实现方式如下:
inheritqthread.cpp与inheritqthread.h
#ifndef INHERITQTHREAD_H
#define INHERITQTHREAD_H
#include <QObject>
#include<QThread>
#include<QDebug>
class InheritQThread : public QThread
{
Q_OBJECT
public:
InheritQThread();
protected:
void run() override;
};
#endif // INHERITQTHREAD_H
#include "inheritqthread.h"
int Add=3;
InheritQThread::InheritQThread()
{
}
void InheritQThread::run()
{
for(int i=0;i<3;++i)
{
++Add;
qDebug()<<" i =" << Add <<"当前线程id:"<<currentThreadId();
}
}
objmovetothread.cpp与objmovetothread.h
#include<QDebug>
#include<QThread>
class ObjMoveToThread : public QObject
{
Q_OBJECT
public:
explicit ObjMoveToThread(QObject *parent = nullptr);
public slots:
void DoWork();
};
#endif // OBJMOVETOTHREAD_H
#include "objmovetothread.h"
extern int Add;
ObjMoveToThread::ObjMoveToThread(QObject *parent) : QObject(parent)
{
}
void ObjMoveToThread::DoWork()
{
for(int j=10 ;j<13;++j)
{
Add*=2;
qDebug() <<"j = " <<Add <<"当前线程id:" <<QThread::currentThreadId();
}
}
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<inheritqthread.h>
#include<objmovetothread.h>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void initThread();
signals:
void SignalMyObjMoveToThreadDoWork();
private:
Ui::MainWindow *ui;
QThread *MyThread;
ObjMoveToThread *MyObjMoveToThread;
InheritQThread *MyInheritQThread;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
initThread();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::initThread()
{
MyThread=new QThread();
MyObjMoveToThread =new ObjMoveToThread();
MyInheritQThread =new InheritQThread();
MyObjMoveToThread->moveToThread(MyThread);
connect(MyThread,&QThread::finished,MyThread,&QObject::deleteLater);
connect(MyInheritQThread,&QThread::finished,MyInheritQThread,&QObject::deleteLater);
connect(this,&MainWindow::SignalMyObjMoveToThreadDoWork,MyObjMoveToThread,&ObjMoveToThread::DoWork);
MyThread->start();
emit SignalMyObjMoveToThreadDoWork();
MyInheritQThread->start();
qDebug() <<"当前线程id:" <<QThread::currentThreadId();;
}
运行结果:
```cpp
当前线程id: 0x1b1c
i = 4 当前线程id: 0x19fc
i = 5 当前线程id: 0x19fc
i = 6 当前线程id: 0x19fc
j = 12 当前线程id: 0x1544
j = 24 当前线程id: 0x1544
j = 48 当前线程id: 0x1544
当前线程id: 0x1b68
j = 6 当前线程id: 0x1a18
i = 7 当前线程id: 0xd38
j = 14 当前线程id: 0x1a18
i = 15 当前线程id: 0xd38
j = 30 当前线程id: 0x1a18
i = 31 当前线程id: 0xd38
- 线程的执行顺序无法保证,它与操作系统的调度策略和线程
- 优先级等因素有关;
- 线程的切换可能发生在任何时刻任何地点;
- 线程对代码的敏感高,代码的细微修改可能产生意想不到的结果。
因此,为了有效使用多线程,我们必须对其进行控制。
二、线程间的同步
线程之间的配合被统称为线程间同步。一个应用程序的各个线程常常需要协同工作。一方面,当有多个线程争用同一个系统资源时,应该确保各线程“分时”使用该资源,而不是无序地争用。另一方面,各个线程之间也可能需要直接的通信,
- QMutex 与 QMutexLocker
系统中的一些资源只允许一个线程使用,不允许多个线程同时使用。
类QMutex的一个对象被称为一个互斥体。一个互斥体有未锁定(unlocked)和锁定(locked)两种状态,初始状态为未锁定。当某个线程A首次调用QMutex的lock函数时,该互斥体变为锁定状态,函数立即返回,该线程继续运行。此后,其他某个线程B调用lock函数时,由于该互斥体已经是锁定状态,线程B无法获得该互斥体,因而被暂停运行,等待线程A调用QMutex的unlock函数解除对该互斥体的锁定。线程B将被设置为阻塞状态(blocked)
objmovetothread.cpp与inheritqthread.cpp改为
#include "inheritqthread.h"
int Add=3;
QMutex mutex;
InheritQThread::InheritQThread()
{
}
void InheritQThread::run()
{
mutex.lock();
for(int i=0;i<3;++i)
{
++Add;
qDebug()<<" i =" << Add <<"当前线程id:"<<currentThreadId();
}
mutex.unlock();
}
#include "objmovetothread.h"
extern int Add;
extern QMutex mutex;
ObjMoveToThread::ObjMoveToThread(QObject *parent) : QObject(parent)
{
}
void ObjMoveToThread::DoWork()
{
mutex.lock();
for(int j=10 ;j<13;++j)
{
Add*=2;
qDebug() <<"j = " <<Add <<"当前线程id:" <<QThread::currentThreadId();
}
mutex.unlock();
}
为了便于使用QMutex,Qt定义了类QMutexLocker。
该类的构造函数接收一个互斥体,并在构造函数中调用该互斥体的lock函数。该类的析构函数调用互斥体的unlock函数。在程序中简单地定义一个QMutexLocker对象,即可申请锁定该互斥体。当程序运行到这个对象的作用域之外时,该对象的析构函数被调用,即可解除对互斥体的锁定。例如:
#include "inheritqthread.h"
#include<QMutexLocker>
int Add=3;
QMutex mutex;
oid InheritQThread::run()
{
QMutexLocker locker(&mutex);
for(int i=0;i<3;++i)
{
++Add;
qDebug()<<" i =" << Add <<"当前线程id:"<<currentThreadId();
}
}
- QSemaphore
同步:线程间的协作
互斥:线程间的竞争
互斥体对象只能管理一个对象,当管理多个具有相同性质的资源时,需要使用信号量。
我们以循环缓冲区(circular buffer)为例来解释这个概念。设有一个能够存放BufferSize个元素的缓冲区。有两个线程使用这个缓冲区。线程Producer能够生成一些数据并把这些数据存放在该缓冲区中。而线程Consumer读取该缓冲区中的数据并做一些其他的处理,比如将数据输出到屏幕上。Producer所产生数据的长度通常大于缓冲区的长度,意味着在Producer线程写满整个缓冲区后,需要从缓冲区的首部开始写入新的数据,但是这要求Condumer线程此时已经读取了缓冲区首部的那些元素。是否能够满足这个条件取决于Producer产生数据的速度和Consumer读取数据的速度。如果前者快,则整个缓冲区很快会被写满,没有空闲的空间来写入新的数据。Producer线程应该等待Consumer线程读取一些数据。如果Consumer读取数据的速度大于Producer的写入速度,则缓冲区中的数据会很快被读完,没有数据可供读取,Consumer线程会等待Producer线程写入新的数据。
具体实现代码如下:
>#include <QtCore>
#include <stdio.h>
#include <stdlib.h>
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);//生产者初始拥有4096B数据缓存区
QSemaphore usedBytes(0);//消费者初始使用了0字节数据
class Producer : public QThread//生产者
{
public:
void run();
};
void Producer::run()
{
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = (i % BufferSize);
usedBytes.release();
}
}
class Consumer : public QThread//消费者
{
public:
void run();
};
void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
if(i % 15 ==0 && i!=0)
fprintf(stderr, "\n");
freeBytes.release();
}
fprintf(stderr, "\n");
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
生产者线程首先获取一个空闲单元,如果此时缓冲区被消费者尚未读取的数据填满,对freeBytes.acquire()函数的调用就会阻塞,直到消费者读取了这些数据为止。一旦生产者获取了某个空闲单元,就用当前的缓冲区单元序号填写这个缓冲区单元,然后调用usedBytes.release()把可用资源加1,表示消费者此时可以读取这个刚刚被填写的单元了。
QT文档说明:
void QSemaphore::acquire(int n = 1)
Tries to acquire n resources guarded by the semaphore. If n > available(), this call will block until enough resources are available.
void QSemaphore::release(int n = 1)
Releases n resources guarded by the semaphore.
- 使用QWaitConditon
条件变量的使用:
: 定义条件变量
: 初始化
: 如果条件不满足则休眠
: 若条件满足则唤醒休眠线程
: 销毁
对线程的执行要求:
一个生产者与两个消费者,在生产者中,首先检查缓冲区是否已填充满,如果是则使用bufferEmpty.wait(&mutex);等待“缓冲区有空位”(bufferEmpty变量)条件。若缓冲区未填满,则争夺缓存区,并写入数据。
在消费者中,若缓冲区没有足够的数据可读,则等待。
bool QWaitCondition::wait ( QMutex *mutex, unsigned long time = ULONG_MAX )。这个函数将互斥量解锁并在此等待,它带有两个参数:第一个参数为一个锁定的互斥量,第二个参数为等待时间。如果作为第一个参数的互斥量在调用时不是锁定的或出现递归锁定的情况,wait()函数将立刻返回。调用wait()操作的线程使得作为参数的互斥量在调用前变为解锁定状态,然后自身被阻塞变为等待状态直到满足以下条件之一:
● 其他线程调用了wakeOne()或者wakeAll()函数,这种情况下将返回“true”值;
● 第二个参数time超时(以毫秒记),该参数默认情况下为ULONG_MAX,表示永不超时,这种情况下将返回“false”值。
wait()函数返回前会将互斥量参数重新置为锁定状态,从而保证从锁定状态到等待状态的原子性转换。
主要代码如下
int DataSize=100;
int BufferSize=81;
int buffer[81]={0};
QWaitCondition bufferEmpty;
QWaitCondition bufferFull;
QMutex mutex;
int numUesdBytes = 0;
int rIndex = 0;
void Consumer::run()
{
forever{
mutex.lock();
//if(numUesdBytes <=0) 若不等待 buffer[rIndex]中有足够的数据再去读取,生产者与消费者会因为一直争夺资源造成生产者无法正常写入数据
if(numUesdBytes <= BufferSize/2)
bufferFull.wait(&mutex);
qDebug() << "consumer Thread Id =" <<currentThread() <<"rIndex=" <<rIndex <<"buffer=" << buffer[rIndex];
// printf("%ul :: [%d]=%d \n",currentThread(),rIndex,buffer[rIndex]);
rIndex =(++rIndex)%BufferSize;
--numUesdBytes;
bufferEmpty.wakeAll();
mutex.unlock();
}
// printf("\n");
}
void Producer::run()
{
for(int i=0 ; i<DataSize ; ++i)
{
mutex.lock();
if(numUesdBytes >= BufferSize)
bufferEmpty.wait(&mutex);
buffer[i%BufferSize] = numUesdBytes;
qDebug() << "producer currentId =" << currentThreadId()<< "buffer[i%BufferSize] =="<<buffer[i%BufferSize] <<"numUesdBytes =" <<numUesdBytes;
++numUesdBytes;
bufferFull.wakeAll();
mutex.unlock();
}
}
void MainWindow::init()
{
Producer produce;
Consumer consumer1;
Consumer consumer2;
produce.start();
consumer1.start();
consumer2.start();
produce.wait();
consumer1.wait();
consumer2.wait();
}
输出:
producer currentId = 0x170c buffer[i%BufferSize] == 0 numUesdBytes = 0
producer currentId = 0x170c buffer[i%BufferSize] == 1 numUesdBytes = 1
producer currentId = 0x170c buffer[i%BufferSize] == 2 numUesdBytes = 2
consumer Thread Id = Consumer(0x28fdb0) rIndex= 0 buffer= 0
consumer Thread Id = Consumer(0x28fda8) rIndex= 1 buffer= 1
producer currentId = 0x170c buffer[i%BufferSize] == 1 numUesdBytes = 1
producer currentId = 0x170c buffer[i%BufferSize] == 2 numUesdBytes = 2
producer currentId = 0x170c buffer[i%BufferSize] == 3 numUesdBytes = 3
consumer Thread Id = Consumer(0x28fdb0) rIndex= 2 buffer= 2
consumer Thread Id = Consumer(0x28fda8) rIndex= 3 buffer= 1
producer currentId = 0x170c buffer[i%BufferSize] == 2 numUesdBytes = 2
producer currentId = 0x170c buffer[i%BufferSize] == 3 numUesdBytes = 3
producer currentId = 0x170c buffer[i%BufferSize] == 4 numUesdBytes = 4
consumer Thread Id = Consumer(0x28fdb0) rIndex= 4 buffer= 2
consumer Thread Id = Consumer(0x28fda8) rIndex= 5 buffer= 3
producer currentId = 0x170c buffer[i%BufferSize] == 3 numUesdBytes = 3
producer currentId = 0x170c buffer[i%BufferSize] == 4 numUesdBytes = 4
producer currentId = 0x170c buffer[i%BufferSize] == 5 numUesdBytes = 5
consumer Thread Id = Consumer(0x28fdb0) rIndex= 6 buffer= 2
consumer Thread Id = Consumer(0x28fda8) rIndex= 7 buffer= 3