POSIX线程 (1)

本文介绍了POSIX线程的概念,包括线程的定义、优缺点以及如何在Linux系统中创建和管理线程。通过讲解pthread_create、pthread_exit和pthread_join等函数,阐述了线程的创建、退出和同步,并提供了简单的线程程序示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2011-11-06 wcdj


BLP 4th P.416

类UNIX操作系统早就具备了多进程的功能了,但有时人们认为,用fork调用来创建新进程的代价太高。在这种情况下,如果能让一个进程同时做两件事情或至少看起来是这样将会非常有用。而且,你可能希望能有两件或更多的事情以一种非常紧密的方式同时发生。这就是需要线程发挥作用的时候了。
知识点:
(1) 在进程中创建新线程。
pthread_create 函数
pthread_exit 函数
pthread_join 函数
(2) 在一个进程中同步线程之间的数据访问。
(3) 修改线程的属性。
(4) 在同一个进程中,从一个线程中控制另一个线程。

1 什么是线程

在一个程序中的多个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的一个控制序列。虽然Linux和许多其他的操作系统一样,都擅长同时运行多个进程,但迄今为止我们看到的所有程序在执行时都是作为一个单独进程。事实上,所有的进程都至少有一个执行线程。
注意:
弄清楚fork系统调用和创建新线程之间的区别非常重要。
(1) 当进程执行fork调用时,将创建出该进程的一份新副本。这个新进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行(通常)几乎完全独立于父进程。
(2) 当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此,也有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。

现在,多核处理器已非常普遍,大多数机器在底层硬件上就已物理支持了同时执行多个线程。而此前,对于单核CPU来说,线程的同时执行只是一个聪明、但非常有效的幻觉。
Linux在1996年第一次获得线程的支持,我们常把当时使用的函数库称为LinuxThread。LinuxThread已经和POSIX的标准非常接近了(事实上,从许多方面来看,它们之前的区别并不明显),它使Linux程序员第一次可以在Linux系统中使用线程。但是,在Linux的线程实现版本中和POSIX标准之间还是存在着细微的差别,最明显的是关于信号处理部分。这些差别中的大部分都受底层Linux内核的限制,而不是函数库实现所强加的。

NPTL(Native POSIX Thread Library, Linux上的本地POSIX线程库)将成为Linux线程的新标准。第一个NPTL的主流版本出现在Red Hat Linux版本9上。关于NPTL的背景资料可以参考“Linux上的本地POSIX线程库”,作者是Ulrich Drepper和Ingo Molnar。
下载地址:http://people.redhat.com/drepper/nptl-design.pdf

2 线程的优点和缺点

在某些情况下,创建新线程要比创建新进程更有明显的优势。新线程的创建代价要比新进程小得多(虽然与其他一些操作系统相比,Linux在创建新进程方面的效率是很高的)。
使用线程的 —— 优点
(1) 有时,让程序看起来好像是在同时做两件事情是很有用的。
多任务的工作如果用多进程的方式来完成将很难做到高效,因为各个不同进程必须紧密合作才能满足加锁和数据一致性方面的要求,而用多线程来完成就比用多进程要容易得多。
(2) 一个混杂着输入、计算和输出的应用程序,可以将这几个部分分离为3个线程来执行,从而改善程序执行的性能。一个需要同时处理多个网络连接的服务器应用程序也是一个天生适用于应用多线程的例子。
(3) 一般而言,线程之间的切换需要操作系统做的工作要比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程。
使用线程的 —— 缺点
(1) 编写多线程程序需要非常仔细的设计。在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发的错误的可能性是很大的。
(2) 对多线程程序的调试要比对单线程程序的调试困难得多,因为线程之间的交互非常难于控制。
(3) 将大量计算分成两个部分,并把这两个部分作为两个不同的线程来运行的程序,在一台单处理器机器上并不一定运行得更快,除非计算确实允许它的不同部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理。

3 第一个线程程序

线程有一套完整的与其有关的函数库调用,它们中的绝大多数函数名都以 pthread_ 开头。
为了使用这些函数库调用,我们必须
(1) 定义宏 _REENTRANT
(2) 在程序中包含头文件pthread.h,
(3) 并且在编译程序时需要用选项 -lpthread 来链接线程库。

