本篇教学代码可在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

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

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

    C++11多线程教学II 从我最近发布的C++11线程教学文章里,我们已经知道C++11线程写法与POSIX的pthreads写法相比,更为简洁.只需很少几个简单概念,我们就能搭建相当复杂的处理图片程 ...

  2. C++11多线程教学II

    从我最近发布的C++11线程教学文章里,我们已经知道C++11线程写法与POSIX的pthreads写法相比,更为简洁.只需很少几个简单概念,我们就能搭建相当复杂的处理图片程序,但是我们回避了线程同步 ...

  3. c++ 11 多线程教学(1)

    本篇教学代码可在GitHub获得:https://github.com/sol-prog/threads. 在之前的教学中,我展示了一些最新进的C++11语言内容: 1. 正则表达式(http://s ...

  4. C++11多线程教学

    转自:http://www.cnblogs.com/lidabo/p/3908705.html 本篇教学代码可在GitHub获得:https://github.com/sol-prog/threads ...

  5. C++11 多线程 教学(2)

      C++11开始支持多线程编程,之前多线程编程都需要系统的支持,在不同的系统下创建线程需要不同的API如pthread_create(),Createthread(),beginthread()等, ...

  6. 【转】C++ 11 并发指南一(C++ 11 多线程初探)

    引言 C++ 11自2011年发布以来已经快两年了,之前一直没怎么关注,直到最近几个月才看了一些C++ 11的新特性,算是记录一下自己学到的东西吧,和大家共勉. 相信Linux程序员都用过Pthrea ...

  7. C++11 并发指南一(C++11 多线程初探)

    引言 C++11 自2011年发布以来已经快两年了,之前一直没怎么关注,直到最近几个月才看了一些 C++11 的新特性,今后几篇博客我都会写一些关于 C++11 的特性,算是记录一下自己学到的东西吧, ...

  8. C++11 多线程编程 使用lambda创建std::thread (生产/消费者模式)

    要写个tcp server / client的博客,想着先写个c++11多线程程序.方便后面写博客使用. 目前c++11中写多线程已经很方便了,不用再像之前的pthread_create,c++11中 ...

  9. C++11 并发指南九(综合运用: C++11 多线程下生产者消费者模型详解)

    前面八章介绍了 C++11 并发编程的基础(抱歉哈,第五章-第八章还在草稿中),本文将综合运用 C++11 中的新的基础设施(主要是多线程.锁.条件变量)来阐述一个经典问题——生产者消费者模型,并给出 ...

随机推荐

  1. [转]mysql-5.6.17-win32免安装版配置

    1. 下载mysql-5.6.17-win32:官网下载地址百度 2. 解压到自定义目录,我这里演示的是D:\wamp\mysql\ 3. 复制根目录下的my-default.ini,改名为my.in ...

  2. mysql 不能远程连接

    不想浪费大家时间,我这文章记录了我在vagrant上架的mysql远程连接不上的问题,不过我在整理时发现这个下面这个链接,如果我一开始能找到这个我就不会绕那么多弯了.不想看我是怎么一步步调错过程的请直 ...

  3. Mysql Java type mapping

    MySQL Type Java Type ---------- --------- CHAR String VARCHAR String LONGVARCHAR String NUMERIC java ...

  4. SQL Server 2008 安装或卸载时提示“重启计算机失败"的解决办法(转)

    安装或卸载SQL Server 遇到错误提示:以前的某个程序安装已在安装计算机上创建挂起的文件操作.运行安装程序之前必须重新启动计算机.如下图: 解决办法: 1.在开始->运行中输入regedi ...

  5. php下删除一篇文章生成的多个静态页面

    php自定义函数之删除一篇文章生成的多个静态页面,可能有多页的文章,都是需要考虑到的. 复制代码代码如下: //– 删除一篇文章生成的多个静态页面  //– 生成的文章名为 5.html 5_2.ht ...

  6. linux安装Vmware的时候出现“Could not open /dev/vmmon”

    在centos6.6上安装了Vmware之后运行出现下列问题 VMware Workstation : Could not open /dev/vmmon: No such file or direc ...

  7. VB.Net 字符串加密类

    Public Class Cls_JM '使用 'Dim Jm As New Cls_JM(2) 'Dim strTmp As String 'Jm.jiemi(strTmp) 'Jm.Jiami(s ...

  8. The content of element type "sqlMapConfig" is incomplete,

    The content of element type "sqlMapConfig" is incomplete, it must match "(properties? ...

  9. linux下使用sqlplus使用上下键显示历史命令

    在linux下使用sqlplus没有在windows下方便,既不好用习惯的退格键,也无法通过上键来显示上一条的命令,这里我们使用rlwrap来解决. 1.安装rlwrap [root@toughhou ...

  10. SQL重复记录查询(转载)

     1.查找表中多余的重复记录,重复记录是根据单个字段(peopleId)来判断 select * from people )  例二:  select * from testtable  where ...