13. 进程和线程

13.1. 进程线程概念

进程和线程是操作系统中的两个基本概念,是伴随着计算机发展,人对计算机的需求而逐渐产生的。

我们都是知道计算机的核心就是CPU,CPU主要负责运算,其速度非常非常快。 但是计算机还有硬盘,输入输出等等组成,这些组成部分速度相较CPU就差很多。 我们理想的状态就是CPU一直工作,在等待其他低速设备把资源准备好的时候,切换去执行其他的任务,这样就产生了进程。

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位, 是应用程序运行的载体。每个进程拥有独立的内存地址控件和运行状态,操作系统会根据调度算法(比如参考进程状态)对任务进行调度切换, 控制CPU加载不同的内存地址最终去执行不同的任务。

计算机发展到这个阶段,似乎从宏观上就实现了并发(即多个任务同时执行,对于单核CPU来说实际上是微观串行宏观并行)。 但是要考虑到,进程本身是需要消耗资源的,进程切换也是如此,所以非常小的进程就不划算,因此就出现了线程。

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。 线程就可以粗暴的理解为进程的细分,多个线程共用一个进程的内存地址,线程更为轻量上下文切换比进程消耗资源更少。

我在举个简单的例子来区分一下这两个概念:上面的这一切就好比我们公司,我们有个共同的目标那就是公司越发展越好。 我们公司分很多部门很多组,有linux,有mcu,有fpga,这就好比进程。 每个组又由很多像我这样的螺丝钉组成,在各自的组里面负责更为细分的工作,比如linux有硬件,驱动,系统,应用,技术等等, 每个部分都有不同的产品,研发都在同时推进(当然在时间上就是完全并行了), 产品开发的进度又由部门内部成员的工作进度决定,对于单个产品来说我们个人的工作也是并行的。

实际上线程的用途也是单个进程的并行,在多核CPU中,拥有多个线程的程序会将线程分配给多个内核实现真的的并行。

13.2. Qt中的线程

这个时候我们再从应用(代码)的角度来看什么是进程和线程。

这时我电脑上运行着两个程序,VSCODE和酷狗音乐播放器。 我正在用VSCODE写文档,也就你看到的这些文字;然后音乐播放器真播放着我喜欢的音乐。 简单的就能把一个应用理解成一个进程,它们同时工作为我服务,这就是所谓的多任务。

在看音乐播放器,播放器将mp3格式的文件解码,并二进制数据送入音频驱动器, 同时桌面上的歌词。进度条也在不断的更新,这就是线程

13.2.1. GUI线程和辅助线程

如上面所说,每个程序在启动时都有一个线程,这个线程称为“主线程”(在Qt应用程序中也称为 GUI线程 )。 Qt GUI 必须在此线程中运行,所有窗口小部件和几个相关类(例如QPixmap)在辅助线程中均不起作用。 辅助线程通常称为 工作线程 ,因为它用于从主线程分担处理工作。

每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史记录和局部变量,与进程不同,线程共享相同的地址空间。 下图显示了线程的构造块如何在内存中定位。

thread001

非活动线程的程序计数器和寄存器通常保存在内核空间中,有一个代码的共享副本,并且每个线程都有一个单独的堆栈。

如果两个线程都具有指向同一对象的指针,则两个线程可能会同时访问该对象,这有可能破坏对象的完整性。 容易想象,当同时执行同一对象的两个方法时,可能会出错。

有时有必要从不同的线程访问一个对象。例如,当位于不同线程中的对象需要进行通信时。 由于线程使用相同的地址空间,数据不必序列化和复制,因此与进程相比,线程交换数据更容易,更快捷。 同时必须严格协调什么线程接触哪个对象,必须防止在一个对象上同时执行操作。

13.3. 多线程

使用线程基本上有下面两个好处

  • 通过使用多核处理器来加快处理速度。

  • 通过减轻长时间处理的负担或阻止对其他线程的调用,使GUI线程或其他时间紧迫的线程保持响应。

但是使用线程时需要非常小心,启动线程很容易,但是很难确保所有共享数据保持一致。 而且问题通常很难发现,因为它们可能仅偶尔出现一次,或者仅在特定的硬件配置上出现。

QObject如何与线程交互

当QObject接收到队列中的信号或发布的事件时,该对象的槽函数或事件处理程序将在该对象所在的线程中运行。 线程之外将将无法接收排队的信号或已发布的事件。

