C++11 并发编程教程&学习笔记

来源:互联网 发布:c语言二级考试选择题 编辑:程序博客网 时间:2024/06/06 04:56

C++11 并发编程教程&学习笔记

参考:

1、原文:http://baptiste-wicht.com/posts/2012/03/cpp11-concurrency-part1-start-threads.html

2、译文:http://billhoo.blog.51cto.com/2337751/1294190

​ 为了能够编译本文的示例代码,你需要有一个支持 C++11 的编译器,笔者使用的是 TDM-GCC4.9.2

并且需要添加 “-std=c++11” 或 “-std=c++0x” 编译选项以启动 GCC 对 C++11 的支持。win7+codeblocks的配置可见:http://blog.csdn.net/huhaijing/article/details/51753085

C++11 并发编程教程 - Part 1 : thread 初探

启动线程

​ 创建一个 std::thread 的实例时,便会自行启动。

​ 创建线程实例时,必须提供该线程将要执行的函数,方法之一是传递一个函数指针。

#include <thread>#include <iostream>using std::cout;using std::endl;using std::thread;void hello(){    cout << "Hello from thread" << endl;}int main(){    thread t1(hello);    t1.join(); // 直到t1结束后,main才返回,若不加该句,则可能t1未完成main就返回了    return 0;}

​ 所有的线程工具均置于头文件 thread 中。

​ 这个例子中值得注意的是对函数 join() 的调用。该调用将导致当前线程等待被 join 的线程结束(在本例中即线程 main 必须等待线程 t1 结束后方可继续执行)。如果你忽略掉对 join() 的调用,其结果是未定义的 —— 程序可能打印出 “Hello from thread” 以及一个换行,或者只打印出 “Hello from thread” 却没有换行,甚至什么都不做,那是因为线程main 可能在线程 t1 结束之前就返回了。

区分线程

 每个线程都有唯一的 ID 以便我们加以区分。使用 std::thread 类的 get_id() 便可获取标识对应线程的唯一 ID。

​ 使用 std::this_thread 来获取当前线程的引用。下面的例子将创建一些线程并使它们打印自己的 ID:

#include <thread>#include <iostream>#include <vector>using namespace std;void hello(){    // std::this_thread 当前线程的引用    // get_id()获取标识进程线程的唯一ID    cout << "Hello from thread" << this_thread::get_id() << endl;}int main(){    vector<thread> threads;    for(int i = 0; i < 5; ++i){        threads.push_back(thread(hello));    }    for(auto& th : threads){        th.join();    }    return 0;}

线程之间存在 interleaving ,某个线程可能随时被抢占。

又因为输出到 ostream 分几个步骤(首先输出一个 string,然后是 ID,最后输出换行),

因此一个线程可能执行了第一个步骤后就被其他线程抢占了,直到其他所有线程打印完之后才能进行后面的步骤。

使用Lambda表达式启动线程

​ 当线程所要执行的代码非常短小时,没有必要专门为之创建一个函数,可使用 Lambda表达式。

#include <thread>#include <iostream>#include <vector>using namespace std;int main(){    vector<thread> threads;    for(int i = 0; i < 5; ++i){        threads.push_back(std::thread(            [](){            cout << "Hello from thread " << this_thread::get_id() << endl;        }));    }    for(auto& th : threads){        th.join();    }    return 0;}

C++11 并发编程教程 - Part 2 : 保护共享数据

上面的代码中,线程互相独立,通常情况下,线程之间可能用到共享数据。一旦对共享数据进行操作,就面临着一个新的问题 —— 同步。

同步问题

我们就拿一个简单的计数器作为示例吧。

这个计数器是一个结构体,他拥有一个计数变量,以及增加或减少计数的函数,看起来像这个样子:

#include <thread>#include <iostream>#include <vector>using namespace std;struct Counter {    int m_nValue;    Counter(int n = 0) : m_nValue(n){}    void increment(){ ++m_nValue; }};int main(){    Counter counter;    vector<thread> threads;    for(int i = 0; i < 5; ++i){        threads.push_back(thread([&counter](){            for(int i = 0; i < 999999; ++i){                counter.increment();            }        }));    }    for(auto& th : threads){        th.join();    }    cout << counter.m_nValue << endl;    return 0;}

每次运行结果会不同,因为计数器的increment()操作并非原子操作,而是由3个独立的操作组成的:

1、读取m_nValue变量的当前值。

2、将读取的当前值+1

3、将+1后的值写回value变量。

当单线程运行上述代码,每次运行的结果是一样的,上述三个步骤会按顺序进行。但是在多进程情况下,可能存在如下执行顺序:

  1. 线程a:读取 m_nValue的当前值,得到值为 0。加1。得到1,但还没来得及写回内存

  2. 线程b:读取 m_nValue的当前值,得到值为 0。加1。得到1,但还没来得及写回内存。

  3. 线程a:将 1 写回 m_nValue 内存并返回 1。

  4. 线程b:将 1 写回 m_nValue内存并返回 1。

这种情况源于线程间的 interleaving(交叉运行)。

