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" 时间上的不规则性.
我是 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" 时间上的不规则性.