CPP多线程2:多线程竞争与死锁问题

在多线程编程中,多个线程协同工作能显著提升程序效率,但当它们需要共享和操作同一资源时,潜在的问题也随之而来;线程间的执行顺序不确定性可能导致资源竞争,可能引发死锁,让程序陷入停滞。

多线程竞争问题示例

我们现在已经知道如何在c++11中创建线程,那么如果多个线程需要操作同一个变量呢?

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;
int n = 0;
void count10000() {
	for (int i = 1; i <= 10000; i++)
		n++;
}
int main() {
	thread th[100];
	for (thread &x : th)
		x = thread(count10000);
	for (thread &x : th)
		x.join();
	cout << n << endl;
	return 0;
}

可能的两次输出分别是:

plain 复制代码
991164
996417

我们的输出结果应该是1000000,可是为什么实际输出结果比1000000小呢?

在多线程的执行顺序------同时进行、无次序,所以这样就会导致一个问题:多个线程进行时,如果它们同时操作同一个变量,那么肯定会出错。为了应对这种情况,c++11中出现了std::atomic和std::mutex。

std::mutex

std::mutex是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特性,我们可以修改一下上一个例子中的代码:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int n=0;
mutex mtx;
void count10000(){
    for(auto i=0;i<10000;i++){
        mtx.lock();
        n++;
        mtx.unlock();
    }
}
int main(){
    thread th[100];
	for (thread &x : th)
		x = thread(count10000);
	for (thread &x : th)
		x.join();
    cout<<n<<endl;
    return 0;
}

mutex的常用成员函数

std::lock_gard

使用mutex需要上锁解锁,但有时由于程序员忘记或者其他奇怪问题时,lock_gard可以自动解锁。其原理大概是构造时自动上锁,析构时自动解锁。示例如下:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int n=0;
mutex mtx;
void count100000(){
    for(auto i=0;i<100000;i++){
        lock_guard<mutex> lock1(mtx);
        n++;
        n--;
    }
}
int main(){
    thread th[10];
    for(int i=0;i<10;i++){
        th[i]=thread(count100000);
    }
    for(int i=0;i<10;i++){
        th[i].join();
    }
    cout<<n<<endl;
    return 0;
}

std::unique_lock

std::unique_lock是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

int n = 0;
mutex mtx;

void count100000() {
    for (auto i = 0; i < 100; ++i) {
        unique_lock<mutex> lock1(mtx);
        n++;
        lock1.unlock();  // 提前解锁,体现unique_lock的灵活性
        this_thread::sleep_for(chrono::milliseconds(1));  // 模拟耗时操作
        lock1.lock();
        n--;
    }
}

int main() {
    thread th[10];
    for (int i = 0; i < 10; ++i) {
        th[i] = thread(count100000);
    }
    for (int i = 0; i < 10; ++i) {
        th[i].join();  // 等待所有线程完成
    }
    cout << n << endl;  // 所有线程结束后输出结果(理论上应为0)
    return 0;
}

公共构造函数

函数 作用
unique_lock() noexcept = default 默认构造函数,创建一个未关联任何互斥量的std::unique_lock对象。
explicit unique_lock(mutex_type& m) 构造函数,使用给定的互斥量m进行初始化,并对该互斥量进行加锁操作。
unique_lock(mutex_type& m, defer_lock_t) noexcept 构造函数,使用给定的互斥量m进行初始化,但不对该互斥量进行加锁操作。
unique_lock(mutex_type& m, try_to_lock_t) noexcept 构造函数,使用给定的互斥量m进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock对象不与任何互斥量关联。
unique_lock(mutex_type& m, adopt_lock_t) noexcept 构造函数,使用给定的互斥量m进行初始化,并假设该互斥量已经被当前线程成功加锁。
std::unique_lock 使用非常灵活方便,上述操作的使用方式将在课程视频中作详细介绍。

常用成员函数

函数 作用
lock() 尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
try_lock() 尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false,否则返回true。
try_lock_for(const std::chrono::duration<Rep, Period>& rel_time) 尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time) 尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
unlock() 对互斥量进行解锁操作。

std::atomic

