本文讨论C++11 中如何创建和使用多线程,以及如何解决资源进程、数据同步等问题。
在C ++ 11中,我们可以通过创建std :: thread类的对象来创建其他线程。每个std :: thread 对象都可以与一个线程关联。
三种方式创建线程
可以在thread对象上附加一个回调,该回调将在新线程启动时执行。这些回调可以是函数指针、函数对象、Lambda函数。
新线程将在创建新thread对象后立即启动,并将与启动它的线程并行执行传递的回调。而且,任何线程都可以通过在该线程的对象上调用 join() 函数来等待另一个线程退出。
//普通函数
void threadFunc(){
for (int i = 0; i < 1000; i++) {
printf("FuncPointer %d\n", i);
}
return;
}
//Lambda函数
auto LambdaFunc = [](){
for (int i = 0; i < 1000; i++){
printf("Lambda %d\n", i);
}
};
//仿函数回调
class OBJFunc {
public:
void operator() (){
for (int i = 0; i < 1000; i++){
printf("Object %d\n", i);
}
}
}objFunc;
int main(){
thread FuncThread(threadFunc);
thread LambdaThread(LambdaFunc);
thread ObjThread(objFunc);
LambdaThread.join();
FuncThread.join();
ObjThread.join();
return 0;
}
区分不同的线程
每个thread对象都有一个关联的ID,我们可以使用成员函数获取相关线程对象的ID,也可以获取当前线程使用的ID。
std::thread::get_id() //成员函数的id
std::this_thread::get_id() //当前线程的id
void threadFunc(){
printf("threadFunc id= %d\n", this_thread::get_id());
_sleep(1000);
cout << "threadFunc return" << endl;
return;
}
int main(){
printf("Main() id= %d\n", this_thread::get_id());
thread FuncThread(threadFunc);
printf("FuncThread.get_id() = %d\n", FuncThread.get_id());
FuncThread.join();
return 0;
}
通过调用join()
成员函数,可以另一个线程等待另外一个线程。它会造成堵塞直至该thread 对象结束。
通过调用detach()
成员函数,可以实现进程分离,分离的进程也称为守护进程或后台进程。调用detach() 之后,thread对象不再与实际的执行线程关联。
注意事项1:不要在已结束的线程对象上调用join()
或 detach
,否则将会导致程序终止。因此,在调用join()
或detach()
之前,我们应该使用joinable()
成员函数检查线程是否可连接。
thread FuncThread(threadFunc);
FuncThread.join();
if (FuncThread.joinable()){
FuncThread.join();
} else {
cout << "Can't not join" << endl;
}
注意事项2:不要忘记在进程结束之前调用join()
或 detach()
,否则将会导致程序崩溃。为防止这种情况,可以使用"资源获取初始化" (RESOURCE ACQUISITION IS INITIALIZATION` (RAII))。
class ThreadRAII{
thread & m_thread;
public:
ThreadRAII(std::thread & threadObj) : m_thread(threadObj){ }
~ThreadRAII(){
if (m_thread.joinable()){
m_thread.detach();
}
}
};
void threadFunc(){
_sleep(1000);
return;
}
int main(){
thread FuncThread(threadFunc);
ThreadRAII wrapperObj(FuncThread); //注释这一行将导致运行错误
return 0;
}
要将参数传递给线程关联的回调函数,只需要将参数传递给thread 的构造函数。默认情况下,所有参数都将复制到新线程的内部存储中。但是传参是需要注意一些基本事项。
传参示例
void threadFunc(string name){
printf("%s id= %d\n",name.c_str(), this_thread::get_id());
_sleep(1000);
return;
}
int main(){
thread FuncThread(threadFunc, "New_thread");
FuncThread.join();
return 0;
}
注意事项:不要将地址变量从本地堆栈传递到线程的回调函数中。这种情况下,可能导致访问无效地址而导致意外。
传递引用类型
使用普通的传递引用方法,新线程内进行的更改外部不可见,因为新线程中引用了堆栈上复制到临时值。因此正确方法是使用std::ref()
void threadFunc(int &num){
num *= 10;
return;
}
int main(){
int number = 10;
thread FuncThread(threadFunc, number);
FuncThread.join();
cout << number << endl; //10
thread FuncThread2(threadFunc, ref(number)); //正确方法
FuncThread2.join();
cout << number << endl; //100
return 0;
}
传入类成员函数
方法是以类成员函数的指针作为回调函数,以对象的指针作为第二个参数。
class ThreadClass {
public :
void threadFunc(string name){
cout << name << endl;
}
};
int main(){
ThreadClass threadClass;
thread FuncThread(&ThreadClass::threadFunc, &threadClass, "ThreadClass");
FuncThread.join();
return 0;
}
关于数据竞争
在多线程环境中,线程之间的数据共享非常容易。但是,这种易于共享的数据可能会导致应用程序出现问题,其中一个问题就是竞争条件。竞争条件是一种在多线程应用程序中发生的错误,当两个或多个线程并行执行一组操作时,它们将访问同一内存位置。而且,其中一个或多个线程会修改该内存位置中的数据,这有时会导致意外结果。竞赛条件通常不会每次都出现,因此通常很难找到和复制。仅当两个或多个线程的相对执行顺序导致意外结果时,它们才会发生。
以下是一个数据竞争的例子。
class ThreadClass {
int value = 0;
public :
void increase(){
for (int i = 0; i < 100000; i++){
value++;
}
}
int getValue(){
return value;
}
};
int main(){
vector<thread> threadVec;
ThreadClass threadObj;
for (int i = 0; i < 5; i++){
threadVec.push_back(thread(&ThreadClass::increase, &threadObj));
}
for (int i = 0; i < 5; i++){
threadVec.at(i).join();
}
cout << threadObj.getValue() << endl; //279853
return 0;
}
为了解决这个问题,我们需要使用Lock机制,即每个线程需要在修改或读取共享数据之前获取一个锁,并且在修改数据之后,每个线程都应该解锁该锁。
互斥锁解决数据竞争问题
为了解决多线程环境中的竞争条件,我们需要互斥锁,即每个线程都需要在修改或读取共享数据之前锁定互斥锁,并且在修改数据之后,每个线程都应解锁互斥锁。
std::mutex
在C ++ 11线程库中,互斥锁位于头文件中。代表互斥量的类是std :: mutex类。互斥锁有两种重要的方法: lock() , unlock() 。使用互斥锁完善ThreadClass类:
class ThreadClass {
int value = 0;
mutex mux;
public :
void increase(){
mux.lock();
for (int i = 0; i < 100000; i++){
value++;
}
mux.unlock();
}
};
std::lock_guard
使用 lock_gurad 是为了防止出现忘记解锁互斥锁的情况。std :: lock_guard是一个类模板,它为互斥量实现RAII。它将互斥体包装在其对象内部,并将附加的互斥体锁定在其构造函数中。调用析构函数时,它将释放互斥量。
class ThreadClass {
int value = 0;
mutex mux;
public :
void increase(){
lock_guard<mutex> lockGuard(mux);
for (int i = 0; i < 100000; i++){
value++;
}
}
};
一些线程之前存在着依赖关系,例如加载文件和读写文件,这就以为着某些情况下需要对线程的运行时机和顺序进行控制。通常有两种方案:全局布尔变量和条件变量。
方案1:全局布尔变量
将布尔全局变量设为默认值false。在线程2中将其值设置为true,线程1将继续在循环中检查其值,并且一旦变为true,线程1将继续处理数据。但是由于它是两个线程共享的全局变量,因此需要与互斥锁同步。注意这种方法消耗许多CPU周期在检查bool值上,不建议使用。
class ThreadClass {
int value = 0;
mutex mux;
bool ok = false;
public :
void increase(){
lock_guard<mutex> lockGuard(mux);
for (int i = 0; i < 100000; i++){
value++;
}
ok = true;
}
void devide(){
while (!ok){
_sleep(100);
}
lock_guard<mutex> lockGuard(mux);
value /= 2;
}
int getValue(){
return value;
}
};
int main(){
vector<thread> threadVec;
ThreadClass threadObj;
//先加后除
threadVec.push_back(thread(&ThreadClass::increase, &threadObj));
threadVec.push_back(thread(&ThreadClass::devide, &threadObj));
for_each(threadVec.begin(), threadVec.end(), [](thread &i){i.join(); });
cout << threadObj.getValue() << endl; //50000
return 0;
}
方案2 : 条件变量
条件变量是一种用于在两个线程之间进行信号传递的事件。一个线程可以等待它发出信号,而另一个线程可以发出信号。C ++ 11中的条件变量所需的头文件是#include
。
主要成员函数介绍
wait()
它使当前线程阻塞,直到信号通知条件变量或发生虚假唤醒为止。
它以原子方式释放附加的互斥锁,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中。当某些线程在同一条件变量对象上调用notify_one()或notify_all()时,该线程将被解除阻塞。它也可能会被虚假地解除阻塞,因此,每次解除阻塞后,都需要再次检查条件。
回调将作为参数传递给此函数,该函数将被调用以检查它是否是虚假调用或是否实际满足条件。
notify_one()
如果有任何线程在同一条件变量对象上等待,则notify_one解除阻塞其中一个正在等待的线程。
notify_all()
如果有任何线程在同一条件变量对象上等待,则 notify_all 取消阻止所有正在等待的线程。
class ThreadClass {
int value = 0;
mutex mux;
condition_variable condition;
bool ok = false;
public :
void increase(){
mux.lock();
for (int i = 0; i < 100000; i++){
value++;
}
mux.unlock();
ok = true;
condition.notify_one(); //发出信号
}
bool isOk(){
return ok;
}
void devide(){
unique_lock<mutex> mlock(mux);
condition.wait(mlock, bind(&ThreadClass::isOk, this));
value /= 2;
}
int getValue(){
return value;
}
};
线程返回结果,旧有的方法是通过指针在线程之前共享数据,当新线程设置了数据并向控制变量发出信号时,主线程唤醒并从该指针获取数据。因此通过指针共享数据的方法需要用到条件变量、互斥量、和指针,使得问题变得更加复杂。C++11 提供了更好的选择:使用std::future 和 std::promise。
一个std :: promise对象与其关联的std :: future对象共享数据。
std :: future是一个类模板,其对象存储将来的值。使用get()成员函数来访问该值,如果尝试在 get() 函数可用之前访问future的值,则get() 函数将阻塞直到该值可用。
std :: promise也是一个类模板,其对象承诺将来会设置该值。每个std :: promise对象都有一个关联的std :: future对象,一旦该值由std :: promise对象设置,它将给出该值。
class ThreadClass{
public:
void func(promise<string> * promObj){
_sleep(2000);
promObj->set_value("OK");
}
}threadObj;
int main(){
std::promise<string> promiseObj;
std::future<string> futureObj = promiseObj.get_future();
thread myThread (&ThreadClass::func, &threadObj, &promiseObj);
string res = futureObj.get();
cout << res << endl;
myThread.join();
return 0;
}
std :: async() 是一个函数模板,它接受回调作为参数,并执行它们。async返回一个 future ,该值存储由回调对象的返回值。回调所需的参数可以在函数指针参数之后作为参数传递给async()。async() 中的第一个参数是启动策略,它控制async的异步行为,有三种可选:
std::launch::async
保证异步行为,即传递的函数将在单独的线程中执行。
std::launch::deferred
非异步行为,即当其他线程调用future对象的get() 以访问共享状态时执行回调函数。
std::launch::async | std::launch::deferred
默认行为。异步运行或不异步运行,具体取决于系统上的负载。我们无法控制它。
在下面实例中,std::async() 做了以下事情:创建一个线程以及一个promise对象;将promise对象传给线程回调函数并返回一个关联的future对象;当传递的参数退出时,将返回值设置与promise对象中。
string getResult(string seed){
reverse(seed.begin(), seed.end());
std::this_thread::sleep_for(std::chrono::seconds(5));
return seed;
}
int main(){
future<string> futureObj = async(std::launch::async, &getResult, "It_is_seed");
cout << futureObj.get() << endl;
return 0;
}
std :: packaged_task <>是类模板,代表异步任务。它封装了
string getResult(string seed){
reverse(seed.begin(), seed.end());
std::this_thread::sleep_for(std::chrono::seconds(5));
return seed;
}
int main(){
packaged_task<string(string)> task(&getResult);
future<string> result = task.get_future();
//std :: packaged_task <>是可移动的,但不可复制,因此需要用std::move()传到线程.
thread mythread(move(task), "It_is_a_task");
cout << result.get() << endl;
mythread.join();
return 0;
}