Not Only Algorithm,不仅仅是算法,关注数学、算法、数据结构、程序员笔试面试以及一切涉及计算机编程之美的内容 。。
你的位置:NoAlGo博客 » 程序设计 » ,

C++11的多线程编程

C++11标准直接提供了并发编程的支持,这是C++新标准中非常重要的部分,可以极大的提高程序的可移植性。以前的多线程编程基本上依赖于特定的操作系统,比如Linux环境下使用pthread库。现在有了统一的接口进行实现,本文将通过几个简单的实例介绍常见的多线程编程的使用方法。

头文件

C++11标准中为了支持多线程编程引入了不同的头文件,各自分别负责不同的部分。

  • <atomic>:包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。
  • <thread>:包含std::thread类以及std::this_thread命名空间。
  • <mutex>:包含与互斥量(mutex)相关的类以及其他类型和函数。
  • <condition_variable>:包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。
  • <future>:包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。

多线程

C++11中使用多线程非常简单,直接使用头文件thread中的thread类型即可创建一个线程,具体例子如下所示,该程序是一个多线程版本的Hello World程序。
注意,这里使用了C++11中的lambda匿名函数,如果对此不是很了解,请先阅读 C++11中的lambda表达式

#include <iostream>
#include <thread>
#include <string>
using namespace std;

int main()
{
  //线程函数无参数
	thread t([](){ cout << "Hello World!" << endl; });
	t.join(); //t1线程调用t2.join(),表示t1要等到t2结束后才能继续执行
}

这里涉及到多线程环境中父子线程的关系问题。一般操作系统中,当主线程执行完毕退出时,无论子线程是否执行完毕,所有的子线程都会终止。而当子线程先退出时,无论是执行完毕正常退出还是以其他方式异常终止,线程均会进入终止态(僵死态),此时线程分配的系统资源还没有释放,线程仍作为一个线程实体存在于操作系统中。如果我们就这样不管这个子线程,那么它占用的系统资源(如动态申请的内存、打开的文件等)将一直被占据,造成资源的浪费。此时可以通过定义以下两种父子线程关系进行解决:

  • 可会和(joinable):主线程必须显示等待子线程,只有子线程结束后,主线程才继续执行后面的操作。
  • 相分离(detached):父子线程分离,主线程无须等待子线程完成,子线程一旦进入终止态后系统立即销毁线程以及回收资源。

以上的线程函数是无参数的,C++中还可以方便地给线程函数传递参数.

#include <iostream>
#include <thread>
#include <string>
using namespace std;

int main()
{
	//线程函数带参数,传值
	thread t([](string s){ cout << s << endl; }, "Hello World!");
	t.join();
}

以上带参数的线程函数是按值传递的,如果需要按引用传递,则使用方法稍微有点不同。

#include <iostream>
#include <thread>
#include <string>
using namespace std;

int main()
{
	int i = 0;
	//线程函数带参数,传引用
	thread t([](int &x){ cout << x++ << endl; }, ref(i)); //需要使用std::ref
	t.join();
	cout << i << endl;
}

thread头文件的this_thread命名空间定义了几个有用的函数:

  • get_id:返回当前的线程id。
  • yield:在处于等待状态时,可以让调度器先运行其他可用的线程。
  • sleep_for:阻塞当前线程,时间不少于参数指定的时间。
  • sleep_util:阻塞当前线程,时间为参数指定的时间。

简单的使用方法如下:

#include <iostream>
#include <thread>
#include <string>
using namespace std;

void f()
{
	cout << "Thread ID: " << this_thread::get_id() << endl;
	this_thread::sleep_for(chrono::seconds(3));
}

int main()
{
	thread t(f);
	t.join();
}

互斥锁

多线程编程一般避免不了同步的问题,C++11这里也提供了非常方便的方法来进行解决。标准中提供了一下四种互斥锁,分别是:

  • Mutex:基本的Mutex类,提供了核心函数lock()和unlock()。
  • Recursive_mutex:递归Mutex类,允许在同一个线程中对一个互斥量的多次请求。一般在项目模块分工中可以使用,这样即使其他人使用了同样的锁也不会导致死锁。
  • Timed_mutex:定时递归Mutex类,除了递归,还可以在某个时间段里或者某个时刻到达之间获取该互斥量。当一个线程在临界区操作的时间非常长,可以用定时锁指定时间。
  • Recursive_timed_mutex:定时递归Mutex类,综合timed_mutex和recuseive_mutex。

下面是一个使用基本锁的小例子,该程序会按顺序输出5对enter和leave,分别对应5个线程。如果注释掉函数f中mt的lock和unlock函数,则线程没有同步,此时先输出5句enter,然后5s后再输出5句leave。

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <vector>
using namespace std;

mutex mt;

void f()
{
	mt.lock();
	cout << "Enter Critical Section" << endl;
	this_thread::sleep_for(chrono::seconds(5));
	cout << "Leave Critical Section" << endl;
	mt.unlock();
}

int main()
{
	vector<thread> v(5);
	for (auto &i : v) i = thread(f);
	for (auto &i : v) i.join();
}

条件变量

条件变量condition_variable也可以进行线程之间的通信,当一个线程要等待另一个线程完成某个操作时,可以使用条件变量进行实现。条件变量可以将一个或多个线程进入阻塞状态,直到收到另外一个线程的通知,或者超时才能退出阻塞状态。
一个线程等待某个条件满足,其首先获得一个unique_lock锁。该锁将会传递给wait()方法,然后wait()方法会释放互斥量并将该线程暂停,直到条件变量得到相应的信号。当接受到信号,线程被唤醒后,该锁就又被重新获得了。
另外一个线程发送信号使得条件满足。其通过调用notify_one()来发送通知,会将处于阻塞状态的等待该条件获得信号的线程中的某一个线程(任意一个线程)恢复执行;还可以通过调用notify_all()将等待该条件的所以线程唤醒。

下面是一个使用条件变量的简单例子。该程序中f1等待某个条件,该条件在f2输出并且睡眠2s后才得到满足,之后f1才能够进行输出。

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
using namespace std;

mutex mt;
condition_variable cv;

void f1()
{
	unique_lock<mutex> lock(mt);
	cv.wait(lock);
	cout << "F1 says Hi" << endl;
}

void f2()
{
	cout << "F2 says Hi" << endl;
	this_thread::sleep_for(chrono::seconds(2));
	cv.notify_all();
}

int main()
{
	thread t1(f1); 
	this_thread::sleep_for(chrono::seconds(2));
	thread t2(f2); 
	t1.join(), t2.join();
}

Future

C++11中的future是标准库提供的一种用于获取异步操作的结果的机制,其可以调用一个函数,然后转而做其他的事情,让函数自己在一边执行,当需要的时候再回过头来获取该函数计算的结果。另外,其还可以延迟异步操作中异常(Exception)的抛出。
下面是一个简单的例子。

#include <iostream>
#include <future>
using namespace std;

int main()
{
	future<int> f = async([]()->int { return 42; });
	this_thread::sleep_for(chrono::seconds(2));
	cout << "The answer to life the universe and everything is: " << f.get() << endl;
}
上一篇: 下一篇:

我的博客

NoAlGo头像编程这件小事牵扯到太多的知识,很容易知其然而不知其所以然,但真正了不起的程序员对自己程序的每一个字节都了如指掌,要立足基础理论,努力提升自我的专业修养。

站内搜索

最新评论