为什么要进行多线程编程

无他,更快。

多线程编程中的难点

在多线程编程环境下,计算顺序的不确定性是一个本质问题。而多线程编程的难点在于

  1. 线程互斥。解决数据争用的问题。多线程编程下(同一进程的)各线程共享的全局对象,由于操作系的抢占式任务调度调度方式会造成计算顺序的混乱,具体来说就是实际运行顺序并不是程序所期待的顺序。
  2. 线程同步。将一个大任务拆分为多个小任务(线程)后,小任务之间是需要通过某种方式组织起来还原会大任务的。具体来说,小任务之间是通过一个有向无环图组织起来的,其中某些小任务需要等待另外一些小任务的完成,否则无法继续。

如何解决线程互斥问题

在C++中,解决线程互斥(问题1)的方式有:

  • lock_gurad/unique_lock + mutex
  • semaphore = mutex + condition_variable + int_cnt
  • atomic

解决问题2的方式有:

  • condition_variable

解决数据争用问题

加锁操作

使用lock_gurad/unique_lock + mutex可实现异常安全的加锁操作。

单纯使用mutex的.lock()和.unlock()方法不是异常安全的。如每一个线程要完成run()这样一个计算任务:

mutex m;
void run(int a, int b, int c) {
    m.lock();
    int d = a + b / c;
    m.unlock();
}

这里使用的.lock()与.unlock()划定了一个临界区。若临界区中的计算抛异常了,就无法执行.unlock()语句。从而,mutex永远被锁上了。

上述问题的解决知道就是使用RAII手法,利用lock_guard/unique_lock持有mutex对象。lock_guard中的构造函数调用了.lock(),析构函数实现了.unlock(),从而使用lock_guard/unique_lock + mutex可实现异常安全的锁操作。具体代码如下:

mutex m;
void run(int a, int b, int c) {
    lock_guard<mutex> l(m)
    int d = a + b / c;
}

跨线程安全的资源计数

信号量实现了这样的功能:

  1. 获得一个资源,资源计数器减1
  2. 释放一个资源,资源计数器加1
  3. 没有资源可以获得,线程阻塞等待

C++原生不支持信号量,可以通过lock_guard/unique_lock + mutex + int_cnt手动实现一个。具体代码如下:

class Semaphore
{
private:
    mutex mMutex;
    condition_variable mCondVar;
    int64_t mAvailable;
public:
    explicit Semaphore(int64_t init) : mAvailable(init)
    {}

    void post()
    {
        {
            unique_lock<mutex> l(mMutex);
            // do computation
            ++mAvailable;
        }
        mCondVar.notify_one();
    }

    void wait()
    {
        unique_lock<mutex> l(mMutex);
        while (mAvailable == 0) {
            mCondVar.wait(l);
        }
        --mAvailable;
    }
};

原子操作

c++的标准库atomic可实现原子操作。 todo

解决线程等待问题

todo

C++高级线程同步原语

future,promise,packaged_task

future实现延迟计算。特别地,future可实现线程通知。

TODO

参考资料

10分钟,带你掌握C++多线程同步!