【和春笋一起学C++】(四十五)多线程编程中的线程安全问题
在C++编程中,经常会用到多线程编程以提高程序的运行效率,使用多线程编程时最重要的是要解决线程安全问题。
非线程安全(Non-Thread-Safe):代码在多线程并发访问时可能导致数据竞争或状态不一致。这类代码或对象在单线程环境下可以正常工作,但在多线程环境下需要额外的同步机制(如锁、原子操作等)来保证安全性。
在上篇文章《【和春笋一起学C++】(四十四)随机行走理论——编程实现》中使用的rand()函数也是非线程安全的,在单线程程序中使用是没有问题的,但在多线程程序中则会导致数据竞争问题。因rand()函数内部使用静态变量存储状态,多线程使用rand()函数势必会导致内部状态竞争,导致其状态被破坏。解决办法是不用rand()函数来产生随机数,用<random>库来代替,因为<random>库是线程安全的。
将随机行走理论的程序改成多线程程序,对比单线程的程序,看看运行效率提升了多少。
使用上篇文章 《【和春笋一起学C++】(四十四)随机行走理论——编程实现》中的Cvector类,单线程程序的代码如下:
#include <iostream>
#include <cmath>
#include <random>
#include "Cvector.h"
#include <opencv2/opencv.hpp>
using namespace cv;std::vector<int> vector_steps;
std::ofstream outf("steps.txt");void GetSteps()
{using namespace std;///获取非确定性种子std::random_device rd;///组合多个种子,使随机性更好std::seed_seq seed{ rd(), rd(), rd() };///初始化随机数引擎std::mt19937 gen(seed);///定义均匀分布范围[1, 360]std::uniform_int_distribution<int> dist(1, 360);///生成随机数Cvector initial_v;int angle;int total_steps = 0;for (int i = 0; i < 200000; i++){///获取随机角度angle = dist(gen);Cvector rand_angle(2, angle);initial_v = initial_v + rand_angle;if (sqrt(initial_v.x*initial_v.x + initial_v.y*initial_v.y) > 50){total_steps = i + 1;break;}}vector_steps.push_back(total_steps);outf << total_steps << endl;return;
}int main()
{using namespace std;double time_start;double time_end;time_start = (double)getTickCount();for (int i = 0; i < 1000; i++){GetSteps();}outf.close();time_end = (double)getTickCount();cout << "计算模块耗时: " << (time_end - time_start) * 1000 / getTickFrequency() << "毫秒" << endl;return 0;
}
单线程程序的运行时间为232ms,将上述代码改成多线程,如下:
#include <iostream>
#include <cmath>
#include <random>
#include <thread>
#include <mutex>
#include "Cvector.h"
#include <opencv2/opencv.hpp>
using namespace cv;std::vector<int> vector_steps;
std::ofstream outf("steps.txt");
std::mutex mtx;
void GetSteps()
{using namespace std;///获取非确定性种子std::random_device rd;///组合多个种子,使随机性更好std::seed_seq seed{ rd(), rd(), rd() };///初始化随机数引擎std::mt19937 gen(seed);///定义均匀分布范围[1, 360]std::uniform_int_distribution<int> dist(1, 360);///生成随机数Cvector initial_v;int angle;int total_steps = 0;for (int i = 0; i < 200000; i++){///获取随机角度angle = dist(gen);Cvector rand_angle(2, angle);initial_v = initial_v + rand_angle;if (sqrt(initial_v.x*initial_v.x + initial_v.y*initial_v.y) > 50){total_steps = i + 1;break;}}lock_guard<mutex> lock(mtx);///进入作用域时自动加锁vector_steps.push_back(total_steps);// 临界区操作outf << total_steps << endl;// 临界区操作return;
}///离开作用域时自动解锁int main()
{using namespace std;double time_start;double time_end;time_start = (double)getTickCount();for (int i = 0; i < 200; i++){thread t1(GetSteps);thread t2(GetSteps);thread t3(GetSteps);thread t4(GetSteps);thread t5(GetSteps);t1.join();t2.join();t3.join();t4.join();t5.join();}outf.close();time_end = (double)getTickCount();cout << "多线程计算模块耗时: " << (time_end - time_start) * 1000 / getTickFrequency() << "毫秒" << endl;return 0;
}
程序说明:
程序创建了5个线程,所以for循环只需要循环200次就能得到1000个数据,多线程的程序运行时间为137ms。可以打开输出文件steps.txt确认下数据数目是否为1000个。上述多线程程序和单线程程序运行的结果是相同的,但多线程比单线程节省了大约41%的时间(注:此处的运行时间因计算机的性能不同而有所差异)。
在多线程编程中,除了内部状态竞争(rand()函数)会引起线程安全问题,数据竞争也会引起线程安全问题。数据竞争指的是多个线程同时读写同一共享变量,且没有同步机制时,会导致数据竞争。如上述程序中的vector_steps和outf都是共享变量,在多线程并发访问这两个变量时,就会导致数据竞争。通过使用互斥锁(Mutex)保护共享数据,从而避免数据竞争导致线程安全问题。
上面多线程程序中lock_guard<mutex> lock(mtx);语句用于自动管理互斥锁生命周期,上述代码中通过lock_guard包裹临界区代码,防止数据竞争。
lock_guard是C++11 引入的模板类,构造时自动调用 mtx.lock()锁定互斥量;析构时(离开作用域时)自动调用mtx.unlock()解锁,确保锁不会遗漏释放。