mutex很好地解决了多线程资源争抢的问题,但它也有缺点:太......慢......了......

比如前面我们定义了100个thread,每个thread要循环10000次,每次循环都要加锁、解锁,这样固然会浪费很多的时间,那么该怎么办呢?接下来就是atomic大展拳脚的时间了。

cpp 复制代码
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;

atomic<int> n{0};// 列表初始化

void count10000(){
    for(int i=0;i<10000;i++)
        n++;
}

int main(){
    thread th[10];
    for(thread& x:th)
        x=thread(count10000);
    for(auto& x:th)
        x.join();
    cout<<n<<endl;
    return 0;
}

可以看到,我们只是改动了n的类型(int->std::atomic_int),其他的地方一点没动,输出却正常了。

atomic,本意为原子,可解释为:

原子操作是最小的且不可并行化的操作。

atomic常用成员函数

死锁问题

在多个线程中由于上锁顺序问题可能导致线程卡死,如下:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int n=0;
mutex mtx1;
mutex mtx2;
void count100000(){
    for(auto i=0;i<100000;i++){
        lock_guard<mutex> lock1(mtx1);
        lock_guard<mutex> lock2(mtx2);
        n++;
        n--;
    }
}
void count200000(){
    for(auto i=0;i<200000;i++){
        lock_guard<mutex> lock2(mtx2);
        lock_guard<mutex> lock1(mtx1);
        n++;
        n--;
    }
}
int main(){
    thread th[10];
    for(int i=0;i<10;i++){
        if(i%2==0)
            th[i]=thread(count100000);
        else
            th[i]=thread(count200000);
    }
    for(int i=0;i<10;i++){
        th[i].join();
    }
    cout<<n<<endl;
    return 0;
}

这是因为在一个线程count100000中mtx1上锁后,另一个线程count200000也正好将mtx2上锁,于是这两个线程没办法获得另一个mutex,这就是死锁问题。

解决方法就是保持一样的上锁顺序,于是当一个线程A抢到第一个mutex时,其他线程无法再获得mutex,即只能线程A按着顺序处理完所有事物。示例如下:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int n=0;
mutex mtx1;
mutex mtx2;
void count100000(){
    for(auto i=0;i<100000;i++){
        lock_guard<mutex> lock1(mtx1);
        lock_guard<mutex> lock2(mtx2);
        n++;
        n--;
    }
}
void count200000(){
    for(auto i=0;i<200000;i++){
        lock_guard<mutex> lock1(mtx1);
        lock_guard<mutex> lock2(mtx2);
        n++;
        n--;
    }
}
int main(){
    thread th[10];
    for(int i=0;i<10;i++){
        if(i%2==0)
            th[i]=thread(count100000);
        else
            th[i]=thread(count200000);
    }
    for(int i=0;i<10;i++){
        th[i].join();
    }
    cout<<n<<endl;
    return 0;
}
相关推荐
2401_841495647 小时前
【数据结构】红黑树的基本操作
java·数据结构·c++·python·算法·红黑树·二叉搜索树
liu****8 小时前
负载均衡式的在线OJ项目编写(六)
运维·c++·负载均衡·个人开发
青草地溪水旁8 小时前
设计模式(C++)详解——迭代器模式(3)
c++·设计模式·迭代器模式
奔跑吧邓邓子9 小时前
【C++实战㊺】解锁C++代理模式:从理论到实战的深度剖析
c++·实战·代理模式
杜子不疼.9 小时前
【C++】玩转模板:进阶之路
java·开发语言·c++
夜晚中的人海9 小时前
【C++】异常介绍
android·java·c++
m0_552200829 小时前
《UE5_C++多人TPS完整教程》学习笔记60 ——《P61 开火蒙太奇(Fire Montage)》
c++·游戏·ue5
charlie1145141919 小时前
精读C++20设计模式——行为型设计模式:迭代器模式
c++·学习·设计模式·迭代器模式·c++20
小欣加油10 小时前
leetcode 1863 找出所有子集的异或总和再求和
c++·算法·leetcode·职场和发展·深度优先
00后程序员张11 小时前
从零构建 gRPC 跨语言通信:C++ 服务端与
开发语言·c++