没有合适的资源?快使用搜索试试~ 我知道了~
linux系统下多线程编程.PDF
需积分: 9 29 下载量 136 浏览量
2013-03-12
16:53:18
上传
评论
收藏 1.37MB PDF 举报
温馨提示
试读
171页
详细的讲述了linux c开发中涉及到的多线程编程问题,171页,能很好的帮助初学者入门
资源推荐
资源详情
资源评论
Linux
Linux
Linux
Linux 系统下的多线程编程入门
引言
线程( thread )技术早在 60 年代就被提出,但真正 应用多线程到操作系统中去,是 在
80 年代中期 , solaris 是这方面的佼佼者 。 传统的 Unix 也支持线程的概念 , 但是在一个进程
( process ) 中只允许有一个线程 , 这样多线程就意味着多进程 。 现在 , 多线程技术已经被许
多操作系统所支持,包括 Windows/NT ,当然,也包括 Linux 。
为什么有了进程的概念后 , 还要再引入线程呢?使用多线程到底有哪些好处?什么的系
统应该选用多线程?我们首先必须回答这些问题。
使用多线程的理由之一是和进程相比 , 它是一种 非常 " 节俭 " 的多任务操作方式 。 我们知
道 , 在 Linux 系统下 , 启动一个新的进程必须分配给它独立的地址空间 , 建立众多的数据表
来维护它的 代码段 、 堆栈段和数据段 , 这是一种 " 昂贵 " 的多任务工作方式 。 而运行于一个进
程中的多个线程 , 它们彼此之间使用相同的地址空间 , 共享大部分数据 , 启动一个线程所花
费的空间远远小于启动一个进程所花费的空间 , 而且 , 线程间彼此切换所需的时间也远远小
于进程间切换所需要的时间。
使用多线程的理由之二是 线程间方便的通信机制 。 对不同进程来说 , 它们具有独立的数
据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便
。
线程则不然 , 由于同一进程下的线程之间共享数据空间 , 所以一个线程的数据可以直接为其
它线程所用 , 这不仅快捷 , 而且方便 。 当然 , 数据的共享也带来其他一些问题 , 有的变量不
能同时被两个线程所修改 , 有的子程序中声明为 static 的数据更有可能给多线程程序带来灾
难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外 , 不和进程比较 , 多线程程序作为一种多任务 、 并发的工作方式
,
当然有以下的优点:
1) 提高应用程序响应 。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系
统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术 , 将
耗时长的操作( time consuming )置于一个新的线程,可以避免这种尴尬的情况。
2) 使多 CPU 系统更加有效 。操作系统会保证当线程数不大于 CPU 数目时,不同的线程运
行于不同的 CPU 上。
3) 改善程序结构 。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立
的运行部分,这样的程序会利于理解和修改。
下面我们先来尝试编写一个简单的多线程程序。
简单的多线程编程
Linux 系统下的多线程遵循 POSIX 线程接口, 称为 pthread 。编写 Linux 下的多线程程
序 , 需要使用头文件 pthread.h , 连接时需要使用库 libpthread.a 。 顺便说一下 , Linux 下 pthrea d
的实现是通过系统调用 clone ()来实现的 。 clone ()是 Linux 所特有的系统调用,它的使
用方式类似 fork ,关于 clone ()的详细情况,有兴趣的读者可以去查看有关文档说明。下
面我们展示一个最简单的多线程程序 example1.c 。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.n");
2
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0)
{
printf ("Create pthread error!n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.n");
pthread_join(id,NULL);
return (0);
}
我们编译此程序:
gcc example1.c -lpthread -o example1
运行 example1 ,我们得到如下结果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次运行,我们可能得到如下结果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前后两次结果不一样,这是两个线程争夺 CPU 资源的结果。上面的示例中,我们使用
到了两个函数, pthread_create 和 pthread_join ,并声明了一个 pthread_t 型的变量。
pthread_t 在头文件 /usr/include/bits/pthreadtypes.h 中定义:
typedef unsigned long int pthread_t;
3
它是一个线程的标识符。函数 pthread_create 用来创建一个线程,它的原型为:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,void
*(*__start_routine) (void *), void *__arg));
第一个参数为 指向线程标识符的指针 , 第二个参数用来设置 线程属性 , 第三个参数是 线
程运行函数的起始地址 , 最后一个参数是 运行函数的参数 。 这里 , 我们的函数 thread 不需要
参数 , 所以最后一个参数设为空指针 。 第二个参数我们也设为空指针 , 这样将生成默认属性
的线程。对线程属性的设定和修改我们将在下一节阐述。 当创建线程成功时,函数返回 0
,
若不为 0 则说明创建线程失败,常见的错误返回代码为 EAGAIN 和 EINVAL 。前者表示系
统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法
。
创建线程成功后 , 新创建的线程则运行参数三和参数四确定的函数 , 原来的线程则继续运行
下一行代码。
函数 pthread_join 用来等待一个线程的结束 。函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一个参数为被等待的线程标识符 , 第二个参数为一个用户定义的指针 , 它可以用来存
储被等待线程的返回值 。 这个函数是一个线程阻塞的函数 , 调用它的函数将一直等待到被等
待的线程结束为止 , 当函数返回时 , 被等待线程的资源被收回 。 一个线程的结束有两种途径
,
一种是象我们上面的例子一样 , 函数结束了 , 调用它的线程也就结束了 ; 另一种方式是通过
函数 pthread_exit 来实现。 它的函数原型为:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的参数是函数的返回代码,只要 pthread_join 中的第二个参数 thread_return 不 是
NULL , 这个值将被传递给 thread_return 。 最后要说明的是 , 一个线程不能被多个线程等待
,
否则第一个接收到信号的线程成功返回,其余调用 pthread_join 的线程则返回错误代 码
ESRCH 。
在这一节里 , 我们编写了一个最简单的线程 , 并掌握了 最常用的三个函 数 pthread_create
,
pthread_join 和 pthread_exit 。 下面 , 我们来了解线程的一些常用属性以及如何设置这些属性
。
修改线程的属性
在上一节的例子里,我们用 pthread_create 函数创建了一个线程,在这个线程中,我们
使用了默认参数 , 即将该函数的第二个参数设为 NULL 。 的确 , 对大多数程序来说 , 使用默
认属性就够了,但我们还是有必要来了解一下线程的有关属性。
属性结构为 pthread_attr_t , 它同样在头文件 /usr/include/pthread.h 中定义 , 喜欢追根问底
的人可以自己去查看。 属性值不能直接设置,须使用相关函数进行操作,初始化的函数 为
pthread_attr_init , 这个函数必须在 pthread_create 函数之前调用 。 属性对象主要包括是否绑定
、
是否分离 、 堆栈地址 、 堆栈大小 、 优先级 。 默认的属性为非绑定 、 非分离 、 缺省 1M 的堆栈
、
与父进程同样级别的优先级。
关于线程的绑定,牵涉到另外一个概念:轻进程( LWP : Light Weight Process ) 。轻进
程可以理解为内核线程 , 它位于用户层和系统层之间 。 系统对线程资源的分配 、 对线程的控
制是通过轻进程来实现的 , 一个轻进程可以控制一个或多个线程 。 默认状况下 , 启动多少轻
进程 、 哪些轻进程来控制哪些线程是由系统来控制的 , 这种状况即称为非绑定的 。 绑定状况
下 , 则顾名思义 , 即某个线程固定的 " 绑 " 在一个轻进程之上 。 被绑定的线程具有较高的响应
速度,这是因为 CPU 时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它
总有一个轻进程可用 。 通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足
诸如实时反应之类的要求。
4
设置线程绑定状态的函数为 pthread_attr_setscope , 它有两个参数 , 第一个是指向属性结
构的指针,第二个是绑定类型,它有两个取值: PTHREAD_SCOPE_SYSTEM (绑定的 ) 和
PTHREAD_SCOPE_PROCESS (非绑定的 ) 。下面的代码即创建了一个绑定的线程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/* 初始化属性值,均设为默认值 */
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
线程的分离状态决定一个线程以什么样的方式来终止自己 。 在上面的例子中 , 我们采用
了线程的默认属性 , 即为非分离状态 , 这种情况下 , 原有的线程等待创建的线程结束 。 只有
当 pthread_join ()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而
分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了
,
马上释放系统资源 。 程序员应该根据自己的需要 , 选择适当的分离状态 。 设置线程分离状态
的函数为 pthread_attr_setdetachstate ( pthread_attr_t *attr, int detachstate ) 。第二个参数可选 为
PTHREAD_CREATE_DETACHED ( 分离线程 ) 和 PTHREAD _CREATE_JOINABLE ( 非分
离线程 ) 。 这里要注意的一点是 , 如果设置一个线程为分离线程 , 而这个线程运行又非常快
,
它很可能在 pthread_create 函数返回之前就终止了,它终止以后就可能将线程号和系统资源
移交给其他的线程使用,这样调用 pthread_create 的线程就得到了错误的线程号。要避免这
种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里 调 用
pthread_cond_timewait 函数 , 让这个线程等待一会儿 , 留出足够的时间让函数 pthread_creat e
返回。 设置一段等待时间,是在多线程编程里常用的方法 。但是注意不要使用诸如 wait (
)
之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
另外一个可能常用的属性是线程的优先级,它存放在结构 sched_param 中。用函 数
pthread_attr_getschedparam 和函数 pthread_attr_setschedparam 进行存放,一般说来,我们总
是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
线程的数据处理
5
和进程相比 , 线程的最大优点之一是数据的共享性 , 各个进程共享父进程处沿袭的数据
段 , 可以方便的获得 、 修改数据 。 但这也给多线程编程带来了许多问题 。 我们必须当心有多
个不同的进程访问相同的变量 。 许多函数是不可重入的 , 即同时不能运行一个函数的多个拷
贝(除非使用不同的数据段 ) 。在函数中声明的静态变量常常带来问题,函数的返回值也会
有问题 。 因为如果返回的是函数内部静态声明的空间的地址 , 则在一个线程调用该函数得到
地址后使用该地址指向的数据时 , 别的线程可能调用此函数并修改了这一段数据 。 在进程中
共享的变量必须用关键字 volatile 来定义 , 这是为了防止编译器在优化时 ( 如 gcc 中使用 -O X
参数 ) 改变它们的使用方式 。 为了保护变量 , 我们必须使用信号量 、 互斥等方法来保证我们
对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。
1 、线程数据
在单线程的程序里,有两种基本的数据:全局变量和局部变量 。但在 多线程程序 里 , 还
有第三种数据类型: 线程数据 ( TSD: Thread-Specific Data ) 。它和全局变量很象,在线程内
部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的 。 这
种数据的必要性是显而易见的。例如我们常见的变量 errno ,它返回标准的出错信息。它显
然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量 , 否
则在 A 线程里输出的很可能是 B 线程的出错信息。要实现诸如此类的变量, 我们就必须使
用线程数据 。 我们为每个线程数据创建一个键 , 它和这个键相关联 , 在各个线程里 , 都使用
这个键来指代线程数据 , 但在不同的线程里 , 这个键代表的数据是不同的 , 在同一个线程里
,
它代表同样的数据内容。
和线程数据相关的函数主要有 4 个 : 创建一个键 ; 为一个键指定线程数据 ; 从一个键读
取线程数据;删除键。
创建键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function)
(void *)));
第一个参数为指向一个键值的指针,第二个参数指明了一个 destructor 函数,如果这个
参数不为空 , 那么当每个线程结束时 , 系统将调用这个函数来释放绑定在这个键上的内存块
。
这个函数常和函 数 pthread_once ((pthread_once_t*once_control, void (*initroutine) (void))) 一起
使用,为了让这个键只被创建一次。函数 pthread_once 声明一个初始化函数,第一次调 用
pthread_once 时它执行这个函数,以后的调用将被它忽略。
在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函 数
createWindow ,这个函数定义一个图形窗口(数据类型为 Fl_Window * ,这是图形界面开发
工具 FLTK 中的数据类型 ) 。由于各个线程都会调用这个函数,所以我们使用线程数据。
/* 声明一个键 */
pthread_key_t myWinKey;
/* 函数 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 调用函数 createMyKey ,创建键 */
pthread_once ( & once, createMyKey) ;
/*win 指向一个新建立的窗口 */
剩余170页未读,继续阅读
资源评论
ytqwq
- 粉丝: 0
- 资源: 1
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功