Interleaving 描述了多线程同时执行几句代码的各种情况。就算仅仅只有两个线程同时执行这三个操作,也会存在很多可能的 interleaving。当你有许多线程同时执行多个操作时,要想枚举出所有 interleaving,几乎是不可能的。而且如果线程在执行单个操作的不同指令之间被抢占,也会导致 interleaving 的发生。

目前有许多可以解决这一问题的方案:

  • Semaphores
  • Atomic references
  • Monitors
  • Condition codes
  • Compare and swap
  • ……

本文将使用 Semaphores 去解决这一问题。

事实上,我们仅仅使用了Semaphores 中比较特殊的一种 —— 互斥量。

互斥量是一个特殊的对象,在同一时刻只有一个线程能够得到该对象上的锁。借助互斥量这种简而有力的性质,我们便可以解决线程同步问题。

使用互斥量保证 Counter 的线程安全

在 C++11 的线程库中,互斥量被放置于头文件 mutex,并以 std::mutex 类加以实现。互斥量有两个重要的函数:lock() 和 unlock()。顾名思义,前者使当前线程尝试获取互斥量的锁,后者则释放已经获取的锁。lock() 函数是阻塞式的,线程一旦调用 lock(),就会一直阻塞直到该线程获得对应的锁。

为了使计数器具备线程安全性,我们需要对其添加 std::mutex 成员,并在成员函数中对互斥量进行 lock()和unlock() 调用。

struct Counter {    int m_nValue;    mutex mtx;    Counter() : m_nValue(0){}    void increment(){        mtx.lock();        ++m_nValue;        mtx.unlock();    }};

如果我们现在再次运行之前的测试程序,我们将始终得到正确的输出。(加锁之后,会大大降低程序运行效率,所以可以将上面的内循环降低至10000)

异常与锁

现在让我们来看看另外一种情况会发生什么。

假设现在我们的计数器拥有一个 derement() 操作,当 value 被减为 0 时抛出一个异常:

struct Counter {    int m_nValue;    Counter() : m_nValue(0){}    void increment(){        ++m_nValue;    }    void decrement(){        if(m_nValue == 0){            throw "Value cannot be less than 0";        }        --m_nValue;    }};

假设你想在不更改上述代码的前提下为其提供线程安全性,那么你需要为其创建一个 Wrapper 类:

struct ConcurrentCounter{    mutex mtx;    Counter counter;    void increment(){        mtx.lock();        counter.increment();        mtx.unlock();    }    void decrement(){        mtx.lock();        counter.decrement();        mtx.unlock();    }};

这个 Wrapper 将在大多数情况下正常工作,然而一旦 decrement() 抛出异常,你就遇到大麻烦了,当异常被抛出时,unlock() 函数将不会被调用,这将导致本线程获得的锁不被释放,你的程序也就顺理成章的被永久阻塞了。为了修复这一问题,你需要使用 try/catch 块以保证在抛出任何异常之前释放获得的锁。

void decrement(){     mtx.lock();     try{         counter.decrement();     } catch(string e) {         mtx.unlock();         throw e;     }     mtx.unlock();}

代码并不复杂,但是看起来却很丑陋。试想一下,你现在的函数拥有 10 个返回点,那么你就需要在每个返回点前调用 unlock() 函数,而忘掉其中的某一个的可能性是非常大的。更大的风险在于你又添加了新的函数返回点,却没有对应地添加 unlock()。下一节将给出解决此问题的好办法。

锁的自动管理

​ 当你想保护整个代码段(就本文而言是一个函数,但也可以是某个循环体或其他控制结构[即一个作用域])免受多线程的侵害时,有一个办法将有助于防止忘记释放锁:std::lock_guard。

这个类是一个简单、智能的锁管理器。当 std::lock_guard 实例被创建时,它自动地调用互斥量的lock() 函数,当该实例被销毁时,它也顺带释放掉获得的锁。你可以像这样使用它:

struct ConcurrentSafeCounter{    mutex mtx;    Counter counter;    void increment(){        lock_guard<mutex> guard(mtx);        counter.increment();    }    void decrement(){        lock_guard<mutex> guard(mtx);        counter.decrement();    }};

代码变得更整洁了不是吗?

使用这种方法,你无须绷紧神经关注每一个函数返回点是否释放了锁,因为这个操作已经被std::lock_guard 实例的析构函数接管了。

注意

现在我们结束了短暂的 Semaphores 之旅。在本章中你学习了如何使用 C++ 线程库中的互斥量来保护你的共享数据。**

但有一点请牢记:锁机制会带来效率的降低。的确,一旦使用锁,你的部分代码就变得有序[非并发]了。如果你想要设计一个高度并发的应用程序,你将会用到其他一些比锁更好的机制,但他们已不属于本文的讨论范畴。

C++11 并发编程教程 - Part 3 : 锁的进阶与条件变量

上一章使用互斥量解决线程同步,这一章将进一步讨论互斥量的话题,并介绍C++11并发库中的另一种同步机制——条件变量。

递归锁

待续。。。

0 0
原创粉丝点击