异常与多线程

C++ 的异常机制为程序员提供了一种处理错误的方式, 使程序员可以用更自然的方式处理错误.(但 C++ 的异常存在各种坑, 参考《More Effective C++》Item 13: Catch exceptions by reference).

有些时候, 我们不得不使用多线程来提高工作的执行效率, 但由于异常都是基于线程的, 如何在线程间传递异常, 就成为了一个问题. C++11 之前异常是无法在线程之前传递的, C++11 在这方面提供了支持.

使用 C++11 中的异常相关内容实现在线程间传递异常

相关类如下:

std::exception_ptr

  • 一个智能指针类型的异常对象, 支持 bool 操作符, 可以存储空异常 (null) 对象. 也支持比较 (operator==).

std::current_exception

  • 当在异常处理块中 (catch块) 调用时, 会捕获当前异常对象并由被捕获的对象创建一个 std::exception_ptr 对象并返回.

std::make_exception_ptr

  • 接受一个 Exception 对象参数, 并由这个异常对象创建一个 std::exception_ptr 对象

std::rethrow_exception

  • 接受一个 std::exception_ptr 参数, 重新抛出这个异常

(以上的一切, 很大一部分都是因为 C++ 的值传递特性引起的一系列不适!!!, !!(╯’ – ‘)╯︵ ┻━┻)

Read on →

C++11中的原子操作 (Atomic Operation)

所谓的原子操作, 取的就是 “原子是最小的, 不可分割的最小个体” 的意义, 它表示在多个线程访问同一个全局资源的时候, 能够确保所有其他的线程都不在同一时间内访问相同的资源. 也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问. 这有点类似互斥对象对共享资源的访问的保护, 但是原子操作更加接近底层, 因而效率更高.

在以往的 C++ 标准中并没有对原子操作进行规定, 我们往往是使用汇编语言, 或者是借助第三方的线程库, 例如 intel 的 pthread 来实现. 在新标准 C++11, 引入了原子操作的概念, 并通过这个新的头文件提供了多种原子操作数据类型, 例如, atomic_bool, atomic_int 等等, 如果我们在多个线程中对这些类型的共享资源进行操作, 编译器将保证这些操作都是原子性的, 也就是说, 确保任意时刻只有一个线程对这个资源进行访问, 编译器将保证, 多个线程访问这个共享资源的正确性. 从而避免了锁的使用, 提高了效率.

我们还是来看一个实际的例子. 假若我们要设计一个广告点击统计程序, 在服务器程序中, 使用多个线程模拟多个用户对广告的点击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <thread>
#include <time.h>
// 全局的结果数据
long total = 0;
// 点击函数
void click()
{
  for(int i = 0; i < 10000; ++i) {
    // 对全局数据进行无锁访问
    total += 1;
  }
}

int main(int argc, char* argv[])
{
  // 计时开始
  clock_t start = clock();
  // 创建100个线程模拟点击统计
  std::thread threads[100];
  for(int i = 0; i < 100; ++i) {
    std::thread t(&click);
    threads[i].swap(t);
  }
  for(int i=0; i<100; ++i) {
    threads[i].join();
  }
  // 计时结束
  clock_t finish = clock();
  // 输出结果
  std::cout << "result:" << total << std::endl;
  std::cout << "duration:" << finish -start << "ms" << std::endl;
  return 0;
}
Read on →

异步编程 Async & Await

使用异步编程

使用异步编程, 可以避免性能瓶颈和增强应用程序的总体响应能力. 但是编写异步程序在以前技术上比较复杂, 使得它们难以编写, 调试以及维护.

在 .NET Framework 4.5 中, 引入了简化的异步编程方式, 编译器帮我们实现了之前需要实现的异步操作, 程序员只需要关注自己的代码逻辑即可. 程序员可以更专注到业务逻辑当中.

async & await

