为什么手动对互斥锁进行加锁会使第二个线程的计数器增加,而lock_guard却做不到这一点?

编程语言 2026-07-07
#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>
using namespace std;

int counter = 0;
mutex mtx;
void incrementCounter(){
    lock_guard<mutex> lock(mtx);   
    // 调用 mtx.lock();
    counter += 1;
    // 调用 mtx.unlock();
    this_thread::sleep_for(chrono::milliseconds(2));
}

int main(){
    thread t1(incrementCounter);
    cout<<"Thread one released "<<counter<<"\n";
    thread t2(incrementCounter);
    cout<<"thread two released "<<counter<<"\n";
    t1.join();
    t2.join(); 
    return 0;
}

当我在互斥锁上使用 lock_guard 时,counter只增加了一次,而当我使用手动加锁(lock()unlock())时,两个线程都对counter进行了自增,因此最终输出为2。

解决方案

通用

首先需要理解的一点是,你的程序有三个线程,而不仅仅是两个。每个程序都有一个初始线程,它进入 main(),而你的程序在此基础上再多创建了两个线程。初始线程在数据竞争防护方面的要求与其他任何线程相同。它们在程序中只有一个线程时虽然不再需要关注,但在多线程程序中你仍需关注它们。

另外需要理解的一点是,不同线程之间执行操作的相对顺序并不可靠地可预测,除非你的程序实现了对其的控制。线程的创建和连接(join)是对这些控制中最强、最粗粒度的方式,但并不是唯一的方法。

第三点你需要理解的是,线程同步以及C++内存模型并不关心时序,而是关注可见性(对内存写入对内存读取的可见性)。引入延迟从来都不是解决线程同步问题的正确方法。

你的程序

你的 lock_guard 使用互斥量 mtx 来保护 incrementCounter() 中对 counter 的访问,但这还不够。初始线程也会访问 counter(为了打印它的值)。这些读取与其他两个线程所进行的更新“冲突”,不同线程之间对这些冲突操作没有任何同步,因此你的程序存在数据竞争。因此它的行为是未定义的。

此外,即使你在 main() 中通过对 mtx 加锁来保护对 counter 的读取,也不足以确保程序的输出像它声称的那样反映操作的时序。也就是说,考虑这个对 main() 的替代版本:

int main(){
    thread t1(incrementCounter);
    {
        lock_guard<mutex> lock(mtx);
        cout<<"Thread one released "<<counter<<"\n";
    }
    thread t2(incrementCounter);
    {
        lock_guard<mutex> lock(mtx);
        cout<<"thread two released "<<counter<<"\n";
    }
    t1.join();
    t2.join(); 
    return 0;
}

这可以消除原程序中的数据竞争,但仍然存在竞争条件,意味着程序对操作顺序的期望不是C++保证会实现的。就C++而言,生成的程序可能输出:

Thread one released 1
thread two released 2

,这被我视为期望输出,但它也可能输出以下任意一种:

Thread one released 0
thread two released 0

Thread one released 0
thread two released 1

Thread one released 0
thread two released 2

Thread one released 1
thread two released 1

Thread one released 2
thread two released 2

额外的变体

在你的具体程序中,你可以选择使用 join() 来提供同步与操作顺序。对一个线程调用 join() 会与该线程的所有操作同步,因此你可以考虑如下:

int main(){
    thread t1(incrementCounter);
    thread t2(incrementCounter);
    t1.join();
    {
        // 仍需要互斥保护,因为还有 t2
        lock_guard<mutex> lock(mtx);
        cout<<"Thread one released "<<counter<<"\n";
    }
    t2.join();
    // 不再需要互斥保护,因为当前只有主线程在运行
    cout<<"thread two released "<<counter<<"\n";
    return 0;
}

这样,你的程序仍然可能输出

Thread one released 1
thread two released 2

,但也可能输出:

Thread one released 2
thread two released 2

,因为t1与 t2对 counter的更新在主线程打印t1相关输出时的可见性,可能会产生上述变化。

如果你希望程序只有一个符合规范的输出同时仍然是多线程的,那么这是一个不错的选项:

int main(){
    thread t1(incrementCounter);
    thread t2(incrementCounter);
    t1.join();
    t2.join();
    // 不再需要互斥保护,因为当前只有一个线程在执行输出
    cout<<"Thread one released "<<counter<<"\n";
    cout<<"thread two released "<<counter<<"\n";
    return 0;
}

,但是它会输出:

Thread one released 2
thread two released 2

如果你希望输出能可靠地、分别地反映每个线程更新后的值,那么更好的做法之一是让执行更新的同一个线程也来输出,尽管这会带来自己的复杂性。如果你决心在任何临界区之外进行输出,那么大致可能像这样:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

int counter = 0;
std::mutex mtx;

void incrementCounter() {
    int result;

    {
        std::lock_guard<mutex> lock(mtx);   
        result = ++counter;
    }

    // cout 本身是线程安全的,但链式输出会带来排序问题
    // 还需要注意线程 ID 是“不透明”的
    std::string message = std::string("线程 ") + std::this_thread::get_id()
            + " 已释放 " + result + "\n";
    std::cout << message;

    // 不需要睡眠
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);
    std:thread::id t1_id = t1.get_id();
    std:thread::id t2_id = t2.get_id();
    t1.join();
    t2.join();

    std::cout << "线程一是 " << t1_id << '\n';
    std::cout << "线程二是 " << t2_id << '\n';

    return 0;
}

你也可以考虑对线程函数进行如下变体,在释放互斥锁前报告更新后的counter值:

void incrementCounter() {
    std::lock_guard<mutex> lock(mtx);   
    counter += 1;
    // cout 链式输出顺序问题通过互斥锁得到控制
    std::string message = std::string("线程 ") << std::this_thread::get_id()
            << " 将 counter 更新为 " << counter << "\n";
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章