C++11多线程教学(一)

本篇教学代码可在GitHub获得:https://github.com/sol-prog/threads。

在之前的教学中,我展示了一些最新进的C++11语言内容:

  • 1. 正则表达式(http://solarianprogrammer.com/2011/10/12/cpp-11-regex-tutorial/)
  • 2. raw string(http://solarianprogrammer.com/2011/10/16/cpp-11-raw-strings-literals-tutorial/)
  • 3. lambda(http://solarianprogrammer.com/2011/11/01/cpp-11-lambda-tutorial/)

也许支持多线程是C++语言最大的变化之一。此前,C++只能利用操作系统的功能(Unix族系统使用pthreads库),或是例如OpenMP和MPI这些代码库,来实现多核计算的目标。

本教程意图让你在使用C++11线程上起个头,而不是只把语言标准在这里繁复地罗列出来。

创建和启动一条C++线程就像在C++源码中添加线程头文件那么简便。我们来看看如何创建一个简单的带线程的HelloWorld:

#include《iostream》

#include《thread》

//This function will be called from a thread

//该函数将在一条线程中得到调用

void call_from_thread() {

std::cout << "Hello, World" << std::endl;

}

int main() {

//Launch a thread

//启动一条线程

std::thread t1(call_from_thread);

//Join the thread with the main thread

//和主线程协同

t1.join();

return 0;

}

在Linux系统中,上列代码可采用g++编译:

g++ -std=c++0x -pthread file_name.cpp

在安装有Xcode4.x的麦金系统上,可用clang++编译上述代码:

clang++ -std=c++0x -stdlib=libc++ file_name.cpp

视窗系统上,可以利用付费代码库,just::thread,来编译多线程代码。但是很不走运,他们没有提供代码库的试用版,我做不了测试。

在真实世界的应用程序中,函数“call_from_thread”相对主函数而言,独立进行一些运算工作。在上述代码中,主函数创建一条线程,并在t1.join()处等待t1线程运行结束。如果你在编码中忘记考虑等待一条线程结束运行,主线程有可能抢先结束它自己的运行状态,整个程序在退出的时候,将杀死先前创建的线程,不管函数“call_from_thread”有没有执行完。

上面的代码比使用POSIX线程的等价代码,相对来说简洁一些。请看使用POSIX线程的等价代码:

//This function will be called from a thread

void *call_from_thread(void *) {

std::cout << "Launched by thread" << std::endl;

return NULL;

}

int main() {

pthread_t t;

//Launch a thread

pthread_create(&t, NULL, call_from_thread, NULL);

//Join the thread with the main thread

pthread_join(t, NULL);

return 0;

}

我们通常希望一次启动多个线程,来并行工作。为此,我们可以创建线程组,而不是在先前的举例中那样创建一条线程。下面的例子中,主函数创建十条为一组的线程,并且等待这些线程完成他们的任务(在github代码库中也包含这个例子的POSIX版本):

...

static const int num_threads = 10;

...

int main() {

std::thread t[num_threads];

//Launch a group of threads 启动一组线程

for (int i = 0; i < num_threads; ++i) {

t[i] = std::thread(call_from_thread);

}

std::cout << "Launched from the mainn";

//Join the threads with the main thread

for (int i = 0; i < num_threads; ++i) {

t[i].join();

}

return 0;

}

记住,主函数也是一条线程,通常叫做主线程,所以上面的代码实际上有11条线程在运行。在启动这些线程组之后,线程组和主函数进行协同(join)之前,允许我们在主线程中做些其他的事情,在教程的结尾部分,我们将会用一个图像处理的例子来说明之。

在线程中使用带有形参的函数,是怎么一回事呢?C++11允许我们在线程的调用中,附带上所需的任意参数。为了举例说明,我们可以修改上面的代码,以接受一个整型参数(在github代码库中也包含这个例子的POSIX版本):

static const int num_threads = 10;

//This function will be called from a thread

void call_from_thread(int tid) {

std::cout << "Launched by thread " << tid << std::endl;

}

int main() {

std::thread t[num_threads];

//Launch a group of threads

for (int i = 0; i < num_threads; ++i) {

t[i] = std::thread(call_from_thread, i);

}

std::cout << "Launched from the mainn";

//Join the threads with the main thread

for (int i = 0; i < num_threads; ++i) {

t[i].join();

}

return 0;

}

在我的系统上,上面代码的执行结果是:

Sol$ ./a.out

Launched by thread 0

Launched by thread 1

Launched by thread 2

Launched from the main

Launched by thread 3

Launched by thread 5

Launched by thread 6

Launched by thread 7

Launched by thread Launched by thread 4

8L

aunched by thread 9

Sol$

能看到上面的结果中,程序一旦创建一条线程,其运行存在先后秩序不确定的现象。程序员的任务就是要确保这组线程在访问公共数据时不要出现阻塞。最后几行,所显示的错乱输出,表明8号线程启动的时候,4号线程还没有完成在stdout上的写操作。事实上假定在你自己的机器上运行上面的代码,将会获得全然不同的结果,甚至是会输出些混乱的字符。原因在于,程序内的11条线程都在竞争性地使用stdout这个公共资源(案:Race Conditions)。

要避免上面的问题,可以在代码中使用拦截器(barriers),如std:mutex,以同步(synchronize)的方式来使得一群线程访问公共资源,或者,如果可行的话,为线程们预留下私用的数据结构,避免使用公共资源。我们在以后的教学中,还会讲到线程同步问题,包括使用原子操作类型(atomic types)和互斥体(mutex)。

从原理上讲,编写更加复杂的并行代码所需的概念,我们已经在上面的代码中都谈到了。

接下来的例子,我要来展示并行编程方案的强大之处。这是个稍为复杂的问题:利用柔化滤波器(blur filter)去除一张图片的杂点。思路是利用一点像素和它相邻像素的加权均值的某种算法形式(案:后置滤波),去除图片杂点。

本教程不在于讨论优化图像处理,笔者也非此路专家,所以我们只采取相当简单的方法。我们的目标是勾勒出如何去编写并行代码,至于如何高效访问图片,与滤波器的卷积计算,都不是重点。我在此作为举例,只利用空间卷积的定义,而不是采用更多的共振峰(?),当然稍微有些实现上的难度,频域的卷积使用快速傅里叶变换。

为简便起见,我们将使用一种简单的非压缩图像文件PPM。接下来,我们提供一个简化的C++类的头文件,这个类负责读写PPM图片,并在内存中的三个无符号字符型数组结构里(RGB三色)重建图像:

class ppm {

bool flag_alloc;

void init();

//info about the PPM file (height and width)

//PPM文件的信息(高和宽)

unsigned int nr_lines;

unsigned int nr_columns;

public:

//arrays for storing the R,G,B values

//保存RGB值的数组

unsigned char *r;

unsigned char *g;

unsigned char *b;

//

unsigned int height;

unsigned int width;

unsigned int max_col_val;

//total number of elements (pixels)

//元素(像素)的总量

unsigned int size;

ppm();

//create a PPM object and fill it with data stored in fname

//创建一个PPM对象,装载保存在文件fname中的数据

ppm(const std::string &fname);

//create an "empty" PPM image with a given width and height;the R,G,B arrays are filled  //with zeros

//创建一个“空”PPM图像,大小由_width和_height指定;RGB数组置为零值

ppm(const unsigned int _width, const unsigned int _height);

//free the memory used by the R,G,B vectors when the object is destroyed

//在本对象销毁时,释放RGB向量占用的内存

~ppm();

//read the PPM image from fname

//从fname文件中读取PPM图像

void read(const std::string &fname);

//write the PPM image in fname

//保存PPM图像到fname文件

void write(const std::string &fname);

};

一种可行的编码方案是:

 

  • 载入图像到内存。
  • 把图像拆分为几个部分,每部分由相应线程负责,线程数量为系统可承受之最大值,例如四核心计算机可启用8条线程。
  • 启动若干线程——每条线程负责处理它自己的图像块。
  • 主线程处理最后的图像块。
  • 与主线程协调并等待全部线程计算完成。
  • 保存处理后的图像。

 

接下来我们列出主函数,该函数实现了如上算法(感谢wiched提出的代码修改意见):

int main() {

std::string fname = std::string("your_file_name.ppm");

ppm image(fname);

ppm image2(image.width, image.height);

//Number of threads to use (the image will be divided between threads)

//采用的线程数量(图像将被分割给每一条线程去处理)

int parts = 8;

std::vectorbnd = bounds(parts, image.size);

std::thread *tt = new std::thread[parts - 1];

time_t start, end;

time(&start);

//Lauch parts-1 threads

//启动parts-1个线程

for (int i = 0; i < parts - 1; ++i) {

tt[i] = std::thread(tst, &image, &image2, bnd[i], bnd[i + 1]);

}

//Use the main thread to do part of the work !!!

//使用主线程来做一部分任务!

for (int i = parts - 1; i < parts; ++i) {

tst(&image, &image2, bnd[i], bnd[i + 1]);

}

//Join parts-1 threads 协同parts-1条线程

for (int i = 0; i < parts - 1; ++i)

tt[i].join();

time(&end);

std::cout << difftime(end, start) << " seconds" << std::endl;

//Save the result 保存结果

image2.write("test.ppm");

//Clear memory and exit 释放占用的内存,然后退出

delete [] tt;

return 0;

}

请无视图像文件名和线程启动数的硬性编码。在实际应用中,应该让用户可以交互式输入这些参数。

现在为了看看并行代码的工作情况,我们需要赋之以足够任务负荷,否则那些创建和销毁线程的开销将会干扰测试结果,使得我们的并行测试失去意义。输入的图像应该足够大,以能显示出并行代码性能方面的改进效果。为此,我采用了一张16000x10626像素大小的PPM 格式图片,空间占用约512MB:

我用Gimp软件往图片里掺入了一些杂点。杂点效果如下图:

前面代码的运行结果:

正如所见,上面的图片杂点程度被弱化了。

 

样例代码运行在双核MacBook Pro上的结果:

 

Compiler Optimization Threads Time Speed up
clang++ none 1 40s  
clang++ none 4 20s 2x
clang++ -O4 1 12s  
clang++ -O4 4 6s 2x

在双核机器上,并行比串行模式(单线程),速率有完美的2倍提升。

 

 

我还在一台四核英特尔i7Linux机器上作了测试,结果如下:

 

Compiler Optimization Threads Time Speed up
g++ none 1 33s  
g++ none 8 13s 2.54x
g++ -O4 1 9s  
g++ -O4 8 3s 3x

显然,苹果的clang++在提升并行程序方面要更好些,不管怎么说,这是编译器/机器特性的一个联袂结果,也不排除MacBook Pro使用了8GB内存的因素,而Linux机器只有6GB。

 

 

如果有兴趣学习新的C++11语法,我建议阅读《Professional C++》,或《C ++ Primer  Plus》。C++11多线程主题方面,建议阅读《C++ Concurrency in Action》,这是一本好书。

 

from:http://article.yeeyan.org/view/234235/268247

时间: 2024-10-04 12:48:04

C++11多线程教学(一)的相关文章

C++11多线程教学(二)

C++11多线程教学II 从我最近发布的C++11线程教学文章里,我们已经知道C++11线程写法与POSIX的pthreads写法相比,更为简洁.只需很少几个简单概念,我们就能搭建相当复杂的处理图片程序,但是我们回避了线程同步的议题.在接下来的部分,我们将进入C++11多线程编程的同步领域,看看如何来同步一组并行的线程. 我们快速回顾一下如何利用c++11创建线程组.上次教学当中,我们用传统c数组保存线程,也完全可以用标准库的向量容器,这样做更有c++11的气象,同时又能避免使用new和dele

C++11 多线程

C++11开始支持多线程编程,之前多线程编程都需要系统的支持,在不同的系统下创建线程需要不同的API如pthread_create(),Createthread(),beginthread()等,使用起来都比较复杂,C++11提供了新头文件<thread>.<mutex>.<atomic>.<future>等用于支持多线程. 使用C++11开启一个线程是比较简单的,下面来看一个简单的例子: #include <thread> #include &

为什么多线程是个坏主意

在 Unix编程艺术 中,提到了尽量避免多线程编程模型, 认为这样只会增加复杂度, 提倡使用多进程, 这样本质上就可以避免多线程『共享内存数据』产生的 "corruotped memory" 问题. 其中, 提到了一篇文章 Why Threads Are A Bad Idea, 对于多线程编程和事件编程分析的非常好, 具体的翻译如下: 1 介绍 线程的背景: 在操作系统中出现多线程 逐渐演变成 用户层面的编程工具 被认为是多种问题的一种通用解决方案 每一个程序员都需要成为 一个多线程编

C++11常用特性的使用经验总结

C++11已经出来很久了,网上也早有很多优秀的C++11新特性的总结文章,在编写本博客之前,博主在工作和学习中学到的关于C++11方面的知识,也得益于很多其他网友的总结.本博客文章是在学习的基础上,加上博主在日常工作中的使用C++11的一些总结.经验和感悟,整理出来,分享给大家,希望对各位读者有帮助,文章中的总结可能存在很多不完整或有错误的地方,也希望读者指出.大家可以根据如下目录跳到自己需要的章节. 1.关键字及新语法 1.1.auto关键字及用法 1.2.nullptr关键字及用法 1.3.

C++多线程编程时的数据保护_C 语言

 在编写多线程程序时,多个线程同时访问某个共享资源,会导致同步的问题,这篇文章中我们将介绍 C++11 多线程编程中的数据保护.数据丢失 让我们从一个简单的例子开始,请看如下代码:   #include <iostream> #include <string> #include <thread> #include <vector> using std::thread; using std::vector; using std::cout; using std

新东方Q3关闭22个教学点新开11个

多知网4月24日消息,新东方今日发布了2013财年第三财季报.俞敏洪表示,Q3新东方共关闭22个状况不佳的教学中心,裁员1200人,但同时在增长迅速的市场新开设了11个教学中心.经过这一番增减,新东方教学中心数量从上一季度的744个变为733个.发布上一季度财报时,新东方CFO谢冬萤表示,未来将关闭15至25个教学中心,四个月内裁员1000至1500名.从今天公布的数字看,优化人员和教学中心的工作可能暂时告一段落.这一季度新东方共斥资1350万美元,在发展迅速的市场增设了11个教学中心,还以26

Android学习路线总结,绝对干货

title: Android学习路线总结,绝对干货 tags: Android学习路线,Android学习资料,怎么学习android grammar_cjkRuby: true --- 一.前言 不知不觉自己已经做了几年开发了,由记得刚出来工作的时候感觉自己能牛逼,现在回想起来感觉好无知.懂的越多的时候你才会发现懂的越少. 如果你的知识是一个圆,当你的圆越大时,圆外面的世界也就越大. 最近看到很多Android新手问Android学习路线,学习方法啊,如何入门啊,所以我从网上找了一些资料,然后

android开发的学习路线

第一阶段:Java面向对象编程 1.Java基本数据类型与表达式,分支循环.  2.String和StringBuffer的使用.正则表达式.  3.面向对象的抽象,封装,继承,多态,类与对象,对象初始化和回收:构造函数.this关键字.方法和方法的参数传递过程.static关键字.内部类,Java的垃极回收机制,Javadoc介绍.  4.对象实例化过程.方法的覆盖.final关键字.抽象类.接口.继承的优点和缺点剖析:对象的多态性:子类和父类之间的转换.抽象类和接口在多态中的应用.多态带来的

过来人公开课:做中国的Coursera

无论何时何地,只要有网络.只要你愿意,就可以免费学习中外一流大学的课程,并且还有机会获得推荐信和学分,这就是过来人公开课试图打造的情景.过来人公开课网站截图(TechWeb配图)过来人公开课是过来人国际教育科技集团旗下的新业务.过来人国际教育科技集团专注于青年成长,业务主要是帮助大学生就业求职服务以及培训,其三位创始人中王迈.陈日佑来自清华,张有明来自北大.目前管理团队除创始人外,还包括天使投资者李志文教授和新东方徐小平老师.2010年过来人开始探索网络教育,第二年就获得数千万元的A轮投资,此后