默认情况下,QObject位于创建它的线程中。可以使用thread() 查询对象的线程亲和力,并使用moveToThread()来更改所在线程。 除此以外,所有QObject必须与其父代驻留在同一线程中。

QObject的成员变量不会自动成为其子对象,必须通过将指针传递给子对象的构造函数或通过调用 setParent() 来指定其父对象

多个线程访问安全数据

Qt提供了QMutex、QReadWriteLock、QSemaphore和QWaitCondition低级原语以及用于同步线程的高级事件队列。

QMutex是强制执行互斥的基本类,线程锁定互斥锁以获取对共享资源的访问。如果第二个线程试图在已锁定互斥锁的同时锁定它,则第二个线程将进入睡眠状态,直到第一个线程完成其任务并解锁该互斥锁。

QReadWriteLock类似于QMutex,除了它“读”和“写”操作进行了区分。当不写入数据时,可以安全地同时读取多个线程,从而提高了并行性。

QSemaphore是的一般化QMutex保护一定数目的相同的资源。相比之下,QMutex只保护一种资源。QSemaphore的典型应用就是同步访问生产者和消费者之间的环形缓冲器(生产者-消费者模型)。

QWaitCondition不通过强制互斥而是通过提供条件变量来同步线程。当其他原语使线程等待直到资源被解锁时,QWaitCondition使线程等待直到满足特定条件。 要允许等待的线程继续进行,请调用wakeOne()来唤醒一个随机选择的线程,或者调用wakeAll()来同时唤醒它们。

例如在文件存储与读取章节中,Config例程中就使用QMutex来保障线程安全。

embed_qt_develop_tutorial_code/Data/Config/config.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
class Config
{
public:
    static QSharedPointer<Config>& instance()
    {
        if (self.isNull())
        {
            QMutexLocker mutexLocker(&m_Mutex);
            if (self.isNull())
                self = QSharedPointer<Config>(new Config());
        }
        return self;
    }

...
private:
    static QMutex m_Mutex;
    static QSharedPointer<Config> self;

...
};

当然使用互斥锁也有可能会存在如下问题:

  • 如果一个线程锁定了一个资源但没有将其解锁,则该应用程序可能会冻结,因为该资源将永久无法供其他线程使用。

  • 另一个类似的情况是僵局。例如,假设线程A正在等待线程B解锁资源。如果线程B也正在等待线程A解锁其他资源,则两个线程最终将永远等待,因此应用程序将冻结。

高级事件队列

Qt的事件系统对于线程间通信非常有用,每个线程可能都有其自己的事件循环。 要在另一个线程中调用插槽(或任何可调用的方法),请将该调用置于目标线程的事件循环中。 这样,目标线程就可以在插槽开始运行之前完成其当前任务,而原始线程则可以继续并行运行。

要将调用置于事件循环中,请建立排队的信号槽连接。 每当发出信号时,事件系统都会记录其自变量,信号接收器所在的线程将运行该插槽; 或者,调用QMetaObject::invokeMethod()以达到没有信号的相同效果。 在这两种情况下,都必须使用排队连接,因为直接连接会绕过事件系统并立即在当前线程中运行该方法。

异步执行

获得工作线程结果的一种方法是等待线程终止。但是,在许多情况下,阻塞等待是不可接受的。阻塞等待的替代方法是通过发布事件或排队的信号和时隙异步传递结果。 这会产生一定的开销,因为操作的结果不会出现在下一个源代码行中,而是出现在源文件中其他位置的插槽中。

线程是一个非常复杂的话题,我们暂时就只讲这么多。

13.4. 进程间通信

前面我们有提到VSCODE和酷狗音乐播放器的例子,这就是两个进程,线程之间可以通信,那进程呢?

当然可以,比如Qt D-Bus,QLocalSocket、QLocalServer以及QSharedMemory

这里举常用的例子,应用程序只运行一个实例

QSharedMemory实现

embed_qt_develop_tutorial_code/Ipc/SingleApp/main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 临界区
    QSystemSemaphore semaphore("semaphore",1,QSystemSemaphore::Open);
    semaphore.acquire();

    // QSharedMemory
    QSharedMemory memory("memory");
    if (!memory.create(1))
    {
        QString dlgTitle="information";
        QString str="实例已运行,该APP只允许运行一个实例";
        QMessageBox::information(nullptr, dlgTitle,str,QMessageBox::Yes);
        semaphore.release();
        return 0;
    }
    semaphore.release();

    MainWindow w;
    w.show();
    return a.exec();
}

