理论教育 操作系统实践:Linux基本线程编程

操作系统实践:Linux基本线程编程

时间:2023-11-25 理论教育 版权反馈
【摘要】:在Windows 系统、 Java 虚拟机、 Linux 系统上都有多线程编程。线程分内核级线程和用户级线程。给传递NULL 表示设置为默认线程属性。thread 线程以不同的方法终止, 通过pthread_join 的retval 指针得到的结果是不同的。主线程可以创建新线程, 新线程可以再创建新线程, POSIX 线程标准将它们视为等同层次。

操作系统实践:Linux基本线程编程

1. 理解线程概念及实现原理

现代操作系统中除了进程的概念, 还引入了线程。 之所以引入线程, 主要目的还是为了提高多道程序执行的并发性或并行性(多处理机上), 从而提高处理效率。 线程可以简单定义为进程内的一个执行线路, 多个线程共享其所属进程的资源。 在进程间切换调度CPU的管理开销相比线程是很高的, 而当把进程当作资源分配单位, 以线程为调度切换单位将大大提高效率。 比如单处理机系统下, 如果一个进程包含多个线程, 当该进程获得CPU 时,系统在它的线程间调度执行时切换开销会很小; 而如果在多处理机系统上, 一个进程的多个线程可以被分配到不同的CPU 上, 同时使用进程资源工作, 更是大幅度提高处理性能。

在Windows 系统、 Java 虚拟机、 Linux 系统上都有多线程编程。 用户所见的一般就是通过调用相关对象或函数, 建立所谓的线程。 但是这个线程在底层到底是如何建立和运行的, 根据实现的不同, 本质也不同。 线程分内核级线程和用户级线程。 所谓内核级线程就是线程是由内核程序建立和管理, 而用户级线程的系统中, 操作系统能看到的仍然是进程, 至于进程中的线程往往是一种伪实现。

Linux 内核早期对多线程的支持是设计函数库来“模拟”线程, 利用线程函数在Linux内核里还是调用创建“进程”的函数去创建了一个进程, 但这些进程有线程的“一些”特征,不能完全符合POSIX 理论上的“线程”概念。 如这种多线程不能被分配到多核上, 用户创建的N 个线程, 对于内核其实就一个个“进程”, 导致调度、 管理还是比较麻烦。 于是,IBM 启动NGPT(Next Generation POSIX Threads)项目, 红帽Red Hat 启动NPTL(Native POSIX Thread Library)对内核进行改造, 而后者发展了下来。

Linux 系统中用clone 实现轻量级进程, 内核又增加了若干机制来保证线程的表现和POSIX 相同, 如用户调用pthread 库创建的一个线程, 系统会在内核创建一个“线程”, 所以, 现在高版本的Linux 一般是有“内核级”线程的。 如果使用2.6 内核的Linux 系统,GCC 支持NPTL, 编译出来的多线程程序, 可以说是“内核级”线程的程序。

2.pthread 线程库编程

IEEE POSIX 1003.1c(1995)标准制订了使用线程的一套标准接口, 依赖于该标准的实现称为POSIX threads 或者Pthreads。 Linux 系统提供Pthread 线程函数库, 让程序员可以利用相关函数创建、 使用线程, 函数定义在pthread.h 头文件中。 由于Pthread 库不是Linux系统默认的库, 需要使用库libpthread.a, 在利用GCC 编译连接时注意在编译命令后加“-l pthread”参数帮助编译器找到相应的库。

下面学习几个主要的线程库函数。

(1) 创建线程函数——pthread_create。

①Thread: 新线程的标识符, 为一个整型。

②attr: 参数用于设置新线程的属性。 给传递NULL 表示设置为默认线程属性。

③start_routine: 是你关联的功能函数, 该函数返回时线程结束退出。

④arg: 参数分别指定新线程将运行的函数和参数。