在 C# 中, 关键字 async 和 await 是异步编程的核心, 当使用这两个关键字的时候, 我们可以直接创建一个异步函数, 就像创建一个同步函数一样简单. 看下面的例子, 所有的东西看起来都是那么的熟悉.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Three things to note in the signature: 
//  - The method has an async modifier.  
//  - The return type is Task or Task<T>. (See "Return Types" section.)
//    Here, it is Task<int> because the return statement returns an integer. 
//  - The method name ends in "Async."
async Task<int> AccessTheWebAsync()
{
    // You need to add a reference to System.Net.Http to declare client.
    HttpClient client = new HttpClient();

    // GetStringAsync returns a Task<string>. That means that when you await the 
    // task you'll get a string (urlContents).
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

    // You can do work here that doesn't rely on the string from GetStringAsync.
    DoIndependentWork();

    // The await operator suspends AccessTheWebAsync. 
    //  - AccessTheWebAsync can't continue until getStringTask is complete. 
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync. 
    //  - Control resumes here when getStringTask is complete.  
    //  - The await operator then retrieves the string result from getStringTask. 
    string urlContents = await getStringTask;

    // The return statement specifies an integer result. 
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value. 
    return urlContents.Length;
}
Read on →

Attorney Client

目的

控制访问一个类内部实现细节的粒度.

动机

C++ 的友元申明可以完全访问一个类的内部细节. 所以, 唔, 友元太邪恶了, 它破坏了精心的封装. C++ 中不提供选择性的授权访问类私有成员子集的方式. 在 C++ 中要么使用友元, 可以访问一个类的全部细节, 要么不用友元, 只能访问公有接口.

看下面的例子, 下面的类 Foo 中声明了 Bar 友元. 因此 Bar 类可以访问 Foo 的全部私有成员. 这不可取, 因为它增加了耦合. Bar 类会变得没有 Foo 类就无法单独使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo
{
private:
  void A(int a);
  void B(float b);
  void C(double c);
  friend class Bar;
};

class Bar
{
// This class needs access to Foo::A and Foo::B only.
// C++ friendship rules, however, give access to all the private members of Foo.
};

提供选择性访问成员子集的功能, 是可取的. 这样的话, 剩余的私有成员如果需要的话, 可以更改接口. 它有助于减少两个类之间的耦合. Attorney-Client 可以精确控制它的友元能够访问的成员的数量.

Read on →

IO 模型

阻塞 I/O

考虑到应用进程与内核的区别, 我们将函数 recvfrom 视为系统调用. 不管函数 recvfrom 如何实现, 一般都有一个从应用进程中运行到内核中运行的切换, 一段时间后再跟着一个返回到应用进程的切换.

如图, 进程调用 recvfrom, 此时系统调用直到数据报到达且拷贝到应用缓冲区, 或者出错才返回. 我们所说的进程阻塞的整段时间是指从调用 recvfrom 开始到它返回的这段时间, 当进程返回成功指示时, 应用进程开始处理数据报.

Read on →
IO

类型选择

目的

在编译期间, 根据一些条件, 决定类型.

动机

在编译期根据一些信息来做出决议是元编程的一个强大工具. 其中一种就是在编译期决定类型, 比如根据一些列的条件最后决定具体的类型.

例如, 考虑一个 Queue 的实现, 这个模板类有一个静态数组, 模板参数作为这个 Queue 类型的最大容量. Queue 类同时需要存储当前元素的个数, 从 0 开始. 可能的优化方案是用不同的类型, 来存储 size 值. 比如当 Queue 的最大容量小于 256 时, 存储容量的类型是 unsigned char, 当容量小于 65536 时, 存储容量的类型是 unsigned short, 更大的容量时使用 unsigned int 类型. 类型选择可以在这个时候发挥作用.

解决方案以及代码示例

一个简单的解决方案是使用 IF 模板. IF 模板有三个模板参数, 第一个参数是编译期的 bool 表达式. 如果表达式为 true 那么第二个模板参数类型将被选中, 否则会选中第三个模板参数. IF 模板由下面的一个模板和一个模板偏特化构成.

1
2
3
4
5
6
7
8
9
10
11
12
template <bool, class L, class R>
struct IF  // primary template
{
  typedef R type;
};
template <class L, class R>
struct IF<true, L, R> // partial specialization
{
  typedef L type;
};
IF<false, int, long>::type i; // is equivalent to long i;
IF<true,  int, long>::type i; // is equivalent to int i;
Read on →

使用 Condition Variable 实现生产者 消费者模型

什么是生产者-消费者问题?

