一、进程与线程的概念
什么是程序?
程序是计算机存储系统中的数据文件。分为源代码程序和可执行程序。源代码程序一般为文本文件,用来描述程序的行为和功能,可执行程序一般为二进制文件,可以直接加载并执行。
源代码程序经过编译器编译,就成为可执行程序。
什么是进程?
广义的概念认为是程序关于某个数据集合的一次运行活动,狭义地讲,就是程序被加载到内存中后,执行得到的进程。
程序和进程的区别:
程序是硬盘中的静态文件,是存储系统中的一段二进制表示。
进程是内存中动态的运行实体,如数据段、代码段、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个线程同时访问临界资源。