linux程序设计——第一个线程程序(第十二章)

第12章    POSIX线程

在第11章中,介绍了如何在linux中处理进程.但有时人们认为,用fork调用来创建新进程的代价太高.在这种情况下,如果能让一个进程同时做两件事情或者至少看起来是这样会非常有用,而且,人们页希望能有两件或更多的事情以一种非常紧密的方式同时发生,这就需要线程发挥作用了.

12.1    什么是线程

一个程序中多个执行路线就叫做线程(thread).更准确的定义是:线程是一个进程内部的一个控制序列.虽然linux和许多其他的操作系统一样,都擅长同时运行多个进程,但迄今为止看到的所有程序都是在执行时都是作为一个单独的进程.事实上,所有的进程都至少有一个执行线程.到目前为止,看到的所有的进程都只有一个执行线程.

弄清楚fork系统调用和创建新线程之间的区别非常重要的.当程序执行fork调用时,将创建出该进程的一份新副本.这个进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行几乎完全独立于父进程.当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此也有自己的局部变量)但与它的创建者共享全局变量,文件描述符,信号处理函数和当期目录状态.

linux系统在1996年第一次获得对线程的支持,常把当时使用的函数库成为LinuxThread.它是linux程序设计中迈出的重要的一步,它使linux程序员第一次可以在linux系统中使用线程.但是,在linux的线程实现版本和POSIX标准之间还是存在着细微的差别,最明显的是关于信号处理部分.这些差别中的大部分都受底层linux内核的限制,而不是函数库实现所强加的.

许多项目都在研究如何才能改善linux对线程支持,这种改善不仅仅是清除POSIX标准和linux具体实现之间的细微的差别,而且要增强linux线程的性能和删除一些不需要的限制,其中大部分工作都集中在如何将用户级的线程映射到内核级的线程.在这些项目中两个主要的项目分别是下一代的POSIX线程(NGPT)和本地POSIX线程(NPTL).这两个项目都必须修改linux的内核来支持新的函数库.

12.2    线程优点和缺点

在某些环境中,创建新线程要比创建新进程有更明显的优势,新线程的创建代价要比进程小的多.

线程有下面的一些优点:

1.有时,让程序看起来好像是在同时做两件事情是很有用的.一个经典的例子是,在编辑文档的同时对文档中的单词个数进行实时统计.一个线程负责处理用户的输入并执行文本编辑工作,另一个(它也可以看到相同的文档内容)则不断刷新单词计数变量.第一个进程通过这个共享的计数变量让用户随时了解自己的工作进展情况.另一个例子是一个多线程的数据库服务器,这种一种明显的单进程服务多用户的情况.它会在响应一些请求的同时阻塞另外一些请求,使之等待磁盘操作,从而改善整体上的数据吞吐量.对于数据库服务器来说,这个明显的多任务工作如果用多进程的方式来完成将很难做到高效,因为各个不同的进程必须紧密合作才能满足加锁和数据一致性方面的需求,而用多线程来完成就比用多进程要容易的多.

2.一个混杂着输入,计算和输出的引用程序,可以将这几个部分分离为3个线程来执行,从而改善程序执行的性能.当输入或输出线程需要等待连接时,另外一个线程可以继续执行.因此,如果一个进程在任一时刻最多只能做一件事情的话,线程可以让它在等待连接之类的事情的同时做一些其他有用的事情.一个需要同时处理多个网络连接的服务器应用程序也是天生适合用于应用多线程的例子.

3.一般而言,线程之间的切换需要操作系统做的工作要比进程之间的切换少的多,因此多个线程对资源的需求要远小于多个进程.如果一个程序在逻辑上需要有多个执行线程,那么在单处理器系统上把它运行为一个多线程程序才更符合实际情况.虽然如此,编写一个多线程的设计困难较大,不应等闲视之.

线程也有下面一些缺点:

1.编写多线程程序需要非常仔细的设计,在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发错误的可能性是很大的.

