1

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

我决定检查没有互斥锁的线程是否真的如此危险(好吧,我相信书,但更喜欢亲眼看到它)。作为“我将来不应该做的事情”的测试用例,我创建了相同概念的 2 个版本:有 2 个线程,其中一个将一个数字递增数次(NUMBER_OF_ITERATIONS),另一个将相同的数字递减相同的数量次,因此我们希望在代码执行后看到与之前相同的数字。附上代码。

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

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

我还测量了“安全”和“不安全”部分花费的时间。正如我所料,对于大量迭代,安全部分花费的时间比不安全部分要多得多:互斥检查花费了一些时间。但是对于少量迭代(400、4000),安全部分执行时间小于不安全时间。为什么会这样?它是操作系统所做的事情吗?或者编译器是否有一些我不知道的优化?我花了一些时间思考它,并决定在这里问。

我使用 windows 和 MSVS12 编译器。

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

主文件

#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;
}
4

1 回答 1

0

如果输入大小非常小,则不应就任何给定算法的速度得出结论。什么定义“非常小”可以是任意的,但在现代硬件上,在通常情况下,“小”可以指任何小于几十万个对象的集合,“大”可以指任何大于该大小的集合.

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

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

更广泛的一点是,在测试算法速度时,您应该始终更喜欢更大的输入,并且还要让相同的程序重复其测试(最好以随机顺序!)以尝试“消除”时序中的不规则性。

于 2017-10-11T20:52:46.610 回答