c++ 线程安全和时间效率:为什么带互斥锁检查的线程有时比不带它的速度更快?

c++ threads safety and time efficiency: why does thread with mutex check sometimes works faster than without it?

我是 c++ 中线程使用的初学者。我已经阅读了有关 std::thread 和互斥锁的基础知识,似乎我理解了使用互斥锁的目的。

我决定检查没有互斥量的线程是否真的如此危险(好吧,我相信书本,但更喜欢亲眼所见)。作为 "what I shouldn't do in future" 的测试用例,我创建了相同概念的 2 个版本:有 2 个线程,其中一个将数字递增几次 (NUMBER_OF_ITERATIONS),另一个将相同的数字递减相同的次数,因此我们希望在代码执行后看到与之前相同的数字。附上代码。

起初我 运行 2 个线程以不安全的方式执行它 - 没有任何互斥锁,只是为了看看会发生什么。在这部分完成后,我 运行 2 个线程以安全的方式(使用互斥锁)做同样的事情。

预期结果:如果没有互斥量,结果可能与初始值不同,因为如果两个线程同时处理数据可能会损坏。特别是对于巨大的 NUMBER_OF_ITERATIONS 来说很常见——因为损坏数据的可能性更高。所以这个结果我可以理解。

我还测量了 "safe" 和 "unsafe" 部分花费的时间。对于大量迭代,安全部分比不安全部分花费更多时间,如我所料:互斥检查花费了一些时间。但对于少量迭代(400、4000),安全部分执行时间小于不安全时间。为什么这可能?这是操作系统的功能吗?或者是否有一些我不知道的编译器优化?我想了想,决定在这里问一下。

我使用 windows 和 MSVS12 编译器。

因此问题是:为什么安全部分的执行速度可能比不安全的部分更快(对于小 NUMBER_OF_ITERATIONS < 1000*n)? 另一个:为什么它与 NUMBER_OF_ITERATIONS 相关:对于较小的 (4000) "safe" 具有互斥锁的部分更快,但对于较大的 (400000) "safe" 部分比较慢?

main.cpp

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <windows.h>
//
///change number of iterations for different results
const long long NUMBER_OF_ITERATIONS = 400;
//
/// time check counter
class Counter{
    double PCFreq_ = 0.0;
    __int64 CounterStart_ = 0;
public:
    Counter(){
        LARGE_INTEGER li;
        if(!QueryPerformanceFrequency(&li))
            std::cerr << "QueryPerformanceFrequency failed!\n";

        PCFreq_ = double(li.QuadPart)/1000.0;

        QueryPerformanceCounter(&li);
        CounterStart_ = li.QuadPart;
    }
    double GetCounter(){
        LARGE_INTEGER li;
        QueryPerformanceCounter(&li);
        return double(li.QuadPart-CounterStart_)/PCFreq_;
    }
};

/// "dangerous" functions for unsafe threads: increment and decrement number
void incr(long long* j){
    for (long long i = 0; i < NUMBER_OF_ITERATIONS; i++) (*j)++;
    std::cout << "incr finished" << std::endl;
}
void decr(long long* j){
    for (long long i = 0; i < NUMBER_OF_ITERATIONS; i++) (*j)--;
    std::cout << "decr finished" << std::endl;
}

///class for safe thread operations with incrment and decrement
template<typename T>
class Safe_number {
public:
    Safe_number(int i){number_ = T(i);}
    Safe_number(long long i){number_ = T(i);}
    bool inc(){
        if(m_.try_lock()){
            number_++;
            m_.unlock();
            return true;
        }
        else
            return false;
    }
    bool dec(){
        if(m_.try_lock()){
            number_--;
            m_.unlock();
            return true;
        }
        else
            return false;
    }
    T val(){return number_;}
private:
    T number_;
    std::mutex m_;
};

///
template<typename T>
void incr(Safe_number<T>* n){
    long long i = 0;
    while(i < NUMBER_OF_ITERATIONS){
        if (n->inc()) i++;
    }
    std::cout << "incr <T> finished" << std::endl;
}
///
template<typename T>
void decr(Safe_number<T>* n){
    long long i = 0;
    while(i < NUMBER_OF_ITERATIONS){
        if (n->dec()) i++;
    }
    std::cout << "decr <T> finished" << std::endl;
}

using namespace std;

// run increments and decrements of the same number
// in threads in "safe" and "unsafe" way
int main()
{
    //init numbers to 0
    long long number = 0;
    Safe_number<long long> sNum(number);

    Counter cnt;//init time counter
    //
    //run 2 unsafe threads for ++ and --
    std::thread t1(incr, &number);
    std::thread t2(decr, &number);
    t1.join();
    t2.join();
    //check time of execution of unsafe part
    double time1 = cnt.GetCounter();
    cout <<"finished first thr"  << endl;
    //
    // run 2 safe threads for ++ and --, now we expect final value 0
    std::thread t3(incr<long long>, &sNum);
    std::thread t4(decr<long long>, &sNum);
    t3.join();
    t4.join();
    //check time of execution of safe part
    double time2 = cnt.GetCounter() - time1;
    cout << "unsafe part, number = " << number << "  time1 = " << time1 << endl;
    cout << "safe part, Safe number = " << sNum.val() << "  time2 = " << time2 << endl << endl;

    return 0;
}

如果输入大小非常小,您不应该对任何给定算法的速度下结论。 "very small" 的定义可以是任意的,但在现代硬件上,在通常情况下,"small" 可以指代小于几十万个对象的任何集合大小,而 "large" 可以指代任何大于那个的集合。

显然,您的里程数可能会有所不同。

在这种情况下,构造线程的开销虽然通常很慢,但也可能相当不一致,并且可能是影响代码速度的一个比实际算法正在执行的更大的因素。编译器可能有一些强大的优化,它可以在较小的输入大小上执行(由于输入大小被硬编码到代码本身,它肯定知道),然后它不能在较大的输入上执行。

更广泛的观点是,在测试算法速度时,您应该始终更喜欢更大的输入,并且还让相同的程序重复其测试(最好以随机顺序!)以尝试 "smooth out" 时间上的不规则性.