多线程加锁的意义与死锁

基础知识

并发与并行

并发:单个CPU核心需要处理多个任务时会进行轮询,同一时刻只能处理一个任务。

并行:多个CPU核心同时处理任务,同一时刻能处理多个任务。

Image description

那么多线程编程是并发还是并行的呢?

我的理解是:在单核系统中,多线程是并发执行的;在多核系统中,多线程是并行执行的。

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();
}

这是在我电脑中执行后的输出:

Image description

可以发现它并没有按照预期按照顺序输出:1到10。这里可以看到在输出相同数字时的thread id是不同的。造成这个的原因见下图:

Image description

线程0x1dac0x2c980x4fb4在同一时刻同时执行了*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()后才有资格去抢这把锁,只有抢到锁对象的才能够继续往下执行。所以执行的顺序如下图:

Image description

利用析构函数在函数退出时自动解锁(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();
}

Image description

**当两个线程分别占有m2和m1时,死锁就这样发生了。**因为线程1占有了m1等待m2解锁后才能解锁m1而线程2占有了m2等待m1解锁后才能解锁m2,所以此时两个线程都被堵塞住无法继续正常执行了。

Image description

解决死锁

将等待资源的循环关系破坏掉,即可避免死锁了。将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才能继续执行,所以不会产生死锁。

Image description

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值