2.对多线程程序的调试要比单线程程序的调试困难的多,因为线程之间的交互非常难于控制

3.将大量计算分为两个部分,并把这两个部分作为两个不同的线程来运行的程序在一台单处理器机器上并不一定运行的更快,除非计算确实允许它的不同的部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理.

12.3    第一个线程程序

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

在设计最初的UNIX和POSIX库例程时,人们假设每个进程中只有一个执行线程,一个明显的例子就是errno,该变量用于获取某个函数调用失败后的错误信息.在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享.在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数所改变.类似的问题还存在与fputs之类的函数中,这些函数通常用一个全局性区域来缓存输出数据.

为了解决这个问题,需要使用被称为可重入的例程.可重入代码可以被多次调用而仍然正常工作,这些调用可以来自不同的线程,也可以是某种形式的嵌套调用.因此,代码中的可重入部分通常只使用局部变量,这使得每次对该代码的调用都将获得它自己的唯一的一份数据副本.

编写多线程程序时,通过定义宏_REENTRANT来告诉编译器需要可重入功能,这个宏的定义位于程序中的任何#include语句之前,它将做3件事情,并且做的非常优雅,以至于一般不需要知道它到底做了哪些事.

1.它会对部分函数重新定义它们的可安全重入的版本,这些函数的名字一般不会发生改变,只是会在函数名后面添加_r字符串.例如,函数名gethostbyname将变为gethostbyname_r.

2.stdio.h中原来以宏的形式实现的一些函数将变成可安全重入的函数

3.在errno.h中定义的变量errno现在将成为一个函数调用,它能够以一种多线程的安全的方式来获取真正的errno值

在程序中包含头文件pthread.h还将提供一些其他的将在代码中使用到的定义和函数原型,就如同在头文件stdio.h为标准输入和标准输入例程所提供的定义一样.最后,需要确保在程序中包含了正确的头文件,并且在编译程序时链接了实现pthread函数的正确的线程库.首先看一个用于管理线程的新函数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.

第三个参数告诉线程将要启动执行的函数

第四个参数是传递给启动执行的函数的参数

void *(start_routine) (void *)

上面这一行指出必须要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针.

因此,可以传递一个任一类型的参数并返回一个任一类型的指针.用fork调用后,父子进程将在同一位置继续执行下去,只是fork调用的返回值是不同的;但是对新线程来说,必须明确地提供给它一个函数指针,新线程将在这个位置开始执行.

该函数调用成功返回值是0,如果失败则返回错误代码.

线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样.这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针.绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不存在了.pthread_exit函数的定义如下所示:

#include <pthread.h>
void pthread_exit(void *retval);

pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数.这个函数的定义如下所示:

#include <pthread.h>
int pthread_joint(pthread_t th, void **thread_return);

第一个参数指定了将要等待的线程,线程通过pthread_create返回的标识符来指定

第二个参数是一个指针,它指向另一个指针,后者指向线程的返回值.与pthread_create类似,这个函数在成功时返回0,失败时返回错误代码.

编写thread1.c,这个程序创建一个新线程,新线程与原来的线程共享变量,并在结束时向原来的线程返回一个结果.

/*************************************************************************
 > File Name:    thread1.c
 > Description:  thread1.c创建一个新线程,新线程与原来的线程共享变量,并在结束时向原来的线程返回一个结果
 > Author:       Liubingbing
 > Created Time: 2015年07月04日 星期六 20时50分31秒
 > Other:		 编译thread1.c程序时,需要定义宏_REENTRANT,并且用选项-lpthread来链接线程库.
				 使用命令gcc -D_REENTRANT thread1.c -o thread1 -lpthread
 ************************************************************************/

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

void *thread_function(void *arg);

