深入理解C++11(十)(1)

深入理解C++11(十)

C++11之前,C++语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多不便。现在C++11增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。
线程的创建
用std::thread创建线程非常简单,只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。下面是创建线程的示例:

#include <thread>
void func()
{
// do some work
}
int main()
{
std::thread t(func);
t.join();
return0;
}

在上例中,函数func将会运行于线程对象t中,join函数将会阻塞线程,直
到线程函数执行结束,如果线程函数有返回值,返回值将被忽略。

如果不希望线程被阻塞执行,可以调用线程的detach()方法,将线程和线程对象分离。比如下面的例子:

#include <thread>
void func()
{
// do some work
}
int main()
{
std::thread t(func);
t.detach();
// 做其他的事情
...
return0;
}

通过detach,线程就和线程对象分离了,让线程作为后台线程去执行,当
前线程也不会阻塞了。但需要注意的是,detach之后就无法再和线程发生联系
了,比如detach之后就不能再通过join来等待线程执行完,线程何时执行完我
们也无法控制了。

线程还可以接收任意个数的参数:

void func(int i, double d, const std::string& s)
{
std::cout << i << ", "<< d << ", "<< s << std::endl;
}
int main()
{
std::thread t(func, 1, 2, "test");
t.join();
return0;
}

上面的例子将会输出:
12,
test

使用这种方式创建线程很方便,但需要注意的是,std::thread出了作用域之后将会析构,这时如果线程函数还没有执行完则会发生错误,因此,需要保证线程函数的生命周期在线程变量std::thread的生命周期之内。

线程不能复制,但可以移动,例如:

#include <thread>
void func()
{
// do some work
}
int main()
{
std::thread t(func);
std::thread t1(std::move(t));
t.join();
t2.join();
return 0;
}

线程被移动之后,线程对象t将不再不代表任何线程了。另外,还可以通过std::bind或lambda表达式来创建线程,代码如下:

void func(int a,double b)
{
}
int main()
{
std::thread t1(std::bind(func, 1, 2));
std::thread t2([](int a, double b){}, 1,2);
t1.join();
t2.join();
return 0;
}

需要注意的是线程对象的生命周期,比如下面的代码:

#include <thread>
void func()
{
// do some work
}
int main()
{
std::thread t(func);
return0;
}

上面的代码运行可能会抛出异常,因为线程对象可能先于线程函数结束
了,应该保证线程对象的生命周期在线程函数执行完时仍然存在。

可以通过join方式来阻塞等待线程函数执行完,或者通过detach方式让线程在后台执行,还可以将线程对象保存到一个容器中,以保证线程对象的生命周期。比如下面的代码:

#include <thread>
std::vector<std::thread> g_list;
std::vector<std::shared_ptr<std::thread>> g_list2;
void CreateThread()
{
std::thread t(func);
g_list.push_back(std::move(t));
g_list2.push_back(std::make_shared<std::thread>(func));
}
int main()
{
CreateThread();
for (auto& thread : g_list)
{
thread.join();
}
for (auto& thread : g_list2)
{
thread->join();
}
return 0;
}

线程的基本用法
线程可以获取当前线程的ID,还可以获取CPU核心数量,例如:

void func()
{
}
int main()
{
std::thread t(func);
cout<<t.get_id()<<endl; // 获取当前线程
ID
// 获取CPU核数,如果获取失败则返回0
cout<< std::thread::hardware_concurrency()<<endl;
return0;
}

可以使当前线程休眠一定时间,代码如下:

void f(){
std::this_thread::sleep_for(std::chrono::seconds(3));
cout<<”
time out”
<<endl;
}
int main(){
std::thread t(f);
t.join();
}

在上面的例子中,线程将会休眠3秒,3秒之后将打印time out。

互斥量是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据。
C++11中提供了如下4种语义的互斥量(mutex):

std::mutex:独占的互斥量,不能递归使用。
std::timed_mutex:带超时的独占互斥量,不能递归使用。
std::recursive_mutex:递归互斥量,不带超时功能。
std::recursive_timed_mutex:带超时的递归互斥量。

