首页 > 代码库 > 多线程编程基础

多线程编程基础

一、线程概念

1、引入

我们知道,进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap或者进程间通信机制,本篇我们将学习如何在一个进程的地址空间中执行多个线程。有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了用场,比如实现一个图形界面的下载软件, 一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件, 等待和处理从多个网络主机发来的数据,这些任务都需要一个“等待-处理”的循环,可以用多线程实现,一个线程专门负责与用户交互,另外一个线程负责和一个网络主机通信。

2、什么叫线程

在一个程序里的多个执行路线就叫做线程。更准确的定义是:线程是“一个进程内部的一个控制序列”。典型的unix进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程以后,在程序设计时可以把进程设计成在同一时刻能够做不止一件事,每个线程处理各只独立的任务。线程可以看作是轻量级进程,它是操作系统调度的基本单位。main函数和信号处理函数是同一个进程地址空间中的多个控制流程,多线程也是如此,但是比信号处理函数更加灵活,信号处理函数的控制流程只是在信号递达时产生,在 处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。

3、线程特性

同一进程的多个线程共享同一地址空间。其中Text Segment、 Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:(1)线件描述符表;(2)每种信号的处理方式(SIG_IGN、 SIG_DFL或者自定义的信号处理函数);(3)当前工作目录;(4)用户id和组id。但有些资源是每个线程独有一分的:(1)线程id;(2)上下文,包括各种寄存器的值、程序计数器和栈指针;(3)栈空间;(4) errno变量;(5)信号屏蔽字;(6)调度优先级。

4、线程的优缺点

优点:(1)通过为每种事件类型的处理分配单独的线程,能够简化处理异步时间的代码;
         (2)多个线程可以自动共享相同的存储地址空间和文件描述符;
         (3)有些问题可以通过将其分解从而改善整个程序的吞吐量;
         (4)交互的程序可以通过使用多线程实现相应时间的改善,多线程可以把程序中处理用户输入输出的部分与其它部分分开。

缺点:线程也有不足之处。编写多线程程序需要更全面更深入的思考。在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的。调试一个多线程程序也比调试一个单线程程序困难得多。

二、线程控制

1、线程的创建

名称:pthread_create
功能:创建线程
头文件:#include <pthread.h>
函数原形:int pthread_create(pthread_t *thread,const pthread _attr_t *attr,void *(*start_routine)(void*),void *restrict arg);
参数:第一个参数thread指id
返回值:若成功返回则返回0,否则返回错误编号

 技术分享

当pthread_creat成功返回时, tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性。可以把它设置为NULL,创建默认的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

技术分享

关于进程的编译我们都要加上参数 –lpthread 否则提示找不到函数的错误
具体编译方法是 gcc –lpthread –o pthread pthread.c
运行结果为

技术分享

 以前学过的系统函数都是成功返回0,失败返回-1,将错误号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收多个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。 start_routine的返回值类型也是void *,这 个指针的含义同样由调用者自己定义。 start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态, 后面在详细介绍pthread_join。

3、名称:pthread_self
功能:获取自身线程的id
头文件:#include <pthread.h>
函数原形:pthread_t pthread_self(void);
参数:无
返回值:调用线程的线程id

技术分享上例中就已经用到了这个函数。

3、线程的终止

  线程是依进程而存在的,当进程终止时,线程也就终止了。当然也有在不终止整个进程的情况下停止它的控制流。
1)从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2) 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3)线程可以调用pthread_exit终止自己。 用pthread_cancel终止一个线程分同步和异步两种情况,比较复杂,后面再详细介绍。

(1)名称:pthread_exit
功能:终止一个线程 
头文件:#include <pthread.h>
函数原形:void pthread_exit(void *rval_ptr);
参数:rval_prt是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以调用pthread_join函数访问到这个指针。
返回值:无 

技术分享

retval是void *类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join获得这个指针。 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

(2)线程等待

名称:pthread_join
功能:调用该函数的线程将挂起等待,直到id为thread的线程终止。
头文件:#include <pthread.h>
函数原形:int pthread_join(pthread_t thread,void **rval_ptr);
参数:
返回值:若成功返回0,否则返回错误编号。

 技术分享

thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ptr所指向的单元内存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_cancel异常终掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr 参数。
当一个线程通过调用pthread_exit退出或者简单地从启动历程中返回时,进程中的其他线程可以通过调用pthread_join函数获得进程的退出状态调用pthread_join进程将一直阻塞,直到指定的线程调用pthread_exit,从启动例程中或者被取消。
如果线程只是从它的启动历程返回,rval_ptr将包含返回码。

技术分享

技术分享

输出结果如下:

技术分享

 可见在Linux的pthread库中常数PTHREAD_CANCELED的值是-1。可以在头文件pthread.h 中找到它的定义。
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach 状态,这样的线程一旦终止就立刻回收它占用的所有资源, 而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。 对一个尚未detach的线程调用pthread_join或pthread_detach都可以把该线程 置为detach状态,也 就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。技术分享返回值:成功返回0,失败返回错误号。

 




 

多线程编程基础