函数调用返回值: 成功返回0, 失败返回错误号。 线程ID 的类型是thread_t, 它只在当前进程中保证是唯一的, 在不同的系统中thread_t 这个类型有不同的实现, 调用pthread_self()可以获得当前线程的ID。

(2) 终止线程——pthread_cancel、 pthread_exit。

终止某个线程而不终止整个进程, 可以有三种方法: 从线程函数return; 线程可以调用pthread_exit 终止自己; 线程可调用pthread_cancel 终止同一进程中的另一个线程。

①thread 参数是目标线程的标识符。

②函数成功返回0, 失败返回错误码。

retval: 其他线程可以调用pthread_join 获得该指针

注意: pthread_exit 或者return 返回的指针所指向的内存单元必须是全局的或者是由malloc 分配的, 不能在线程函数的栈上分配, 因为当其他线程得到这个返回指针时, 线程函数已经退出了。 pthread_exit 函数通过retval 参数向线程的回收者传递其退出信息。 它执行之后不会返回到调用者, 且永远不会失败。

(3) 线程等待——pthread_join。

调用该函数的线程将挂起等待, 直到ID 为thread 的线程终止。 thread 线程以不同的方法终止, 通过pthread_join 的retval 指针得到的结果是不同的。

①如果thread 线程通过return 返回, 指针所指向的单元里存放的是thread 线程函数的返回值。(www.daowen.com)

②如果thread 线程被别的线程调用pthread_cancel 异常终止, 指针指向的单元里存放的是常数: PTHREAD_CANCELED。

③如果thread 线程是自己调用pthread_exit 终止的, 指针指向的单元存放的是传给pthread_exit 的参数。

④如果对thread 线程的终止状态不感兴趣, 可以传NULL 参数。 成功返回0, 失败返回错误码。

POSIX 线程中不存在父子层次关系, 我们后面用子线程的说法只是体现它们是后来被创建出来的。 主线程可以创建新线程, 新线程可以再创建新线程, POSIX 线程标准将它们视为等同层次。 POSIX 线程标准不记录任何“家族”信息, 如果要等待一个线程终止, 就必须将线程的TID 传递给pthread_join()。

【例5-10】 一个进程下的两个线程共享资源执行实例如下。

该程序执行启动一个主进程, 进程中会创建两个线程, 分别关联到功能函数thread_function1 和thread_function2, 线程的功能是修改它们共享的进程变量message 的值。 为了方便观察和体验程序执行时的动态过程, 我们需要在适当的地方加入sleep 进行适当的睡眠控制。

基于原理分析, 我们能推出该程序执行效果:

message 的内容会被主线程、 两个子线程操作, 子线程主要做修改, 主线程主要做打印输出。

注意, main 中的两个pthread_create 调用, 在第二个调用返回后, thread1 和thread2 两个线程都已存在并交替获得CPU 处理时间, 线程CPU 时间片的分配取决于内核和线程库,谁先运行, 并没有严格的规则。 尽管thread1 更有可能在thread2 之前开始执行, 但这并无保证。 对于多处理器系统, 情况更是如此。

我们程序代码中设置了每个子线程执行线路会随机sleep 几秒, 进一步模拟了他们并发执行的速度是不确定的, 所以对共享变量的修改操作的执行顺序是不确定的。 执行效果开始会输出默认值“Hello World”, 但后来由于该变量被修改, 会交替输出不同的值, 最后两个子线程结束后, 主线程会循环输出该字符串最后的取值直到主线程的循环结束。

从执行结果看, 印证了上述分析。

其执行效果如下。

【例5-11】 一个进程的主线程与子线程执行顺序分析实例如下。

由主进程产生一个线程, 线程功能为修改进程变量message 字符串里的内容, 利用join 使主线程阻塞等待子线程结束返回后才继续执行, 从而控制了两者的执行顺序。 同时, 本例同样体现了线程共享进程资源这一概念。

其执行结果如下。

两个线程间执行关系图如图5.17 所示。

图5.17 两个线程间执行关系图

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