这种方式有弊端,在程序发生崩溃时,未及时清除共享区数据,可能会导致程序不能正常启动。

13.4.1. QProcess

出来进程间通信,可能我们现在这阶段用得更多是,如何去启动一个进程。

要用QProcess启动进程,请将要运行的程序的名称和命令行参数作为参数传递给start(),参数作为单独的字符串提供在QStringList中。 或者,您可以将程序设置为与setProgram()和setArguments()一起运行,然后调用start()或open()。 然后QProcess进入启动状态,并且在程序启动后,QProcess进入运行状态并发出started()信号。

QProcess允许您将进程视为顺序I/O设备,您可以像使用QTcpSocket访问网络连接一样来写入和读取该过程。 然后,您可以通过调用write()来写入流程的标准输入,并通过调用read(),readLine()和getChar()来读取标准输出。 由于QProcess继承了QIODevice,因此它还可以用作QXmlReader的输入源,或生成要使用QNetworkAccessManager上载的数据。

当QProcess退出,QProcess中重新进入notrunning状态的状态(初始状态),并发出finished()信号。 finished()信号提供工艺参数的退出代码和退出状态,你也可以调用exitCode() 来获得最后的进程的退出代码完成,并通过exitStatus()来获得它的退出状态。 如果进程在任何时间点发生错误,QProcess将发出errorOccurred()信号。您还可以调用error()来查找上次发生的错误的类型,并调用state()来查找当前进程的状态。

在野火demo中很多地方都使用到QProcess。

比如在语言设置,之后需要重启App,就是通过QProcess去重新执行run.sh脚本。

embed_qt_develop_tutorial_code/Data/Settings/src/languagepage.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void LanguagePage::SltCurrentIndexClicked(QtListWidgetItem *item)
{
    AppConfig::SaveSetting("System", "language", item->m_strText);
#if 0
    int nRet = QtMessageBox::ShowAskMessage(tr("语言设置重启生效,是否立即重启?"), tr("语言设置"));
    if (nRet == QDialog::Accepted)
    {
#ifdef __arm__
        QString strCmd = qApp->applicationDirPath() + "/run.sh";
        QProcess::startDetached(strCmd);
        qApp->quit();
#endif
    }

#endif
}

13.5. 例程说明

野火提供的Qt Demo已经开源,仓库地址在:

文档所涉及的示例代码也提供下载,仓库地址在:

本章例程在 embed_qt_develop_tutorial_code/KeyPressTest

本例程为野火demo的按键测试,注意本例程涉及硬件,需要根据LubanCat板子型号更改硬件设备。

具体参考 Qt控制LED灯章节

本例程主要监控LubanCat的按键状态,例程使用线程实现。

创建类ThreadKey,继承自QThread。

embed_qt_develop_tutorial_code/KeyPressTest/threadkey.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef THREADKEY_H
#define THREADKEY_H

#include <QThread>

class ThreadKey : public QThread
{
    Q_OBJECT
public:
    explicit ThreadKey(QObject *parent = 0, quint8 type = 0);
    ~ThreadKey();
    void Stop();

signals:
    void SltKeyPressed(const quint8 &type);
    void signalKeyPressed(const quint8 &code,const quint8 &value);

public slots:

private:

protected:
    quint8 m_nKeyType;
    quint8 m_nKeyPressed;

    bool m_bRun;
    void run();
};

#endif // THREADKEY_H

重写run()函数。

embed_qt_develop_tutorial_code/KeyPressTest/threadkey.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void ThreadKey::run()
{
#ifdef __arm__
    int fd = 0;
    struct input_event inmyself;
    //fd = open(0 == m_nKeyType ? POWER_DEV : KEY_DEV, O_RDONLY);
    fd = open(KEY_DEV, O_RDONLY);
    if (fd < 0)
    {
        printf("Open keyboard failed .\n");
        return;
    }

    m_bRun = true;
    while (m_bRun)
    {

        if (read(fd, &inmyself, sizeof(inmyself)) < 0) {
            m_bRun = false;
            break;
        }
        else {
            //qDebug()<<inmyself.code<<inmyself.type<<inmyself.value;
            emit signalKeyPressed(inmyself.code,inmyself.value);
        }
    }

    close(fd);
#else

    while(1)
    {
        switch(qrand() % 3)
        {
        case 0:
            emit signalKeyPressed(KEY1,qrand() % 2);
            break;
        case 1:
            emit signalKeyPressed(KEY2,qrand() % 2);
            break;
        case 2:
            emit signalKeyPressed(WAKE_UP,qrand() % 2);
            break;
        default:
            break;
        }
        sleep(1);
    }
#endif

}