独占互斥量std::mutex
这些互斥量的基本接口很相似,一般用法是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务之后,就必须使用unlock()来解除对互斥量的占用,lock()和unlock()必须成对出现。try_lock()尝试锁定互斥量,如果成功则返回true,如果失败则返回false,它是非阻塞的。std::mutex的基本用法如代码清单5-1所示:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex g_lock;
void func()
{
g_lock.lock();
std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;
g_lock.unlock();
}
int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
t1.join();
t2.join();
t3.join();
return 0;
}

输出结果如下:
entered thread 10144
leaving thread 10144
entered thread 4188
leaving thread 4188
entered thread 3424
leaving thread 3424

使用lock_guard可以简化lock/unlock的写法,同时也更安全,因为lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock操作,因此,应尽量用lock_guard。lock_guard用到了RAII技术,这种技术在类的构造函数中分配资源,在析构函数中释放资源,保证资源在出了作用域之后就释放。上面的例子使用lock_guard后会更简洁,代码如下

void func()
{
std::lock_guard< std::mutex> locker(g_lock); // 出作用域之后自动解锁
std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;
}

递归互斥量std::recursive_mutex
递归锁允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。在代码清单5-2中,一个线程多次获取同一个互斥量时会发生死锁。

struct Complex {
std::mutex mutex;
int i;
Complex() : i(0) {}
void mul(int x){
std::lock_guard<std::mutex> lock(mutex);
i *= x;
}
void div(int x){
std::lock_guard<std::mutex> lock(mutex);
i /= x;
}
void both(int x, int y){
std::lock_guard<std::mutex> lock(mutex);
mul(x);
div(y);
}
};
int main(){ 
Complex complex; 
complex.both(32, 23); 
return 0; 
}

这个例子运行起来后就会发生死锁,因为在调用both时获取了互斥
量,之后再调用mul又要获取相同的互斥量,但是这个互斥量已经被当前线程
获取了,无法释放,这时就会发生死锁。

要解决这个死锁的问题,一个简单的办法就是用递归锁:std::recursive_mutex,它允许同一线程多次获得互斥量,如代码清单5-3所示:

struct Complex {
std::recursive_mutex mutex;
int i;
Complex() : i(0) {}
void mul(int x){
std::lock_guard<std::recursive_mutex> lock(mutex);
i *= x;
}
void div(int x){
std::lock_guard<std::recursive_mutex> lock(mutex);
i /= x;
}
void both(int x, int y){
std::lock_guard<std::recursive_mutex> lock(mutex);
mul(x);
div(y);
}
};
int main(){
Complex complex;
complex.both(32, 23);// 因为同一线程可以多次获取同一互斥量,不会发生死锁
return 0;
}

需要注意的是尽量不要使用递归锁好,主要原因如下:

1)需要用到递归锁定的多线程互斥处理往往本身就是可以简化的,允许
递归互斥很容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩
问题。
2)递归锁比起非递归锁,效率会低一些。
3)递归锁虽然允许同一个线程多次获得同一个互斥量,可重复获得的最
大次数并未具体说明,一旦超过一定次数,再对lock进行调用就会抛出std::
system错误。

带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
std::timed_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用在获取锁时增加超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直在等待获取互斥量,就设置一个等待超时时间,在超时后还可以做其他的事情

std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until,这两个接口是用来设置获取互斥量的超时时间,使用时可以用一个while循环去不断地获取互斥量。std::timed_mutex的基本用法如代码清单5-4所示。

std::timed_mutex mutex;
void work(){
std::chrono::milliseconds timeout(100);
while(true){
if(mutex.try_lock_for(timeout)){
std::cout << std::this_thread::get_id() << ": do work with the mutex"
<< std::endl;
std::chrono::milliseconds sleepDuration(250);
std::this_thread::sleep_for(sleepDuration);
mutex.unlock();
std::this_thread::sleep_for(sleepDuration);
} else {
std::cout << std::this_thread::get_id() << ": do work without mutex"
<< std::endl;
std::chrono::milliseconds sleepDuration(100);
std::this_thread::sleep_for(sleepDuration);
}
}
}
int main(){
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
return 0;
}

在上面的例子中,通过一个while循环不断地去获取超时锁,如果超时还
没有获取到锁时就休眠100毫秒,再继续获取超时锁。

相比std::timed_mutex,std::recursive_timed_mutex多了递归锁的功能,允许同一线程多次获得互斥量。std::recursive_timed_mutex和std::recursive_mutex的用法类似,可以看作在std::recursive_mutex的基础上加了超时功能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值