多进程和多线程的概念
一、多线程是什么?
说起多线程,那么就不得不说什么是线程,而说起线程,又不得不说什么是进程。
进程可以简单的理解为一个可以独立运行的程序单位,它是线程的集合,进程就是有一个或多个线程构成的。而线程是进程中的实际运行单位,是操作系统进行运算调度的最小单位。可理解为线程是进程中的一个最小运行单元。
那么多线程就很容易理解:多线程就是指一个进程中同时有多个线程正在执行。
为什么要使用多线程?
- 在一个程序中,有很多的操作是非常耗时的,如数据库读写操作,IO操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程,可以在将耗时任务放在后台继续执行的同时,同时执行其他操作。
- 可以提高程序的效率。
- 在一些等待的任务上,如用户输入,文件读取等,多线程就非常有用了。
多线程的缺点:
- 使用太多线程,是很耗系统资源,因为线程需要开辟内存。更多线程需要更多内存。
- 影响系统性能,因为操作系统需要在线程之间来回切换。
- 需要考虑线程操作对程序的影响,如线程挂起,中止等操作对程序的影响。
- 线程使用不当会发生很多问题。
总结:多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换(因为系统切换的速度非常的快,所以给我们在同时运行的错觉)。
二、多进程是什么?
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。凡是用于完成操作系统的各种功能的进程就是系统进程,而所有由你启动的进程都是用户进程。 |
同理,多进程就是指计算机同时执行多个进程,一般是同时运行多个软件。
三、多线程与多进程,选择谁?
下面是本人从知乎-pansz上转载的一个答案,非常通俗地回答了这个问题。
- 单进程单线程:一个人在一个桌子上吃菜。
- 单进程多线程:多个人在同一个桌子上一起吃菜。
- 多进程单线程:多个人每个人在自己的桌子上吃菜。
多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。
1。对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。
2。对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。
开桌子的意思是指创建进程。开销这里主要指的是时间开销。 可以做个实验:创建一个进程,在进程中往内存写若干数据,然后读出该数据,然后退出。此过程重复 1000 次,相当于创建/销毁进程 1000 次。在我机器上的测试结果是: UbuntuLinux:耗时 0.8 秒 Windows7:耗时 79.8 秒 两者开销大约相差一百倍。 这意味着,在 Windows 中,进程创建的开销不容忽视。换句话说就是,Windows 编程中不建议你创建进程,如果你的程序架构需要大量创建进程,那么最好是切换到 Linux 系统。 |
大量创建进程的典型例子有两个,一个是 gnu autotools 工具链,用于编译很多开源代码的,他们在 Windows 下编译速度会很慢,因此软件开发人员最好是避免使用 Windows。另一个是服务器,某些服务器框架依靠大量创建进程来干活,甚至是对每个用户请求就创建一个进程,这些服务器在 Windows 下运行的效率就会很差。这"可能"也是放眼全世界范围,Linux 服务器远远多于 Windows 服务器的原因。
再次补充:如果你是写服务器端应用的,其实在现在的网络服务模型下,开桌子的开销是可以忽略不计的,因为现在一般流行的是按照 CPU 核心数量开进程或者线程,开完之后在数量上一直保持,进程与线程内部使用协程或者异步通信来处理多个并发连接,因而开进程与开线程的开销可以忽略了。 另外一种新的开销被提上日程:核心切换开销。 现代的体系,一般 CPU 会有多个核心,而多个核心可以同时运行多个不同的线程或者进程。当每个 CPU 核心运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。 当每个 CPU 核心运行一个线程的时候,由于每个线程需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,才能继续运算,这占用了额外的开销。换句话说,在 CPU 为多核的情况下,多线程在性能上不如多进程。 因而,当前面向多核的服务器端编程中,需要习惯多进程而非多线程。 |
四、并行、并发、高并发等概念
并行:多个CPU实例或多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过CPU调度算法,让用户看上去同时执行,实际上CPU操作层面不是真正的同时。
并发时如果操作了公用资源,可能产生线程安全问题。
线程安全:多个线程操作公用资源,有可能产生安全问题。
高并发:高并发指的是是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问或者socket端口集中性收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化……。
多线程与高并发的联系
多线程只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式。
多线程在高并发问题中的作用就是充分利用计算机资源,使计算机的资源在每一时刻都能达到最大的利用率,不至于浪费计算机资源使其闲置。
进程和进程间通信
Qt提供了一个与平台无关的QProcess类,用以对进程的支持。本节讲述了怎样在Qt应用程序中启动一个外部程序进程,以及几种常用的进程间通信方法。
设计应用程序时,有时不希望将一个不太相关的功能集成到程序中,或者是因为该功能与当前设计的应用程序联系不大,或者是因为该功能已经可以使用现成的程序很好地实现了,这时就可以在当前的应用程序中调用外部的程序来实现该功能,这就会使用到进程。Qt应用程序可以很容易地启动一个外部应用程序,而且Qt也提供了多种进程间通信的方法。
一、运行一个进程
Qt的QProcess类可以用来启动一个外部程序并与其进行通信。下面我们来看一下怎么在Qt代码中启动一个进程。
运行一个进程打开记事本
首先创建QtGui应用,工程名称为“myProcess”,其他选项保持默认即可。先进入mainwindow.h文件添加代码,添加私有对象定义:QProcess myProcess。然后在设计模式往界面上拖入一个Push Button部件,修改其显示文本为“启动一个进程”。在按钮上点击鼠标右键,转到其clicked()信号对应的槽,更改如下:
void MainWindow::on_pushButton_clicked()
{
myProcess.start("notepad.exe");
}
这里我们使用QProcess对象运行了Windows系统下的记事本程序(即notepad.exe程序),因为该程序在系统目录中,所以这里不需要指定其路径。大家也可以运行其他任何的程序,只需要指定其具体路径即可。我们看到,可以使用start()来启动一个程序,有时启动一个程序时需要指定启动参数,这种情况在命令行启动程序时是很常见的。
运行程序,单击界面上的按钮时就会弹出一个记事本程序。
QProcess也提供了一组函数,可以脱离事件循环来使用,它们会挂起调用的线程直到确定的信号被发射:
- waitForStarted()阻塞直到进程已经启动;
- waitForReadyRead()阻塞直到在当前读通道上有可读的数据;
- waitForBytesWritten()阻塞直到一个有效负载数据已经被写人到进程;
- waitForFinished()阻塞直到进程已经结束。
二、进程间通信方式
Qt提供了多种方法在Qt应用程序中实现进程间通信IPC(Inter-Process Communication)。 简单介绍如下 :
TCP/IP
跨平台的Qt Network模块提供的类可以让网络编程更加便携和方便。它提供了高级类(例如:QNetworkAccessManager、QFtp)通信,使用特定的应用程序级协议,和较底层的类(例如:QTcpSocket、QTcpServer、QSslSocket)用于实现协议。
共享内存
跨平台的QSharedMemory-共享内存类,提供对操作系统的共享内存的实现。它允许多个线程和进程安全访问共享内存段。此外,QSystemSemaphore可以用来控制访问由系统共享的资源,以及进程之间的通信。
D-Bus
Qt的D-Bus模块是一种可用于使用D-Bus协议实现IPC的唯一Unix库。它将Qt的信号和槽机制延伸到IPC级别,允许由一个进程发出的信号被连接到另一个进程的槽。Qt的D-Bus文档已经详细说明如何使用Qt中的D-Bus模块。
QProcess
使用先前提到的QProcess类。跨平台类QProcess可以用于启动外部程序作为子进程,并与它们进行通信。它提供了用于监测和控制该子进程状态的API。另外,QProcess为从QIODevice继承的子进程提供了输入/输出通道。
会话管理
在Linux/X11平台上,Qt提供了会话管理的支持。会话允许事件传播到进程,例如,当检测到关机时。进程和应用程序可以执行任何必要的操作,例如:保存打开的文档。
三、共享内存之进程间通信实例
下面来看一个使用共享内存的例子,它实现的功能是:先在一个对话框中将一张图片写入到共享内存段中,然后再在另一个对话框中从共享内存段读出该图片。
新建Qt Gui应用,名称为mylPC,类名为Dialog,基类选择QDialog。完成后进人设计模式,向界面中放入两个Push Button部件和一个Label部件。将一个按钮的显示文本更改为“从文件中加载图片”,将其objectName属性更改为loadFromFileButton,将另一个按钮的显示文本更改为“从共享内存显示图片”,将其objectName属性更改为loadFromSharedMemoryButton。然后进人dialog.h文件,修改如下:
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QSharedMemory>
namespace Ui {
class Dialog;
}
class Dialog : public QDialog
{
Q_OBJECT
public:
explicit Dialog(QWidget *parent = nullptr);
~Dialog();
public slots:
//从文件中加载图片到共享内存
void loadFromFile();
//从共享内存中加载图片
void loadFromMemory();
private slots:
//从文件中加载图片按钮
void on_loadFromFileButton_clicked();
//从共享内存加载并显示图片按钮
void on_loadFromSharedMemoryButton_clicked();
private:
//将进程与共享内存段进行分离,如果失败则进行提示
void detach();
private:
Ui::Dialog *ui;
QSharedMemory sharedMemory; //定义一个共享内存对象
};
#endif // DIALOG_H
然后进人dialog.cpp文件,修改如下:
#include "dialog.h"
#include "ui_dialog.h"
#include <QFileDialog>
#include <QBuffer>
#include <QDebug>
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::Dialog)
{
ui->setupUi(this);
//在使用共享内存以前,需要设置key,系统用它作为底层共享内存段的标识
sharedMemory.setKey("QSharedMemoryExample");
}
Dialog::~Dialog()
{
delete ui;
}
//从文件中加载图片到共享内存
void Dialog::loadFromFile()
{
//判断该进程是否已经连接到共享内存段,连接成功返回true
if (sharedMemory.isAttached())
detach();
ui->label->setText(tr("选择一个图片文件!"));
//使用文件对话框获得打开图片路径
QString fileName = QFileDialog::getOpenFileName(nullptr, QString(), QString(),
tr("Images (*.png *.jpg)"));
//label标签显示图片
QImage image;
if (!image.load(fileName))
{
ui->label->setText(tr("选择的文件不是图片,请选择图片文件!"));
return;
}
ui->label->setPixmap(QPixmap::fromImage(image));
//将图片加载到共享内存
//使用QBuffer来暂存图片,这样便可以获得图片的大小,还可以获得图片数据的指针
QBuffer buffer;
buffer.open(QBuffer::ReadWrite);
QDataStream out(&buffer);
out << image;
int size = static_cast<int>(buffer.size());
//使用create()函数创建指定大小的共享内存段,该函数还会自动将共享内存段连接到本进程上。
if (!sharedMemory.create(size))
{
ui->label->setText(tr("无法创建共享内存段!"));
return;
}
//在进行共享内存段的操作前,需要先进行加锁
sharedMemory.lock();
char *to = static_cast<char*>(sharedMemory.data());
const char *from = buffer.data().data();
//使用memcpy()函数将图片数据复制到共享内存
memcpy(to, from, static_cast<size_t>(qMin(sharedMemory.size(), size)));
//等操作完成后,再进行解锁。
sharedMemory.unlock();
}
//从共享内存中加载图片
void Dialog::loadFromMemory()
{
//使用attache()函数将进程连接到共享内存段
if (!sharedMemory.attach())
{
ui->label->setText(tr("无法连接到共享内存段,\n"
"请先加载一张图片!"));
return;
}
QBuffer buffer;
QDataStream in(&buffer);
QImage image;
//使用QBuffer来读取共享内存段中的数据
sharedMemory.lock();
buffer.setData((char*)sharedMemory.constData(), sharedMemory.size());
buffer.open(QBuffer::ReadOnly);
in >> image;
sharedMemory.unlock();
//将进程与共享内存段进行分离,如果失败则进行提示
sharedMemory.detach();
ui->label->setPixmap(QPixmap::fromImage(image));
}
//将进程与共享内存段进行分离,如果失败则进行提示
void Dialog::detach()
{
if (!sharedMemory.detach())
ui->label->setText(tr("无法从共享内存中分离!"));
}
//从文件中加载图片按钮
void Dialog::on_loadFromFileButton_clicked()
{
loadFromFile();
}
//从共享内存加载并显示图片按钮
void Dialog::on_loadFromSharedMemoryButton_clicked()
{
loadFromMemory();
}
在一个运行的实例上单击“从文件中加载图片”按钮,然后选择一张图片。在第二个运行的实例上单击“从共享内存显示图片”按 钮,这时便会显示第一个实例中加载的图片,效果如下所示:
四、程序分析
(1)loadFromFile()
- 这里先使用isAttached()函数判断该进程是否已经连接到共享内存段,如果是,那么就调用detach()先将该进程与共享内存段进行分离。
- 然后使用QFileDialog类来打开一个图片文件,并将其显示到标签上。为了将图片加载到共享内存,这里使用了QBuffer来暂存图片,这样便可以获得图片的大小,还可以获得图片数据的指针。
- 后面使用了create()函数来创建指定大小的共享内存段,其大小的单位是字节,该函数还会自动将共享内存段连接到本进程上。
- 在操作共享内存段时要使用lock()进行加锁,然后才可以使用memcpy()函数将buffer对应的数据段复制到共享内存段,操作完成后要使用unlock()进行解锁。这样在同一时间,就只能有一个进程允许操作共享内存段了。
(2)loadFromMemory()
- 这里先使用attache()函数将进程连接到共享内存段。
- 在操作共享内存段时要使用lock()进行加锁,然后才可以使用QBuffer来读取共享内存段中的数据,操作完成后要使用unlock()进行解锁。
- 因为现在已经不需要使用共享内存了,所以调用detach()函数将进程与共享内存段进行分离。最后将图片显示到标签中。