char message[] = "hello world";

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

	/* pthread_create类似于fork函数,它创建一个新线程,创建成功时返回0
	 * 第一个参数是指向pthread_t类型数据的指针,这个指针指向的变量中写入一个用来引入引用新线程的标识符
	 * 第二个参数是用来设置线程属性,一般为NULL
	 * 第三个参数是要线程将要启动执行的函数
	 * 第四个参数是传递给该函数的参数 */
	res = pthread_create(&a_thread, NULL, thread_function, (void *)message);
	if (res != 0){
		perror("Thread creation failed");
		exit(EXIT_FAILURE);
	}
	printf("Waiting for thread to finish...\n");

	/* pthread_join类似于用来收集子进程的信息的wait函数,成功时返回0
	 * 第一个参数指定将要等待的线程,线程通过pthread_create返回的标识符来指定
	 * 第二个参数是一个指针,它指向另一个指针,后者返回等待线程的返回值 */
	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", message);
	exit(EXIT_SUCCESS);
}

void *thread_function(void *arg){
	printf("thread_function is running. Argument was %s\n", (char *)arg);
	sleep(3);
	strcpy(message, "Bye!");

	/* pthread_exit终止调用它的线程并返回一个指向某个对象的指针 */
	pthread_exit("Thank you for the CPU time");
}

首先定义了在创建线程时需要由它调用的一个函数的原型,如下所示:

void *thread_function(void *arg);

根据pthread_create的要求,它只有一个指向void的指针作为参数,返回的也是指向void的指针.

在main函数中,定义了几个变量,然后调用pthread_create开始运行新线程,如下所示:

pthread_t a_thread;
void *thread_result;
res = pthread_create(&a_thread, NULL, thread_function, (void *)message);

向pthread_create函数传递了一个pthread_t类型对象的地址,可用用它来引用这个新线程.第二个参数设置为NULL.最后两个参数分别为将要调用的函数和一个传递给该函数的参数.

如果这个调用成功了,就会有两个线程在运行,原来的线程(main)继续执行pthread_create后面的代码,而新线程开始执行thread_function函数.

原来的线程在查明新线程已经开始启动后,将调用pthread_join函数,如下所示:

res = pthread_join(a_thread, &thread_result);

给该函数传递两个参数,一个是正在等待其结束的线程的标识符,另一个是指向线程返回值的指针.这个函数将等到它所指定的线程终止后才返回.然后主线程将带引新线程的返回值和全局变量message的值,最后退出.

新线程在thread_function函数中开始执行,它先打印出自己的参数,休眠一会儿,然后更新全局变量,最后退出并向主线程返回一个字符串.新线程修改了数组message,而原来的线程也可以访问该数组.如果调用的是fork而不是pthread_create,就不会有这样的效果.

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-07 05:27:03

linux程序设计——第一个线程程序(第十二章)的相关文章

linux程序设计——取消一个线程(第十二章)

12.7    取消一个线程 有时,想让一个线程能够要求还有一个线程终止,就像给它发送一个信号一样. 线程有方法能够做到这一点,与与信号处理一样.线程能够被要求终止时改变其行为. pthread_cancel是用于请求一个线程终止的函数: #inlude <pthread.h> int pthread_cancel(pthread_t thread); 这个函数提供一个线程标识符就能够发送请求来取消它. 线程能够用pthread_setcancelstate设置线程的取消状态 #include

linux程序设计——IPC状态命令(第十四章)

14.5    IPC状态命令 虽然X/Open规范并没有定义它们,但大多数linux系统都提供了一组命令,用于从命令行上访问IPC信息以及清理游离的IPC机制.它们是ipcs和ipcrm命令,这两个命令对于开发程序非常有用. IPC机制一个让人烦恼的问题是:编写错误的程序或者因为某些原因而执行失败的程序把它的IPC资源(如消息队列中的数据)遗留在系统中,并且这些资源在程序结束后很长时间仍然在系统中游荡.这将导致对程序的新调用执行失败,因为程序期望以一个干净的系统来启动,但事实上却发现一些遗留的

linux程序设计——CD唱片应用程序(第七章)

