Я новичок в использовании потоков в C ++. Я прочитал основы о std :: thread и mutex, и, похоже, я понимаю цель использования мьютексов.
Я решил проверить, действительно ли потоки так опасны без мьютексов (ну, я верю книгам, но предпочитаю видеть это своими глазами). В качестве тестового примера «что я не должен делать в будущем» я создал 2 версии одной и той же концепции: есть 2 потока, один из которых увеличивает число несколько раз (NUMBER_OF_ITERATIONS), другой уменьшает то же число на то же число раз, поэтому мы ожидаем увидеть то же число после выполнения кода, как и до него. Код прилагается.
Сначала я запускаю 2 потока, которые делают это небезопасным способом — без мьютексов, просто чтобы посмотреть, что может произойти. И после того, как эта часть закончена, я запускаю 2 потока, которые делают то же самое, но безопасным образом (с мьютексами).
Ожидаемые результаты: без взаимных исключений результат может отличаться от исходного значения, поскольку данные могут быть повреждены, если два потока работают с ним одновременно. Особенно это обычно для огромных NUMBER_OF_ITERATIONS — потому что вероятность повреждения данных выше. Так что этот результат я могу понять.
Также я измерил время, проведенное как «безопасными», так и «небезопасными» частями. Как и ожидалось, для огромного количества итераций безопасная часть тратит гораздо больше времени, чем небезопасная: на проверку мьютекса уходит некоторое время. Но для небольшого количества итераций (400, 4000) безопасное время выполнения детали меньше небезопасного. Почему это возможно? Это то, что делает операционная система? Или есть какая-то оптимизация компилятором, о которой я не знаю? Я провел некоторое время, думая об этом, и решил спросить здесь.
Я использую Windows и компилятор MSVS12.
Таким образом, возникает вопрос: почему безопасное выполнение части может быть быстрее, чем небезопасное выполнение первой части? (для небольших NUMBER_OF_ITERATIONS < 1000 * п)?
Еще один: почему это связано с NUMBER_OF_ITERATIONS: для меньших (4000) «безопасная» часть с мьютексами быстрее, а для огромных (400000) «безопасная» часть медленнее?
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;
}
Вы не должны делать выводы о скорости любого данного алгоритма, если размер ввода очень мал. То, что определяет «очень маленький», может быть произвольным, но на современном оборудовании в обычных условиях «маленький» может относиться к любому размеру коллекции, не превышающему несколько сотен тысяч объектов, а «большой» может относиться к любой коллекции, которая больше этого размера. ,
Очевидно, ваше молоко может меняться.
В этом случае накладные расходы на создание потоков, которые, как правило, медленны, также могут быть довольно противоречивыми и могут быть более значительным фактором в скорости вашего кода, чем то, что делает настоящий алгоритм. Вполне возможно, что компилятор имеет какие-то мощные оптимизации, которые он может выполнять для входных данных меньшего размера (о которых он может определенно знать из-за жестко заданного размера входного кода в самом коде), которые он затем не может выполнять на больших входных данных.
Суть в том, что при тестировании скорости алгоритма вы всегда должны отдавать предпочтение большим входным данным, а также повторять тесты одной и той же программы (желательно в случайном порядке!), Чтобы попытаться «сгладить» неровности во времени.
Других решений пока нет …