Qt中的多线程及其应用(1)

一、进程与线程的概念

什么是程序?

程序是计算机存储系统中的数据文件。分为源代码程序和可执行程序。源代码程序一般为文本文件,用来描述程序的行为和功能,可执行程序一般为二进制文件,可以直接加载并执行。

源代码程序经过编译器编译,就成为可执行程序。

什么是进程?

广义的概念认为是程序关于某个数据集合的一次运行活动,狭义地讲,就是程序被加载到内存中后,执行得到的进程。

程序和进程的区别:

程序是硬盘中的静态文件,是存储系统中的一段二进制表示。

进程是内存中动态的运行实体,如数据段、代码段、PC指针等。

程序和进程的联系:

一个程序可以多次运行,每次运行都会产生一个进程。

一个进程也可能包含很多个程序,比如Qt工程的release版本,依赖很多其他的动态库。

但是,在当代操作系统中,资源分配的基本单位是进程,而CPU进行调度执行的基本单位是线程

什么是线程?

线程是进程内的一个执行单元;

是操作系统中一个可调度的实体;

是进程中相对独立的一个控制流序列;

是执行时的线程数据和其他调度所需的信息。

C/C++程序被执行后从main()函数开始运行,中间经历了什么样的历程?

可执行程序加载执行后,第一步系统分配资源(内存、IO等),然后将PC指针指向main()函数入口地址,然后从PC指针包含的地址处开始执行。——第一个线程开始。

线程是进程使用CPU资源的基本单位。

进程与线程的关系:

进程中可以存在多个线程来共享进程资源;(进程(资源分配)中的多个线程(调度执行)并行执行,共享进程资源)

线程是被调度的执行单元,而进程不是;

线程不能脱离进程单独存在,只能依赖于进程运行;

线程有生命期,有诞生和死亡;

任意线程都可以创建其他新的线程。



二、Qt中的多线程编程

Qt中通过QThread类来支持多线程,它是一个跨平台的多线程解决方案,以简洁易用的方式实现多线程编程。

Qt中,线程以对象的形式被创建和使用,每个线程对应着一个QThread对象。

QTread中的关键成员函数:

1、void run()函数:线程体函数,用于定义线程功能(执行流)

2、void start()函数:启动函数,将线程入口地址设置为run()函数

3、void terminate()函数:强制结束线程(工程中不推荐

编程示例:创造两个线程,分别实现输出1~5的功能;

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
class MyThread : public QThread
{
protected:
    void run()
    {
        for(int i=0; i<5; i++)
        {
            qDebug() << objectName() << " : " << i;
            sleep(1);
        }
    }
};
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    MyThread t;
    t.setObjectName("t");
    t.start();

    MyThread tt;
    tt.setObjectName("tt");
    tt.start();

    for(int i=0; i<10000; i++)
    {
        for(int j=0; j<10000; j++)
        {
        }
    }
    return a.exec();
}

从上述代码可以看出,线程由QThread类 创建对象,通过start()函数开始运行,run()函数执行具体功能,线程结束后自动死亡。

也可以通过调用terminate()函数直接对线程进行强制结束。

但是,强制结束不能保证数据完整性,比如内存收回等功能可能就会因为线程提前结束而无法执行,造成内存泄漏。所以在工程中terminate()函数是禁止使用的,因为其是使操作系统暴力终止线程。

所以如何能安全地在需要的时候终止线程呢?

run()函数执行结束是安全地终止线程的唯一方法,所以我们需要在线程类中增加标志变量(volatile bool),通过标识变量的值,判断是否需要从run()函数中返回。

示例如下:在自定义类中定义volatile bool类型的变量 m_toStop,然后在构造函数中对其初始化为false,定义成员函数stop()来讲变量的值设置为true,在run()函数运行时,将m_toStop的值作为函数执行的条件之一,通过调用stop()函数,可以安全地结束run()函数。

class Sample : public QThread
{
protected:
    volatile bool m_toStop;

    void run()
    {
        int* p = new int[10000];

        for(int i=0; !m_toStop && (i<10); i++)
        {
            qDebug() << objectName() << " : " << i;

            p[i] = i * i * i;

            msleep(500);
        }

        delete[] p;  //内存泄漏

        qDebug() << objectName() << " : end";
    }

public:
    Sample()
    {
        m_toStop = false;
    }

