C++11 多线程

本文讨论C++11 中如何创建和使用多线程,以及如何解决资源进程、数据同步等问题。

C++ 11 多线程

文章目录

  • C++ 11 多线程
      • 创建和使用线程
      • 连接和分离线程
      • 关于参数传递
      • 数据竞争问题与解决
      • 顺序控制
      • 线程返回值
      • std::async
      • std::packaged_task
      • 注意事项

创建和使用线程

在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

  • 主要成员函数介绍

    1. wait()

      它使当前线程阻塞,直到信号通知条件变量或发生虚假唤醒为止。

      ​ 它以原子方式释放附加的互斥锁,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中。当某些线程在同一条件变量对象上调用notify_one()或notify_all()时,该线程将被解除阻塞。它也可能会被虚假地解除阻塞,因此,每次解除阻塞后,都需要再次检查条件。

      ​ 回调将作为参数传递给此函数,该函数将被调用以检查它是否是虚假调用或是否实际满足条件。

    2. notify_one()

      如果有任何线程在同一条件变量对象上等待,则notify_one解除阻塞其中一个正在等待的线程。

    3. 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

std :: async() 是一个函数模板,它接受回调作为参数,并执行它们。async返回一个 future ,该值存储由回调对象的返回值。回调所需的参数可以在函数指针参数之后作为参数传递给async()。async() 中的第一个参数是启动策略,它控制async的异步行为,有三种可选:

  1. std::launch::async

    保证异步行为,即传递的函数将在单独的线程中执行。

  2. std::launch::deferred

    非异步行为,即当其他线程调用future对象的get() 以访问共享状态时执行回调函数。

  3. 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

std :: packaged_task <>是类模板,代表异步任务。它封装了

  1. 可调用实体,即函数,lambda函数或函数对象。
  2. 一种共享状态,用于存储由关联的回调返回或引发的异常的值。
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;
}

注意事项

  • 使用多线程时在Linux上编译: g ++ –std = c ++ 11 sample.cpp -lpthread
  • 要求的编译器: Linux:gcc 4.8.1(完全并发支持)Windows:Visual Studio 2012和MingW。

你可能感兴趣的:(C++,11新特征,C++)