文章目录
- 线程并发
- 线程同步
- 原子访问(InterLocked)
- 关键段(Critical_Section,也叫临界区)
- 回顾单例出现的问题
- 关键段基本使用
- 封装关键段
- Qt下的多线程
- 多线程与进度条
- Qt-QThread
线程并发
我们再创建一个控制台文件命名为main2.cpp,然后在这个文件中创建三条线程,在这三条中同时为一个全局变量进行递增操作,并在最后输出这个全局变量。
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
//线程句柄
HANDLE handle1 = ::CreateThread(nullptr/*默认的安全属性*/,
0/*windows 默认1M*/,
&ThreadProc/*线程函数的地址*/,
nullptr/*线程函数的参数*/,
0/*0:立即运行 ,SUS_PEND 挂起*/,
nullptr/*线程ID*/);
HANDLE handle2 = ::CreateThread(nullptr/*默认的安全属性*/,
0/*windows 默认1M*/,
&ThreadProc/*线程函数的地址*/,
nullptr/*线程函数的参数*/,
0/*0:立即运行 ,SUS_PEND 挂起*/,
nullptr/*线程ID*/);
HANDLE handle3 = ::CreateThread(nullptr/*默认的安全属性*/,
0/*windows 默认1M*/,
&ThreadProc/*线程函数的地址*/,
nullptr/*线程函数的参数*/,
0/*0:立即运行 ,SUS_PEND 挂起*/,
nullptr/*线程ID*/);
if(WAIT_OBJECT_0 == WaitForSingleObject(handle1,INFINITE)){
if(handle1){
::CloseHandle(handle1);
handle1 = nullptr;
}
}
if(WAIT_OBJECT_0 == WaitForSingleObject(handle2,INFINITE)){
if(handle2){
::CloseHandle(handle2);
handle2 = nullptr;
}
}
if(WAIT_OBJECT_0 == WaitForSingleObject(handle3,INFINITE)){
if(handle3){
::CloseHandle(handle3);
handle3 = nullptr;
}
}
qDebug()<<"N============== "<<N;
return a.exec();
}
线程函数以及全局变量
int N = 0;
//线程函数
DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){
for(int i=0;i<100;i++){
N++;
qDebug()<<"N= "<<N;
Sleep(50);
}
return 0;
}
我们经过多次测试发现,最终结果并不一定是我们预想的300
这里出现的就是并发问题:
多个线程同时操作同一个资源(内存空间、文件句柄、网络句柄),可能会导致的结果不一致的问题。发生的前提条件一定是多个线程下共享资源。
那么既然出现了问题,我们也会有相应的解决办法,就是接下来要说的线程同步
线程同步
线程同步,就是通过协调线程执行的顺序,避免多个线程同时操作同一个资源导致并发问题,使结果多次执行结果一致。
常见的线程同步方式:原子访问、关键段、事件、互斥量、条件变量、信号量。
上面提到的并发问题,解决方法有很多,重点学习 锁。
原子访问(InterLocked)
同一时刻,只允许一个线程访问一个变量。注意:他只是对一个变量保持原子自增、自减操作,对于一个代码段来说并不适用。
DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){
for(int i=0;i<100;i++){
//N++;
::InterlockedIncrement((long*)&N); //源自方式 递增 ++
//N--;
//::InterlockedDecrement((long*)&N);
qDebug()<<"N= "<<N;
Sleep(50);
}
return 0;
}
这样我们在每次运行都会得到理想的值了
关键段(Critical_Section,也叫临界区)
回顾单例出现的问题
我们还记得在学习 设计模式中的单例模式时,提到过一个问题,那就是在多线程下,可能会创建出来多个对象
我们试着把这个问题显现出来
单例:
class CSingleton {
CSingleton(): m_a(0){}
CSingleton(const CSingleton&) = delete; //弃用 拷贝构造函数
~CSingleton(){}
static CSingleton* m_psin;
static class Destory {
public:
~Destory() {
if (m_psin) {
delete m_psin;
}
m_psin = nullptr;
}
} m_destory;
public:
//问题:在多线程下 可能会创建出来多个对象
static CSingleton* GetSingleton() {
//1.加锁
if (!m_psin)
m_psin = new CSingleton;
//2.解锁
return m_psin;
}
static void DestorySingleton(CSingleton*& psin) {
if (m_psin) {
delete m_psin;
}
psin = m_psin = nullptr;
}
int m_a;
};
CSingleton* CSingleton::m_psin = nullptr;
CSingleton::Destory CSingleton::m_destory;//类外定义
主函数
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
for(int i=0;i<30;i++){
CreateThread(nullptr/*默认的安全属性*/,
0/*windows 默认1M*/,
&ThreadProc/*线程函数的地址*/,
nullptr/*线程函数的参数*/,
0/*0:立即运行 ,SUS_PEND 挂起*/,
nullptr/*线程ID*/);
}
return a.exec();
}
线程函数
DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){
Sleep(100);
CSingleton* pSin = CSingleton::GetSingleton();
qDebug()<<pSin;
return 0;
}
运行后通过查看对象地址可以发现,真的可能会创建出多个对象
那么这个问题就不能用原子访问来解决了,我们需要使用到关键段
关键段基本使用
结构体:CRITICAL_SECTION和四个函数(初始化、进入、离开、删除)
我们可以使用这个来解决单例中出现的问题
CRITICAL_SECTION m_cs; //关键段的变量
static CSingleton* GetSingleton() {
//1.加锁
if (!m_psin){
::EnterCriticalSection(&m_cs);
if (!m_psin)
m_psin = new CSingleton;
//2.解锁
::LeaveCriticalSection(&m_cs);
}
return m_psin;
}
主函数中初始化和删除,注意删除之前最好要加个延迟
::InitializeCriticalSection(&m_cs); //初始化关键段
......
Sleep(3000);
::DeleteCriticalSection(&m_cs); //删除关键段
这样在运行过程中就只会创建一个对象了
运行原理:
封装关键段
把创建对象和四个结构体都封装到一个类中
class MyLock{
CRITICAL_SECTION m_cs; //关键段的变量
public:
MyLock(){
::InitializeCriticalSection(&m_cs); //初始化关键段
}
~MyLock(){
::DeleteCriticalSection(&m_cs); //删除关键段
}
void Lock(){
::EnterCriticalSection(&m_cs);
}
void UnLock(){
::LeaveCriticalSection(&m_cs);
}
};
使用的时候直接创建类对象调用函数即可
static CSingleton* GetSingleton() {
static MyLock lock;
//1.加锁
if (!m_psin){
lock.Lock();
if (!m_psin)
m_psin = new CSingleton;
//2.解锁
lock.UnLock();
}
return m_psin;
}
Qt下的多线程
多线程与进度条
我们创建一个带有设计界面的项目,并且在设计界面中放入一个进度条以及四个按钮组件,组件的作用分别是启动、暂停、恢复和退出
并将这些按钮直接转到clicked的槽函数
private slots:
void on_pb_start_clicked();
void on_pb_pause_clicked();
void on_pb_resume_clicked();
void on_pb_quit_clicked();
void MainWindow::on_pb_start_clicked()
{
}
void MainWindow::on_pb_pause_clicked()
{
}
void MainWindow::on_pb_resume_clicked()
{
}
void MainWindow::on_pb_quit_clicked()
{
}
在启动的槽函数中实现创建线程
void MainWindow::on_pb_start_clicked()
{
if(!m_handle){
m_isQuit = false;
m_handle = CreateThread(nullptr/*默认的安全属性*/,
0/*windows 默认1M*/,
&ThreadProc/*线程函数的地址*/,
(void*)this/*线程函数的参数*/,
0/*0:立即运行 ,SUS_PEND 挂起*/,
nullptr/*线程ID*/);
}
}
线程函数,在线程函数中实现设置进度条的值
DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){
MainWindow* pMain = (MainWindow*)lpThreadParameter;
int n = 0;
while(!pMain->m_isQuit){
pMain->GetUI()->progressBar->setValue(n); //设置进度条的值
n = ++n%101;
Sleep(200);
}
return 0;
}
在头文件中设置公有的ui接口以及退出标志和线程句柄
public:
Ui::MainWindow * GetUI(){return ui;}
bool m_isQuit; //是否退出
HANDLE m_handle;
在构造函数中初始化
到这里我们先点击启动按钮进行测试,发现程序 会崩溃
报错原因是:在另一个线程中设置了当前组件的值时,使用sendEvent发送事件,跨线程报错。
针对于此情况解决方法就是手动设置信号与槽进行连接
所以我们先创建一个自定义槽函数和信号
private slots:
void slots_setValue(int);
signals:
void signals_sendValue(int);
这样我们就在线程函数中去发射信号,然后在槽函数中接收信号并且设置进度条的值
槽函数
void MainWindow::slots_setValue(int n){
ui->progressBar->setValue(n); //设置进度条的值
}
之后对信号和槽进行连接
connect(this,SIGNAL(signals_sendValue(int)),this,SLOT(slots_setValue(int)),Qt::QueuedConnection);
注意这个函数有有一个第五个参数,用来设置连接类型,我们之前报错只要就是因为默认的连接类型不符合,我们可以选择AutoConnection或QueuedConnection。
这样程序就可以正常运行了
然后我们再来实现以下退出槽函数
void MainWindow::on_pb_quit_clicked()
{
m_isQuit = true; //通知线程退出
if( WAIT_OBJECT_0 == WaitForSingleObject(m_handle,3000)){
qDebug()<<"子线程已经退出";
if(m_handle){
CloseHandle(m_handle);
m_handle = nullptr;
}
}
}
暂停和恢复就是将线程挂起和连接
暂停
void MainWindow::on_pb_pause_clicked()
{
::SuspendThread(m_handle);
qDebug()<<"子线程已经挂起";
}
恢复
void MainWindow::on_pb_resume_clicked()
{
::ResumeThread(m_handle);
qDebug()<<"子线程已经恢复";
}
通过测试都可以正常运行
Qt-QThread
在Qt中封装了创建线程的类:QThread,一般情况下,手动添加一个类继承QThread,这样既能继承QThread的功能,又能扩展自己的功能。
添加新文件
Qt线程+关键段:为何要使用关键段,原来的只是靠一个暂停标志位,while一直在空跑,也会耗费一点cpu。好一点的效果是让其等待,直到恢复。
所以我们就会创建一个新的线程用来阻塞当前线程
头文件
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <windows.h>
class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = 0);
~MyThread();
signals:
void signals_sendThreadValue(int); //声明信号
public slots:
public:
virtual void run();
bool m_isQuit; //标识当前线程,是否退出
bool m_isPause; //是否暂停
CRITICAL_SECTION m_cs;
};
#endif // MYTHREAD_H
源文件
#include "mythread.h"
#include <QDebug>
MyThread::MyThread(QObject *parent) : QThread(parent),m_isQuit(false),m_isPause(false)
{
::InitializeCriticalSection(&m_cs);
}
MyThread::~MyThread(){
::DeleteCriticalSection(&m_cs);
}
void MyThread::run(){
int n = 0;
while(!m_isQuit){
if(m_isPause){
// qDebug()<<"暂停了";
// continue;
qDebug()<<"暂停了";
::EnterCriticalSection(&m_cs);
qDebug()<<"进入关键段";
::LeaveCriticalSection(&m_cs);
qDebug()<<"离开关键段";
}
//发射一个信号
emit signals_sendThreadValue(n);
n = ++n%101;
Sleep(100);
}
qDebug()<<"子线程即将退出";
}
信号和槽连接以及实现线程功能
connect(&this->m_thread,SIGNAL(signals_sendThreadValue(int)),this,SLOT(slots_setValue(int)),Qt::QueuedConnection);
启动线程
void MainWindow::on_pb_start_clicked()
{
m_thread.m_isQuit = false;
m_thread.start(); //启动线程
}
注意:启动线程需要调用系统函数start()而不是直接调用run()。
暂停线程
void MainWindow::on_pb_pause_clicked()
{
::EnterCriticalSection(&m_thread.m_cs);
m_thread.m_isPause = true;
}
恢复线程
void MainWindow::on_pb_resume_clicked()
{
m_thread.m_isPause = false;
::LeaveCriticalSection(&m_thread.m_cs);
}
退出线程
void MainWindow::on_pb_quit_clicked()
{
m_thread.m_isQuit = true;
}
运行测试后也没有什么毛病
到此,线程相关的知识就结束了