在设计最初的UNIX和POSIX库例程时,人们假设每个进程中只有一个执行线程。一个明显的例子就是errno,该变量用于获取某个函数调用失败后的错误信息。在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个全局性区域来缓存输出数据。
为了解决这个问题,我们需要使用被称为 —— 可重入的例程。可重入代码可以被多次调用而仍能正常工作。这些调用可以来自不同的线程,也可以是某种形式的嵌套调用。因此,代码中的可重入部分通常只使用局部变量,这使得每次对该代码的调用都将获得它自己的唯一的一份数据副本。

编写多线程程序时,我们通过定义 _REENTRANT 来告诉编译器我们需要可重入功能,这个宏的定义必须位于程序中的任何 #include 语句之前。它将为我们做3件事,并且做得非常优雅,以至于我们一般不需要知道它到底做了哪些事:
(1) 它会对部分函数重新定义它们的可安全重入的版本,这些函数的名字一般不会发生改变,只是会在函数名后面添加 _r 字符串。例如,函数名 gethostbyname 将变为 gethostbyname_r。
(2) stdio.h 中原来以宏的形式实现的一些函数将变成可安全重入的函数。
(3) 在errno.h中定义的变量errno现在将成为一个函数调用,它能够以一种多线程安全的方式来获取真正的errno值。

pthread_create 函数
首先来看一个用于管理线程的新函数 pthread_create,它的作用是创建一个新线程,类似于创建新进程的 fork 函数。它的定义为:
#include <pthread.h>
int pthread_create( pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg );

参数解释:
第一个参数是指向pthread_t类型数据的指针。线程被创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。
第二个参数是用于设置线程的属性。一般不需要特殊的属性,所以只需设置该参数为NULL。
第三个参数是告诉线程将要启动执行的函数。
第四个参数是传递给执行函数的参数。

注意:
(1) void *(* start_routine)(void *)
第三个参数要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针。因此,可以传递一个任一类型的参数并返回一个任一类型的指针。
(2) 区别:用 fork 调用厚,父子进程将在同一位置继续执行下去,只是 fork 调用的返回值是不同的;但对新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。
(3) pthread_create函数的调用成功时,返回值是0,如果失败则返回错误代码。详细可以参考手册页。
PS: pthread_create和大多数pthread_系列函数一样,在失败时并未遵循UNIX函数的习惯返回-1,这种情况在UNIX函数中属于一少部分。所以,除非你很有把握,在对错误码代码进行检查之前一定要仔细阅读使用手册中的有关内容。

pthread_exit 函数
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是:终止调用它的线程并返回一个指向某个对象的指针。
注意:绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不再存在了,这将引起严重的程序漏洞。
pthread_exit函数的定义为:
#include <pthread.h>
void pthread_exit(  void *retval );


pthread_join 函数
pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数。
pthread_join函数的定义为:
#include <pthread.h>
int pthread_join( pthread_t th, void **thread_return );

第一个参数是:指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。
第二个参数是:一个指针,它指向另一个指针,而后者指向线程的返回值。
返回值:与pthread_create类似,这个函数在成功时返回0,失败时返回错误代码。

例子:一个简单的线程程序
这个程序创建一个新线程,新线程与原先的线程共享变量,并在结束时向原先的线程返回一个结果。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>// pthread header

void *thread_func(void *arg);// thread function
char msg[] = "hi wcdj";// shared between main thread and new thread

int main() {
	int res;
	pthread_t a_thread;
	void *thread_result;

	res = pthread_create(&a_thread, NULL, thread_func, (void *)msg);
	if (res != 0) {
		perror("Thread creation failed");
		exit(EXIT_FAILURE);
	}
	printf("Waiting for thread to finish...\n");
	res = pthread_join(a_thread, &thread_result);
	if (res != 0) {
		perror("Thread join failed");
		exit(EXIT_FAILURE);
	}
	printf("Thread joined, it returned %s\n", (char *)thread_result);
	printf("Message is now %s\n", msg);

	exit(EXIT_SUCCESS);
}

// Thread function
void *thread_func(void *arg){
	printf("thread_func is running. Argument was %s\n", (char *)arg);
	sleep(3);
	strcpy(msg, "Bye, wcdj");// modify the shared variable
	pthread_exit("Thank u 4 the CPU time");
}