7.4 CD唱片应用程序 这篇为第七章的CD唱片应用程序,代码在CD唱片应用程序代码下载.我们使用dbm数据库对数据存储,改进之前的CD唱片应用程序. 7.4.1 更新设计 虽然在文件中以逗号分隔变量来存储信息是一种在shell中很容易实现的方式,但是这样局限性很大,因为许多CD标题和曲目都包含逗号.可以通过使用dbm数据库来改进这种方法. 将CD资料分为标题和曲目两个部分,并用不同的文件来保存它们. 前面的实现存在一个问题,即将应用程序的数据访问部分和用户接口部分混在了一起,这与程序全实现在一

Linux与云计算——第二阶段Linux服务器架设 第一十二章:数据库搭建—PostgreSQL

Linux与云计算--第二阶段Linux服务器架设 第一十二章:数据库搭建-PostgreSQL 1.1 安装PostgreSQL [1] 安装并启动PostgreSQL. [[email protected] ~]# yum -y install postgresql-server [[email protected] ~]# postgresql-setup initdb Initializing database ... OK [[email protected] ~]# vim /var

2017-2018-1 《Linux内核原理与设计》第十二周作业

<linux内核原理与设计>第十二周作业 Sql注入基础原理介绍 分组: 和20179215袁琳完成实验 一.实验说明 ??SQL注入攻击通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,本章课程通过 LAMP 搭建 Sql 注入环境,两个实验分别介绍 Sql 注入爆破数据库.Sql 注入绕过验证两个知识点. 首先通过下面命令将代码下载到实验楼环境中,作为参照对比进行学习. $ wget http://labfil

张季跃 201771010139《面向对象程序设计(java)》第十二周学习总结

张季跃 201771010139<面向对象程序设计(java)>第十二周学习总结 第二部分:实验部分 1.实验目的与要求 (1) 掌握Java GUI中框架创建及属性设置中常用类的API: (2) 掌握Java GUI中2D图形绘制常用类的API: (3) 了解Java GUI中2D图形中字体与颜色的设置方法: (4) 了解Java GUI中2D图像的载入方法. 2.实验内容和步骤 实验1: 导入第9章示例程序,测试程序并进行代码注释. 2.实验内容和步骤 实验1: 导入第10章示例程序,测试

Linux运维 第三阶段 (十二)tcp wrapper

Linux运维第三阶段(十二)tcp wrapper tcp wrapper tcp wrapper(工作在TCP层的访问控制工具,通常只对TCP协议的应用做控制,它本身只是个库文件libwrap.so(由glibc提供)) 当来自客户端的请求访问本机服务时,请求先到达本机网卡,再到内核TCP/IP协议栈,路由发现是访问本机的,转至用户空间服务所监听的套接字上,服务响应送至内核TCP/IP协议栈,再通过路由经网卡返回至客户端:有了tcp wrapper后,在这过程当中附加了一层访问控制机制,由t

第二十二章 Linux文件比较,文本文件的交集、差集与求差:comm命令

第二十二章 Linux文件比较,文本文件的交集.差集与求差:comm命令 名词解释 comm 命令 可以用于两个文件之间的比较,它有一些选项可以用来调整输出,以便执行交集.求差.差集操作. 交集:打印两个文件所共有的行 求差:打印出指定文件所包含的其不相同的行. 差集:打印出包含在一个文件中,但不包含在其他指定文件中的行. 语法 comm(选项)(参数) 选项 -1 :不显示在第一个文件出现的内容: -2 :不显示在第二个文件中出现的内容: -3 :不显示同时在两个文件中都出现的内容. ? 参数

C Primer Plus 第十二章程序清单……2015.5.10

C Primer Plus           第五版 第十二章  程序清单 #include<stdio.h> int main() { int x=30; printf("x in outer block:%d\n",x); { int x=77; printf("x in inner block:%d\n",x); } printf("x in outer block:%d\n",x); while(x++<33) { i