c++ 多线程公共变量读写竞争 thread 加锁 mutex volatile

c++ 多线程

多线程共用的公共变量往往需要为了避免读写竞争而加锁,一切实现避免读写竞争的基础是基于cpu的原子操作。所以还有使用原子变量这种方法。

下面给出常见的几种避免读写竞争的写法

一般是用mutex,下面给出三种mutex的写法:

第一种 try_lock()

自己手动找位置加锁解锁。并且使用非阻塞的方式获取锁,(尝试在不阻塞的情况下获得互斥锁的所有权)。就是代码会立即返回true或false。

#include <iostream>
#include <stdio.h>
#include <thread>
#include <mutex>

using namespace std;

typedef long long LL;

int cnt = 0;
int add_times = 20000;
mutex my_mutex;

void thread1()
{
    for (LL i = 0; i < add_times; i++)
    {
        if(my_mutex.try_lock())
        {
            cnt++;
            my_mutex.unlock();
        }
        else i--;
    }

}

void thread2()
{
    for (LL i = 0; i < add_times; i++)
    {
        if(my_mutex.try_lock())
        {
            cnt++;
            my_mutex.unlock();
        }
        else i--;
    }
}

int main()
{
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    cout << cnt << endl;
    cout << add_times * 2 << endl;
    return 0;
}

第二种 lock()

自己手动找位置加锁解锁。使用阻塞的方式获取锁,(阻塞调用线程,直到线程获得互斥锁的所有权。它的返回值被忽略)。代码的返回值是void,这个函数会一直卡住,直到拿到锁才继续运行。

#include <iostream>
#include <stdio.h>
#include <thread>
#include <mutex>

using namespace std;

typedef long long LL;

int cnt = 0;
int add_times = 20000;
mutex my_mutex;

void thread1()
{
    for (LL i = 0; i < add_times; i++)
    {
        my_mutex.lock();
        cnt++;
        my_mutex.unlock();
    }

}

void thread2()
{
    for (LL i = 0; i < add_times; i++)
    {
        my_mutex.lock();
        cnt++;
        my_mutex.unlock();
    }
}

int main()
{
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    cout << cnt << endl;
    cout << add_times * 2 << endl;
    return 0;
}

第三种 std::lock_guard

使用一个std::lock_guard<std::mutex> guard(my_mutex);的临时变量。

相比于mutex功能,lock_guard具有创建时加锁,析构时解锁的功能,类似于智能指针,为了防止在线程使用mutex加锁后异常退出导致死锁的问题,建议使用lock_guard代替mutex。

这种也是阻塞式的锁,测试了一下,也容易导致一些线程长期霸占着锁。

个人感觉考虑效率的话,第一种非阻塞的方式应该更快。

鲁棒性的话,第三种锁可以自动析构解锁,会比第二种好一点。

#include <iostream>
#include <stdio.h>
#include <thread>
#include <mutex>

using namespace std;

typedef long long LL;

int cnt = 0;
int add_times = 20000;
mutex my_mutex;

void thread1()
{
    for (LL i = 0; i < add_times; i++)
    {
        std::lock_guard<std::mutex> guard(my_mutex);
        cnt++;
    }

}

void thread2()
{
    for (LL i = 0; i < add_times; i++)
    {
        std::lock_guard<std::mutex> guard(my_mutex);
        cnt++;
    }
}

int main()
{
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    cout << cnt << endl;
    cout << add_times * 2 << endl;
    return 0;
}

详细测试部分

vs的编译器对于thread的实现感觉有问题。

对于一个不加锁的多线程自增变量的代码,vs在自增量较小的时候完全不出错,但当数值大于某个值的时候就会出奇怪的错。

按理来说不加锁的话最后累加的结果应该每次都是不确定的。

测试代码如下:

#include <iostream>
#include <stdio.h>
#include <thread>

using namespace std;

typedef long long LL;

int cnt = 0;
int add_times = 200000;

void thread1()
{
    for (LL i = 0; i < add_times; i++)
        cnt++;
}

void thread2()
{
    for (LL i = 0; i < add_times; i++)
        cnt++;
}

int main()
{
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    cout << cnt << endl;
    cout << add_times * 2 << endl;
    return 0;
}

在vs中测试为:

200000
400000

但当累加次数小一点的时候就又会完全不出错。

40000
40000

用mingw测试结果为:

212187
400000
30824
40000

我认为mingw这种表现才算正常嘛。

一般来说想避免读写冲突,导致使用脏数据,我们需要对临界区(就是那部分需要访问公共变量的代码)进行加锁。

原子变量 解决读写竞争

除了使用锁,也可以使用原子变量做信号量,来解决读写竞争

#include <iostream>
#include <stdio.h>
#include <thread>
#include <atomic>

using namespace std;

typedef long long LL;

atomic_int cnt = 0;
int add_times = 200000;

void thread1()
{
    for (LL i = 0; i < add_times; i++)
        cnt++;
}

void thread2()
{
    for (LL i = 0; i < add_times; i++)
        cnt++;
}

int main()
{
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    cout << cnt << endl;
    cout << add_times * 2 << endl;
    return 0;
}
文章目录