在run()函数中,打开按键的设备文件,读取按键状态。

其中input_event是一个结构体,内容如下:

struct input_event {
#if (__BITS_PER_LONG != 32 || !defined(__USE_TIME_BITS64)) && !defined(__KERNEL)
    struct timeval time;
#define input_event_sec time.tv_sec
#define input_event_usec time.tv_usec
#else
    __kernel_ulong_t __sec;
    __kernel_ulong_t __usec;
#define input_event_sec  __sec
#define input_event_usec __usec
#endif
    __u16 type;
    __u16 code;
    __s32 value;
};
  • time:该变量用于记录事件产生的时间戳,既evtest输出的time值。

  • type:输入设备的事件类型。系统常用的默认类型有EV_KEY、 EV_REL和EV_ABS,分别用 于表示按键状态改变事件、相对坐标改变事件及绝对坐标改变事件,特别地,EV_SYN用于分隔事件,无特别意义。如果选择鼠标(本章第一个图) evtest输出的type类型为EV_ABS。相关的枚举值可以参考内核文件include/uapi/linux/input-event-codes.h。

  • code:事件代号,它以更精确的方式表示事件。例如 在EV_KEY事件类型中,code的值常用于表 示键盘上具体的按键,其取值范围在0~127之间,例如按键Q对应的是KEY_Q,该枚举变量的 值为16。如果选择鼠标, evtest输出内容的code分别有ABS_X/ABS_Y,表示上报的是X或Y坐标。

  • value:事件的值。对于EV_KEY事件类型,当按键按下时,该值为1;按键松开时,该值为0。如果选择 鼠标,中evtest输出的内容里,ABS_X事件类型中的value值表示X坐标,ABS_Y类型中的value值表示Y坐标。

更多 input子系统

调用read函数将按键的状态读到inmyself中,然后将按键的来源(具体哪一个按键被按下),按键状态通过信号发送出去。

m_threadKey = new ThreadKey(this, 1);
connect(m_threadKey, SIGNAL(signalKeyPressed(const quint8 ,const quint8)), this, SLOT(SltKeyPressed(const quint8 ,const quint8)));
m_threadKey->start();

在 embed_qt_develop_tutorial_code/KeyPressTest/src/keypresswidget.cpp 中,我们将该信号绑定到SltKeyPressed()槽函数。 在SltKeyPressed()中控制UI。

build001
  • Ubuntu 选择 Desktop Qt 5.11.3 GCC 64bit 套件,编译运行

  • LubanCat 选择 ebf_lubancat,只编译

提示

当两种构建套件进行切换时,请重新构建项目或清除之前项目。针对我们的工程还需要手动重新构建QtUI和Skin。

build002

13.5.1. PC实验

直接点击编译并运行程序,结果如下:

result001

13.5.2. LubanCat实验

通过SCP或者NFS将编译好的程序拷贝到LubanCat上

NFS环境搭建 参考这里

SCP命令如下:

# scp传输文件
# scp 文件名 服务器上的某个用户@服务器ip:/文件保存路径
scp filename server_user_name@192.168.0.205:server_file_path
# 从服务器拉取文件
# scp 服务器上的某个用户@服务器ip:/服务器文件存放路径 拉取文件保存路径
scp server_user_name@192.168.0.229:server_file_path local_path

编译好的程序在 embed_qt_develop_tutorial_code/app_bin 目录中,通过scp命令将编译好的程序拉到LubanCat。

scp root@192.168.0.174:/home/embed_qt_develop_tutorial_code/app_bin/KeyPressTest /usr/local/qt-app/

在LubanCat运行程序,使用run_myapp.sh配置好环境,并执行 KeyPressTest 。

sudo /usr/local/qt-app/run_myapp.sh /usr/local/qt-app/KeyPressTest
result002