C++ 多线程编程尝试

在开发 animeloop-cli 的时候,考虑到某些使用者机器是服务器版多核心多线程 CPU ,需要添加程序运行多核心支持。

大致尝试了几种多线程编程方法。

OpenMP

OpenMP 用于共享内存并行系统的多线程程序设计的一套指导性注释(Compiler Directive)。

首先用一段简单的代码展示 OpenMP 最基础的使用:

#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel for
    for (int i = 0; i < 10; ++i)
    {
        std::cout << i;
    }

    return 0;
}

通过在 for 循环前一行添加 #pragma omp parallel for,可以方便的指定循环采用多线程执行。如果你需要的多线程并行运行的相关内容变量之间相互独立,那么这么两行代码就完全足够了。实际上上述使用方法是一个简写的版本,声明 for 循环并行化可以通过以下两种方式:

#pragma omp parallel for
for (int i = 0; i < 10; ++i)
{
    // do something...
}

#pragma omp parallel
{
    #pragma omp for
    for (int i = 0; i < 10; ++i){
        for (int j = 0; j < 10; ++j){
            // do something...
        }
    }
    #pragma omp for
    for (int j = 0; j < 10; ++j){
        // do something...
    }
}

嵌套结构的支持意味着可以再申明一个并行区域里可以更加详细地定制并行运行规则。这里的 #pragma omp for 仅针对最外层的 for 循环。

OpenMP 大多数情况下的通用使用结构

// Header file
#include <omp.h>

// Parallel region
#pragma omp construct [clauses…]
{
    // do some work...
}

construct 用于定义使用何种类型运行方式的区域(Region)。并行(parallel)结构算是这当中最重要的结构了。

clauses 于定制更加详细的规则。一些常用的 clauses:

shared nowait if
reduction copyin private
firstprivate num_threads default

OpenMP 常用的几个函数:

// 显示当前可用的处理器个数
extern int omp_get_num_procs (void) __GOMP_NOTHROW;

// 设置并行线程数量
extern void omp_set_num_threads (int) __GOMP_NOTHROW;

// 获取当前并行线程序号,从零开始
extern int omp_get_thread_num (void) __GOMP_NOTHROW;

OpenMP 的执行模型(Execution Model)

Fork_join
图片来自维基百科[1]

Fork-Join Parallelism

在程序运行需要大量计算量耗时很长的部分,从主线程中产生(spawn)出一组子线程(数量默认为可用处理器个数 N-1 个),构成一个并行区域(Parallel Regions),同时运行处理数据,并在这个并行区域内所有线程全部结束之后,回到主线称,继续执行后续部分。

更多参考:

C++11 的线程类 std::thread

相比 OpenMP,C++11 自带的线程类使用上就方便很多了。

使用 Clion 编译 OpenMP 项目时,在 CMakeLists.txt 文件里添加两行即可:

set(CMAKE_CXX_STANDARD 11)
IF (CMAKE_SYSTEM_NAME MATCHES "Linux")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
ENDIF(CMAKE_SYSTEM_NAME MATCHES "Linux")
#include <iostream>
#include <thread>

void do_something(std::string str, int x)
{
    for (int i = 0; i < 255555; ++i) {
        for (int j = 0; j < x; ++j) {
        }
    }

    std::cout << "Hello, " + str << "!" << std::endl;
}

int main()
{
    std::thread threads[4];
    std::string names[4] = {"Steve", "Bill", "Larry", "Jeff"};
    int values[4] = {1000, 10000, 1, 500};
    for (int i = 0; i < 4; ++i)
    {
        threads[i] = std::thread(do_something, names[i], values[i]);
    }

    for (int j = 0; j < 4; ++j) {
        threads[j].join();
    }

    std::cout << "work done!" << std::endl;

    return 0;
}

输出:

Hello, Larry!
Hello, Jeff!
Hello, Steve!
Hello, Bill!
work done!

使用 std::thread 相对来说就简单粗暴多了,我们只关注将可以并行运行相互独立的一组程序段分开丢进各自的线程中,剩下的就靠编译器操作系统帮我们完成调度。

测试后发现这种写法有问题,线程数爆炸会非常恐怖的,应该考虑用线程池在使用。

Boost thread_group/threadpool

auto group = thread_group();
boost::thread *t1 = new boost::thread(threaded_function, 10);
group.add_thread(t1);

boost::asio::io_service ioService;
boost::thread_group threadpool;
boost::asio::io_service::work work(ioService);

const int threads_num = 4;
for (int i = 0; i < threads_num; ++i)
{
	threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioService));
}

ioService.post(boost::bind(task1, "Hello World!"));
ioService.post(boost::bind(task2, 3.1415926));

// ioService.stop();
threadpool.join_all();

animeloop-cli 多线程改进

在目前的版本,程序最后输出 loop 文件的时候,是一个一个通过 opencv 输出的,只能使用单 CPU,所以小小改写了一下,加入了一个线程池,多线程并行输出 loop 文件。


  1. https://en.wikipedia.org/wiki/OpenMP ↩︎