编译程序:
pic


待续……






这是一个基于Delphi XE2的OpenCL控件。其中使用到了Khronos Group Inc.的CL.pas单元。 OpenCL的设计思路和OpenGL类似,对于大部分Delphi的设计者来说,非常不习惯,而且使用起来并不十分方便 设计这个TOpenCL控件的目的不是替代OpenCL的原生使用方式,而是为了开发者能够快速对OpenCL进行应用并且可以 用来测试性能和功能。 使用TOpenCL控件,可以象使用数据库控件那样方便的去调用OpenCL程序,不需要太多代码就可以运行一个OpenCL 的Kernel。这对于学习和深入研究OpenCL的性能有一个很好的铺垫。 使用OpenCL做并行计算的一个主要因素就是提高大数据量计算的速度,这和通常的业务处理类程序大不相同,因 此提升OpenCL的运行效率是至关重要的,本控件附带的Demo程序中,是对两个长度分别为8192和32的float数组,进行 一维卷积计算的。在选择不同的数据传递方式(如使用显存还是Host内存、使用只读方式还是可读写或者只写方式), 或者不同的Device(如在多核CPU上和GPU上运行Kernel程序)上运行,其效率相差是非常大的。 Demo程序中缺省的使用不显示获取结果的方式运行,缺省的数据传输是使用显存(CPU作为Device的时候,其实还 是系统内存)并Copy数据的方式,因此显示结果始终是0。当输出的参数传递方式改为直接使用系统内存指针的方式时, 不使用显示获取计算结果则是可以得到运算结果的。这些参数之间的差异,读者自行测试并仔细体会,通过调整,相信 可以得到最佳的运行方式。 Demo中包含了四个Kernel函数,分别是Convolution_Kernel_With_Barrier。这是一个带有同步函数Barrier的卷积 过程,并在卷积完成后,等待所有单元计算完毕,然后对结果进行微分(差商)处理,实际情况表明Barrier函数对GPU 的影响甚微,但如果使用CPU作为Device计算,则效率影响非常大,其耗时几乎和单核计算不相上下,估计是同步函数 在等候的过程中,引起了CPU对Catch竞争访问的结果吧。对这种情况,反倒不如拆分成两个Kernel进行单独计算,其累 积的计算时间基本上为两个独立Kernel耗时只和。 Differ_Kernel是单独进行微分计算的,是为了验证上面计算耗时结果的。 Convolution_Kernel是只进行卷积计算的,可以认为和Differ_Kernel前后执行,其结果应该和Convolution_Kerne- l_With_Barrier单独执行是一样的。 Convolution则是一个简单的计算过程,用来测试启动Kernel、等候数据等操作会占用的时间情况的。 OpenCL其实并不是想象中那么美妙,也不是想象中的那么复杂,但要使用好OpenCL,就必须认真的对待每个细节, 甚至到每一个函数调用或者if控制等,大家可以参考“http://hi.baidu.com/fsword73”,上面涉及到的很多方面,都是 可以提升Kernel运行效率的。 目前这个TOpenCL控件只是作者为了测试OpenCL运行效率编写的一个小的工具,作为一个测试工具或者技术积累阶段 的工具足矣,但在实际工程中,希望还是能够尽可能使用原生的调用方式,控件模式势必会带来一定的性能损失的,这是 无法克服的是一个实际情况,对于某些流式数据处理的计算而言,多次重复使用同一个Kernel对流式数据进行处理的,则 使用本控件应该不会造成太大的性能影响。 目前TOpenCL不支持多个Device同时工作,可以选择CPU、GPU或者APU作为首选设备, X86下运行正常,X64下运行仍有 问题,疑和cl.pas中对context等处理的方式不支持X64或者其他原因。 目前支持的OpenCL版本为1.2。控件没有考虑OpenCL和OpenGL协同工作的情况,需要做这方面应用或者测试的读者,请 自行处理。 一下是控件几个主要类的引用关系图。供参考。 由于时间的关系,不可能提供详细的使用说明,往谅解,有问题可邮件与作者联系或者QQ联系。 Mail:18909181984@189.cn QQ:57440981 TOpenCL --| | |--TclKernels --| |--- TclKernel --| | |-- TclK
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值