    void stop()
    {
        m_toStop = true;
    }
};

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

    qDebug() << "main begin";

    Sample t;

    t.setObjectName("t");

    t.start();

    for(int i=0; i<10000; i++)
    {
        for(int j=0; j<10000; j++)
        {

        }
    }

    t.stop();  //执行后无内存泄漏

    //t.terminate();  //会在线程未结束时,杀死线程, 内存泄漏

    qDebug() << "main end";

    return a.exec();
}

要点:start()启动run()函数,run()函数实现线程执行体。

三、多线程之间的同步

多线程编程的本质是什么?   —— 并发性

在宏观上,所有线程并发执行,多个线程间相互独立,互不干涉。


线程间的运行是相互独立的,但是线程间总是完全独立毫无依赖吗?——并不是。

在某些情况下,多线程的执行需要时序上的相互支持。

举个例子:煮饭、做菜、吃饭在执行时都是相互独立的,但是必须是煮饭和做菜两个线程结束后,吃饭这个线程才可以执行。

同步的概念:在特殊情况下,需要控制多线程间的相对执行顺序。

QThread类直接内置了多线程间的同步函数:

bool QThread::wait(unsigned long time = ULONG_MAX)

qDebug() << "begin";

QThread t;

t.start();
t.wait();

qDebug() << "end";

编程示例如下:

/*
    sum(n) => 1 +2 +3 +4 +... +n;

    sum(1000) => ?;

    [1, 1000] = [1, 300]、 [301, 600]、 [601, 1000]
*/

class Calculator : public  QThread//QObject
{
  protected:
    int m_begin;
    int m_end;
    int m_result;

    void run()
    {
        qDebug() << objectName() << ": run() begin";

        for(int i=m_begin; i<=m_end; i++)
        {
            m_result += i;

            msleep(10);
        }

        qDebug() << objectName() << ": run() end";
    }
public:
    Calculator(int begin, int end)
    {
        m_begin = begin;
        m_end = end;
        m_result = 0;
    }

    void work()
    {
        run();
    }

    int result()
    {
        return m_result;
    }
};

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

    qDebug() << "main begin : ";

    Calculator call1(1, 300);
    Calculator call2(301, 600);
    Calculator call3(601, 1000);

    call1.setObjectName("call1");
    call2.setObjectName("call2");
    call3.setObjectName("call3");

    call1.start();
    call2.start();
    call3.start();

    call1.wait(); //不添加wait()函数直接执行会导致result等于0
    call2.wait();
    call3.wait();

    //call1.work();  
    //call2.work();
    //call3.work();

    int result = call1.result() + call2.result() + call3.result();

    qDebug() << " result : " << result;

    qDebug() << "main end .";

    return a.exec();
}

四、多线程间的互斥

多线程除了在时序上可能存在依赖,在其他方面是否也可能产生依赖呢?

生产—消费者问题:

有n个生产者同时制造产品,并把产品存入仓库中,有m个消费者同时需要从仓库中取出产品,规则:当仓库未满时,任意生产者可以存入产品,当仓库未空时,任意消费者可以取出产品。

存入和取出不得冲突!

同样的例子,洗手间的使用和清扫,当保洁员清洗时,洗手间必须暂停使用。

也就是说仓库、洗手间是一个被“限制”的资源,每次只允许一个线程进行访问(读、写),我们称之为临界资源(Critical Resource)

线程间的互斥:多个线程在同一时间都需要访问邻接资源。

Qt中,用QMutex类来保证线程间的互斥,它是一把线程锁,利用线程锁可以保证临界资源的安全性。

QMutex类中的关键成员函数:

void lock()函数:当调用这个函数时,获取锁并执行,获取之后,阻塞并等待锁释放;

void unlock()函数:释放锁。(通一把锁的获取和释放必须在同一线程中成对的出现)

注意:如果mutex在调用unlock()时处于空闲状态,那么程序的行为是未定义的。

解决生产消费者问题:

创建两个类,分别表示生产者和消费者的存入和取出行为,定义仓库对象QString类,用来表示仓库中的产品。

程序实例如下:

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QMutex>

static QString g_store;
static QMutex g_mutex;

