本文已参与「新人创作礼」活动,一起开启掘金创作之路。
基础知识
并发与并行
并发:单个CPU核心需要处理多个任务时会进行轮询,同一时刻只能处理一个任务。
并行:多个CPU核心同时处理任务,同一时刻能处理多个任务。
那么多线程编程是并发还是并行的呢?
我的理解是:在单核系统中,多线程是并发执行的;在多核系统中,多线程是并行执行的。
QtConcurrent::run
的使用
在.pro
文件中添加:QT += concurrent
QtConcurrent::run
可以使用全局线程池和自定义线程池
// 全局线程池
template <typename T> QFuture<T> QtConcurrent::run(QThreadPool::globalInstance(), Function function, ...);
// 自定义线程池
template <typename T> QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, ...)
单线程与多线程程序的设计
调用简单的add()
函数
#include <QCoreApplication>
#include <QDebug>
// add函数用于将count指向的内存自增1并打印出结果
void add(int *count) {
*count = *count + 1;
qDebug() << "thread id: " << QThread::currentThreadId() << "count: " << *count;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
int count = 0;
for (int i=0; i<10; i++) {
add(&count);
}
return a.exec();
}
无论如何执行,函数总是会按照顺序输出:1到10。此时输出的thread id为主线程id。
多线程中的的add()
函数
#include <QCoreApplication>
#include <QDebug>
#include <QtConcurrent/QtConcurrent>
#include <QThreadPool>
#include <thread>
// add函数用于将count指向的内存自增1并打印出结果
void add(int *count) {
*count = *count + 1;
qDebug() << "thread id: " << QThread::currentThreadId() << "count: " << *count;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 获取CPU核心数
int numberOfCores = std::thread::hardware_concurrency();
qDebug() << "Number of cores: " << numberOfCores;
int count = 0;
// 定义局部线程池变量
QThreadPool pool;
// 设置线程池最大使用的线程数量
pool.setMaxThreadCount(numberOfCores * 2 + 1);
for (int i=0; i<10; i++) {
// 将函数放入线程池中运行
QtConcurrent::run(&pool, add, &count);
}
return a.exec();
}
这是在我电脑中执行后的输出:
可以发现它并没有按照预期按照顺序输出:1到10。这里可以看到在输出相同数字时的thread id是不同的。造成这个的原因见下图:
线程0x1dac
、0x2c98
和0x4fb4
在同一时刻同时执行了*count = *count + 1;
,所以当执行到qDebug() << ......
时,变量count
已经被增加了3,所以会出现红色框中qDebug
输出了3个相同的4,至于qDebug
在何时输出就没那么重要了。
在多线程开发中,如果有一个变量对多线程是可见的,那么这个变量是非线程安全的。
加锁,使多线程按照预期输出(QMutex)
#include <QCoreApplication>
#include <QDebug>
#include <QtConcurrent/QtConcurrent>
#include <QThreadPool>
#include <thread>
#include <QMutex>
// add函数用于将count指向的内存自增1并打印出结果
void add(QMutex *mutex, int *count) {
mutex->lock();
*count = *count + 1;
qDebug() << "thread id: " << QThread::currentThreadId() << "count: " << *count;
mutex->unlock();
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 获取CPU核心数
int numberOfCores = std::thread::hardware_concurrency();
qDebug() << "Number of cores: " << numberOfCores;
int count = 0;
// 定义局部线程池变量
QThreadPool pool;
// 设置线程池最大使用的线程数量
pool.setMaxThreadCount(numberOfCores * 2 + 1);
// 定义锁变量
QMutex mutex;
for (int i=0; i<10; i++) {
// 将函数放入线程池中运行
QtConcurrent::run(&pool, add, &mutex, &count);
}
return a.exec();
}
无论运行多少次,函数总会预期的按照顺序输出:1到10。这是因为多线程共用了QMutex
的锁对象,当一个线程执行方法QMutex::lock()
后会占用当前锁对象,其他线程会等待该锁对象解锁QMutex::unlock()
后才有资格去抢这把锁,只有抢到锁对象的才能够继续往下执行。所以执行的顺序如下图:
利用析构函数在函数退出时自动解锁(QMutexLocker)
#include <QCoreApplication>
#include <QDebug>
#include <QtConcurrent/QtConcurrent>
#include <QThreadPool>
#include <thread>
#include <QMutex>
#include <QMutexLocker>
// add函数用于将count指向的内存自增1并打印出结果
void add(QMutex *mutex, int *count) {
QMutexLocker _(mutex);
*count = *count + 1;
qDebug() << "thread id: " << QThread::currentThreadId() << "count: " << *count;
}
......
死锁
怎样才能死锁?
以下死锁产生的四个必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:若干进程行程形成循环等待资源关系。
测试死锁的代码:
#include <QCoreApplication>
#include <QDebug>
#include <QtConcurrent/QtConcurrent>
#include <QThreadPool>
#include <thread>
#include <QMutex>
// 定义f1函数
void f1(QMutex *m1, QMutex *m2) {
m1->lock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "m1 lock";
m2->lock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "m2 lock";
m2->unlock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "m2 unlock";
m1->unlock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "m1 unlock";
}
// 定义f2函数
void f2(QMutex *m1, QMutex *m2) {
m2->lock();
qDebug() << "f2 thread id: " << QThread::currentThreadId() << "m2 lock";
m1->lock();
qDebug() << "f2 thread id: " << QThread::currentThreadId() << "m1 lock";
m1->unlock();
qDebug() << "f2 thread id: " << QThread::currentThreadId() << "m1 unlock";
m2->unlock();
qDebug() << "f2 thread id: " << QThread::currentThreadId() << "m2 unlock";
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 获取CPU核心数
int numberOfCores = std::thread::hardware_concurrency();
qDebug() << "Number of cores: " << numberOfCores;
// 定义局部线程池变量
QThreadPool pool;
// 设置线程池最大使用的线程数量
pool.setMaxThreadCount(numberOfCores * 2 + 1);
// 定义锁变量
QMutex m1, m2;
for (int i=0; i<100; i++) {
// 将函数放入线程池中运行
QtConcurrent::run(&pool, f1, &m1, &m2);
QThread::sleep(1);
QtConcurrent::run(&pool, f2, &m1, &m2);
}
return a.exec();
}
**当两个线程分别占有m2和m1时,死锁就这样发生了。**因为线程1占有了m1等待m2解锁后才能解锁m1而线程2占有了m2等待m1解锁后才能解锁m2,所以此时两个线程都被堵塞住无法继续正常执行了。
解决死锁
将等待资源的循环关系破坏掉,即可避免死锁了。将
f2
加锁顺序修改成先获得m1再获得m2,与f1
的加锁顺序一致。
#include <QCoreApplication>
#include <QDebug>
#include <QtConcurrent/QtConcurrent>
#include <QThreadPool>
#include <thread>
#include <QMutex>
// 定义f1函数
void f1(QMutex *m1, QMutex *m2, int index) {
m1->lock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m1 lock";
m2->lock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m2 lock";
m2->unlock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m2 unlock";
m1->unlock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m1 unlock";
}
// 定义f2函数
void f2(QMutex *m1, QMutex *m2, int index) {
m1->lock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m1 lock";
m2->lock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m2 lock";
m2->unlock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m2 unlock";
m1->unlock();
qDebug() << "f1 thread id: " << QThread::currentThreadId() << "index: " << index << " m1 unlock";
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 获取CPU核心数
int numberOfCores = std::thread::hardware_concurrency();
qDebug() << "Number of cores: " << numberOfCores;
// 定义局部线程池变量
QThreadPool pool;
// 设置线程池最大使用的线程数量
pool.setMaxThreadCount(numberOfCores * 2 + 1);
// 定义锁变量
QMutex m1, m2;
for (int i=0; i<10; i++) {
// 将函数放入线程池中运行
QtConcurrent::run(&pool, f1, &m1, &m2, i);
QThread::sleep(1);
QtConcurrent::run(&pool, f2, &m1, &m2, i);
}
return a.exec();
}
当线程1占有m1时,线程2等待m1解锁**(此时线程2无法占用m2锁)**,这时候线程1继续占有m2,然后解锁m2和m1,当m1解锁后线程2才能继续执行,所以不会产生死锁。