生产者消费者问题 (英语: Producer-consumer problem), 也称有限缓冲问题 (英语: Bounded-buffer problem), 是一个多线程同步问题的经典案例. 该问题描述了两个共享固定大小缓冲区的线程 —— 即所谓的 “生产者” 和 “消费者” —— 在实际运行时会发生的问题. 生产者的主要作用是生成一定量的数据放到缓冲区中, 然后重复此过程. 与此同时, 消费者也在缓冲区消耗这些数据. 该问题的关键就是要保证生产者不会在缓冲区满时加入数据, 消费者也不会在缓冲区中空时消耗数据.

要解决该问题, 就必须让生产者在缓冲区满时休眠 (要么干脆就放弃数据), 等到下次消费者消耗缓冲区中的数据的时候, 生产者才能被唤醒, 开始往缓冲区添加数据. 同样, 也可以让消费者在缓冲区空时进入休眠, 等到生产者往缓冲区添加数据之后, 再唤醒消费者. 通常采用进程间通信的方法解决该问题, 常用的方法有信号量等. 如果解决方法不够完善, 则容易出现死锁的情况. 出现死锁时, 两个线程都会陷入休眠, 等待对方唤醒自己. 该问题也能被推广到多个生产者和消费者的情形.

Read on →
C++

Visitor 模式与 boost.variant.static_visitor

visitor 模式

visitor 模式是一种将算法与对象结构分离的软件设计模式.

这个模式的基本想法如下: 首先我们拥有一个由许多对象构成的对象结构, 这些对象的类都拥有一个 accept 方法用来接受访问者对象; 访问者是一个接口, 它拥有一个 visit 方法, 这个方法对访问到的对象结构中不同类型的元素作出不同的反应; 在对象结构的一次访问过程中, 我们遍历整个对象结构, 对每一个元素都实施 accept 方法, 在每一个元素的 accept 方法中回调访问者的 visit 方法, 从而使访问者得以处理对象结构的每一个元素. 我们可以针对对象结构设计不同的实在的访问者类来完成不同的操作.

访问者模式使得我们可以在传统的单分派语言 (如 Smalltalk, Java 和 C++) 中模拟双分派技术.

Read on →

C++11 完美转发

动机

在泛型编码中经常出现的一个问题是: 如何将一组参数原封不动的转发给另一个函数? 这里的原封不动指的是: 保持参数的左值 (右值), const (non-const) 属性不变.

C++03 中参数转发存在的问题

下面来看一个例子, 对于表达式 E(a, b, …, c) 我们希望它与 f(a, b, …, c) 完全等价. 在 C++03 中这是不可能的. 下面是几种设计方案, 但所有的都会在某些条件下失效.

最简单的, 使用引用参数:

1
2
3
4
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c) {
    E(a, b, c);
}

很遗憾, 函数 f 无法处理临时变量, 例如 f(1, 2, 3); 会编译失败. 这三个参数都无法绑定到引用.

再来看看 const 引用:

1
2
3
4
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c) {
    E(a, b, c);
}
Read on →

C++11 右值引用

什么是右值引用?

在一般情况下, C++ 不把表达式的左值属性作为类型的一部分, 比如下面的变量声明:

1
int n;

那么变量 n 的类型是 int. 另外常量 3 也是 int 类型. 虽然他们有相同的类型, 不过这并不是说 n 和 3 总是可以互换的. 比如下面的代码:

1
2
int& r = n;  // OK
int& s = 3;  // Error

这是因为标准中规定, 当定义一个引用时, 我们必须使用一个左值来初始化它. 在上面的代码中, n 是一个左值, 而 3 不是. 所以我们不能用 3 来初始化 s. 这个规定可以帮助编译器来捕捉错误, 比如下面的代码:

1
std::cin >> 3;  // equals std::cin.operator>>(3);

简单的说, 上面的代码调用了 std::cin 的名为 operator>> 的成员函数, 并使用 3 作为参数. operator>> 是重载函数, 虽然它有接受一个 int& 参数的重载版本, 但是没有能接受一个简单的 int 常量, 或者 int 右值作为参数的重载. 因此, 编译器会检查出我们无法使用 int 的右值作为参数来调用 operator>>.

Read on →