class Producer : public QThread
{
protected:
    void run()
    {
        int count = 0;

        while (true)
        {
            g_mutex.lock();

            g_store.append(QString::number((count++) % 10));  //将随机数字(0-9)存入仓库

            qDebug() << objectName() << " : " + g_store;

            g_mutex.unlock();

            msleep(1);
        }
    }
};

class Customer : public QThread
{
protected:
    void run()
    {
        while (true)
        {
            g_mutex.lock();

            if( g_store != "")
            {
                g_store.remove(0, 1);

                qDebug() << objectName() << " : " + g_store;
            }

            g_mutex.unlock();

            msleep(1);
        }

    }
};

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

    Producer p;
    Customer c;

    p.setObjectName("Producer");
    c.setObjectName("Customer");

    p.start();
    c.start();

    return a.exec();
}

程序运行后,对仓库的访问(g_store)只能是单线程,无论是生产者还是消费者,在进行具体的操作前,都会对线程上锁,保证不会出现冲突现象。

程序有多少临界资源?需要多少线程锁?

一般性原则:每一个临界资源都需要一个线程所保护。

但是,下面的 示例哪个是正确的呢?


编程发现,当两个线程按照上述方式对线程上锁之后,程序无法执行,也无法解锁。这就是线程中的死锁现象。

死锁:线程间相互等待临界资源而造成彼此无法继续执行。

什么时候会发生死锁:

系统中存在多个临界资源且临界资源不可抢占,线程需要多个临界资源才能继续执行。

如何避免线程间的死锁呢?

对所有的临界资源都分配一个唯一的序号,对应的线程也分配同样的序号,系统中每个线程按照严格递增的次序请求资源。

修改代码如下:

class THreadA : public QThread
{
  protected:
    void run()
    {
        while ( true )
        {
            m1.lock();
            m2.lock();
            m2.unlock();
            m1.unlock();

            sleep(1);
        }

    }
};

class THreadB : public QThread
{
  protected:
    void run()
    {
        while ( true )
        {
            m1.lock();
            m2.lock();

            m2.unlock();
            m1.unlock();

            sleep(1);
        }

    }
};

信号量的概念:

信号量是特殊的线程锁,它允许N个线程同时访问临界资源,Qt中以QSemaphore类的方式支持信号量。

示例如下:


注意:QSemaphore对象中维护了 一个整形值,acquire()使得该值减一,release()使得该值加一,当该值为零时,acquire()函数将阻塞当前线程。

再论生产消费者问题:

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QSemaphore>

const int SIZE = 5;
unsigned char g_buff[SIZE] = {0};
QSemaphore g_sem_free(SIZE);  //提高并发性,标识当前五个仓库都是空闲的
QSemaphore g_sem_used(0);     //标识多少个已经被使用

class Producer : public QThread
{
  protected:
    void run()
    {
        while ( true)
        {
            int value = qrand() % 256;

            g_sem_free.acquire();

            for(int i=0; i<SIZE; i++)
            {
                if( !g_buff[i] )
                {
                    g_buff[i] = value;

                    msleep(1000);

                    qDebug() << objectName() << " generate : {" << i << ", " << value << "} ";

                    msleep(1000);

                    break;
                }
            }

            g_sem_used.release(); //货物已经放进去了

            sleep(2);
        }
    }
};

class Customer : public QThread
{
  protected:
    void run()
    {
        while ( true)
        {
            g_sem_used.acquire();

            for(int i=0; i<SIZE; i++)
            {
               if( g_buff[i])
               {
                   int value = g_buff[i];

                   g_buff[i] = 0;

                   msleep(500);

                   qDebug() << objectName() << " consume : {" << i << ", " << value << "} ";

                   msleep(500);

                   break;
               }
            }

            g_sem_free.release();

            sleep(1);
        }
    }
};

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

    Producer p1;
    Producer p2;
    Producer p3;

    p1.setObjectName("Producer1");
    p2.setObjectName("Producer2");
    p3.setObjectName("Producer3");

    Customer c1;
    Customer c2;

    c1.setObjectName("Customer1");
    c2.setObjectName("Customer2");

    p1.start();
    p2.start();
    p3.start();
    c1.start();
    c2.start();

    return a.exec();
}

通过这种方式,可以提高并发性,使得三个生产者、两个消费者都可以对仓库进行操作。

重点:信号量允许N个线程同时访问临界资源。


猜你喜欢

转载自blog.csdn.net/qq_28388835/article/details/79763476