首页 > 嵌入式 > 系统编程 > POSIX线程、线程同步
2018
03-07

POSIX线程、线程同步

概述

线程就是进程内部的一条执行路线,对应于代码中的某个函数。进程可以同时运行多个线程,相比于多进程形式的并发操作,使用多线程方式的并发具有更高的效率。

进程是系统分配资源的最小单位。每个进程都拥有有自己的代码段、数据段、堆栈段,无论是使用替换还是复制方式创建进程,都要在内存中重新创建一份新进程的内存空间,建立众多的数据表来维护它的代码段、数据段和堆栈段,这需要较大的开销。在进程切换时,需要保存当前整个进程的CPU环境,再载入新进程的CPU环境,需要的开销也比较大。

线程是CPU调度的最小单位。使用线程可以将并发的开销降到最低。首先使用线程不需要重新分配进程空间。线程运行在它所在的进程中,除了程序计数器,一组寄存器和栈以外,线程共享进程的地址空间,比如代码,全局变量,描述符,信号处理函数等。共享意味着在多进程间进行同步和通信将变得非常容易,完全不需要像多进程间那样需要通过IPC系统调用来实现通信,各线程可以操作相同的全局变量。同时,在线程切换时,也只需要保存和设置少量寄存器的内存,并不需要切换整个进程的运行环境,从而能更有效地使用系统资源和提高系统的吞吐量。

POSIX线程、线程同步 - 第1张  | rs232的博客

使用多线程的目的主要有以下几点:

  1. 多任务程序的设计。一个程序可能要处理不同应用,要处理多种任务,如果开发不同的进程来处理,系统开销很大,数据共享,程序结构都不方便,这时可使用多线程编程方法。
  2. 并发程序设计。一个任务可能分成不同的步骤去完成,这些不同的步骤之间可能有的相互影响,有的没有影响,这时可以为不同的任务步骤建立线程,通过线程的互斥,让程序尽可能快地同步并发完成。
  3. 网络程序设计。为提高网络的利用效率,我们可能使用多线程,对每个连接用一个线程去处理。
  4. 数据共享。同一个进程中的不同线程共享进程的数据空间,方便不同线程间的数据共享。
  5. 在多CPU系统中,操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

线程编程

线程号

就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。进程号用pid_t数据类型表示,是一个非负整数。线程号则用pthread_t数据类型来表示。有的系统在实现pthread_t的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。linux系统可创建线程的总数也是有上限的,可通过cat /proc/sys/kernel/threads-max来查看。

POSIX线程、线程同步 - 第2张  | rs232的博客

除了线程上限,使用线程时还要注意,单个进程的栈空间是有限的,如果所有线程的栈加起来超过了上限,则程序也会出错。

编程接口

linux系统下的多线程遵循POSIX标准线程接口,称为pthread。编写linux下的多线程程序,需要使用头文件pthread.h,链接阶段需要使用库libpthread.a(-lpthread)。

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

功能:
创建一个线程。
参数:
thread:输出参数,线程标识符地址,调用后可以获得该线程的线程号。
attr:线程属性结构体地址,可以填NULL,表示使用线程的默认属性。
start_routine:线程函数的入口地址,参数和返回值都为void*的函数。
arg:传给线程函数的参数,可以为NULL,表示不需要参数。不为空时,参数将被传递给线程入口函数。
返回值:
成功返回0,失败返回非0(不是返回-1),错误号存储于errno。

注意:
与fork不同的是pthread_create创建的线程不与父线程在同一点开始运行,而是从指定的函数开始运行,该函数运行完后,该线程也就退出了。线程依赖进程存在,如果创建线程的进程结束了,线程也会结束(比如线程睡眠的时间比进程的还要长,则进程退出后线程也退出)。
示例:pthread_create_1.c
pthread_create_2.c

int pthread_join(pthread_t thread, void **retval);

功能:
阻塞主线程,使其等待子线程结束,并回收子线程资源,功能等同于多进程中的wait函数。
参数:
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址,可以为NULL。这是一个二级指针,它指向另一个指针,后者由线程返回,指向线程退出码。由于都是指针操作,一定要注意指针所指向的地址是否可用问题。绝不能返回一个指向线程局部变量的指针,因为线程调用后,局部变量所在的栈空间会被回收。
返回值:
成功返回0,失败返回非0。
示例:pthread_join.c

int pthread_detach(pthread_t thread);

功能:
创建一个线程后应回收其资源,但使用pthread_join函数会使调用者阻塞,故Linux提供了线程分离函数pthread_detach,它可以使调用线程与当前进程分离,使其成为一个独立的线程,该线程终止时,系统将自动回收它的资源。该函数在创建线程后调用。如果一个线程既不需要向主线程返回信息,也不需要主线程等待他结束,则可以将其设为分离线程。
参数:
thread:线程号
返回值:
成功:返回 0,失败返回非0。
示例:thread_detach.c

任何情况下调用exit()或_exit()函数都将结束整个进程,包括进程内部的所有线程。如果只要终止线程而不终止整个进程,有几下三种方式:

  1. 线程从执行函数中返回。
  2. 线程调用pthread_exit退出线程。
  3. 线程被同一进程中的其它线程取消。
void pthread_exit(void *retval);

功能:
使调用线程退出。
参数:
retval:存储线程退出状态的变量的指针,同样绝不能让线程返回一个指向局部变量的指针。
注意:
一个进程中的多个线程共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
例:pthread_exit.c

int pthread_cancel(pthread_t thread);

功能:
取消一个正在执行的线程,使其停止运行。
参数:
thread:目标线程ID。
返回值:
成功返回 0,失败返回出错编号
注意:
该函数可以要求另一个线程终止,就像给它发送一个信号一样,线程可以在要求终止时改变其行为。此函数只是发送终止信号给目标线程,不会等待取消目标线程执行完才返回。发送成功并不意味着目标线程一定就会终止,线程被取消时,线程的取消属性会决定线程能否被取消以及何时被取消。关注一个线程的取消行为需要考虑以下三点:

  • 线程的取消状态
  • 线程的取消类型
  • 线程取消点

线程的取消状态
在Linux系统下,线程默认可以被取消。编程时可以通过pthread_setcancelstate函数设置线程是否可以被取消。

int pthread_setcancelstate(int state, int *old_state);

功能:
设置线程的取消状态,即线程是否可以被取消
参数:
state:表示线程是否可以被取消,取以下两个值
PTHREAD_CANCEL_DISABLE:不可以被取消
PTHREAD_CANCEL_ENABLE:可以被取消(默认情况)
old_state:非空时,用于保存线程原先的取消状态,不关心时,可以传NULL
返回值:
成功返回0,失败返回错误号。
示例:pthread_setcancelstate.c

线程的取消类型
如果线程接受了取消的请求,则线程就可以进入第二个控制层次,用pthread_setcanceltype设置取消类型。

int pthread_setcanceltype(int type, int *oldtype);

功能:
获取或设置线程的取消类型,决定线程在接受取消后是否立即取消。
参数:
type:表示线程的取消类型,可以有以下两个取值
PTHREAD_CANCEL_ASYNCHRONOUS:接收到取消请求后立即取消
PTHREAD_CANCLE_DEFERRED:不立即取消,执行到取消点再取消(默认情况)
oldtype:保存线程原来的取消类型。
返回值:
成功返回0,失败返回错误号。
注意:
所谓取消点是指某些函数,线程在接受取消后,将在遇到这些函数时取消。具体可以充当取消点的函数是pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_testcancle、sem_wait或sigwait。POSIX标准还规定了其他的取消点,但并不是所有的都被linux所支持,如果想保证程序被取消,可以手动设置程序的取消点,即在需要取消的地方调用pthread_testcancel函数。
示例:thread_setcanceltype.c

线程的取消点
线程被取消后,该线程并不是马上终止,默认情况下线程执行到消点时才能被终止。编程时可以通过pthread_testcancel函数设置线程的取消点。

void pthread_testcancel(void);

功能:
默认情况下,被取消的线程在执行到此函数时结束。
注意:POSIX保证线程调用到以下列表中的任何函数时,取消点都都会出现。

POSIX线程、线程同步 - 第3张  | rs232的博客

POSIX线程、线程同步 - 第4张  | rs232的博客

和进程的退出清理一样,线程也可以注册它退出时要调用的函数,这样的函数称为线程清理函数。线程可以创建多个清理函数,清理函数存放在栈中,执行顺序与它们注册时的顺序相反。线程清理包括以下两个函数:

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

功能:
push:将清理函数压栈,即注册清理函数。
pop:将清理函数弹栈,即删除清理函数。
参数:
routine:线程清理函数的指针。
arg:传给线程清理函数的参数。
execute:线程清理函数执行标志位,可取以下值:
非0:弹出清理函数,执行清理函数。
0:弹出清理函数,不执行清理函数
注意:
当线程执行以下动作时会调用清理函数:

  • 调用pthread_exit退出线程。
  • 响应其它线程的取消请求。
  • 用非零execute调用pthread_cleanup_pop。
    无论哪种情况pthread_cleanup_pop都将删除上一次pthread_cleanup_push调用注册的清理处理函数。pthread_cleanup_pop、pthread_cleanup_push必须配对使用。
    示例: thread_cleanup_exit.c
    thread_cleanup_cancel.c
    thread_cleanup_pop.c
int pthread_attr_init(pthread_attr_t *attr);

功能:
初始化一个描述线程属性的变量,用于在pthread_create时传入以设置对应的属性。
参数:
attr:pthread_attr_t指针,用于描述线程的属性。
返回值:
成功返回0,失败返回错误码。
注意:
线程可设置的属性非常多,且设置每一个属性都需要单独的一套接口,所以非常复杂,但是通常不需要设置太多属性就可以让线程正常工作,下面以设置线程的可分离属性来介绍线程属性的使用。设置可分离属性使用以下两个函数:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

功能:
设置属性,detachstate可取的两个标志是PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACHED,默认是PTHREAD_CREATE_JOINABLE,所以可以使用pthread_join来获取 进程的退出状态。设为PTHREAD_CREATE_DETACHED时,就不能使用pthread_join来获得线程的退出状态。
返回值:
成功返回0,失败返回错误码

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

功能:
获取属性变量的可分享属性。
int pthread_attr_destroy(pthread_attr_t *attr);
功能:
对线程属性变量进行清理与回收,一旦被回收,除非它被重新初始化,否则不能被再次使用。

示例:thread_attr.c

详细的关于线程属性的介绍可以考虑文档《线程的属性》。

线程同步-信号量

在线程间通信时,也可以使用信号量接口,此处一般使用POSIX线程间通信的接口,比如sem_init,sem_wait,sem_post,sem_destroy这几个。在线程间进行信号量通信时,信号量变量可以使用全局变量,或是信号量成员打包到线程共享的结构体内部。使用信号量进行线程同步的思路与进程同步的思路是一致的,都要保证某段程序的临界访问,具体操作是每一个线程在进入临界代码时先独占信号量,使用完后再释放信号量。
示例:thread_semaphore.c

线程同步-互斥锁

互斥锁(mutex)也用于保证程序的互斥访问与同步执行,它的使用场景与二值信号量完全一样。互斥锁只有两种状态,即上锁和解锁。程序在进入临界代码区时,需要锁住一个互斥锁,然后再完成操作之后解锁它。申请互斥锁时如果锁处于解锁状态,则会申请到锁并立即将锁住,如果锁被上锁,则默认会阻塞申请者。锁的解锁操作应该由加锁者进行。

互斥锁在linux下的数据类型是pthread_mutex_t,在使用互斥锁前,必须对它进行初始化。初始化一个互斥锁有以下两种方法:

静态分配的互斥锁初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配互斥锁初始化:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

早期的POSIX标准要求PTHREAD_MUTEX_INITIALIZER只能用于静态分配的互斥锁,比如互斥锁全局变量,而程序内部的自动变量则不可以,但是基本上所有平台都支持自动变量调用该宏来进行初始化,POSIX标准后期也将该限制解除。
初始化过的互斥锁默认是解锁状态的。在不需要使用互斥锁时,应该调用pthread_mutex_destroy销毁互斥锁。

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

功能:
初始化一个静态分配的互斥锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

功能:
初始化一个互斥锁。
参数:
mutex:互斥锁变量地址
attr:互斥锁的属性,一般填NULL,表示使用默认的属性。
返回值:
成功返回0,失败返回错误号,错误号可以有以下取值:
EBUSY:对已经初始化过的互斥锁进行初始化
EINVAL:attr参数不合法

int pthread_mutex_destroy(pthread_mutex_t *mutex);

功能:
销毁一个互斥锁。
参数:
mutex:互斥锁变量的地址
返回值:
成功返回0,失败返回错误码,可以有以下取值:
EBUSY:互斥锁已被线程锁住。
EINVAL:互斥锁不合法

int pthread_mutex_lock(pthread_mutex_t *mutex);

功能:
对互斥锁上锁,若已经上锁,则调用者一直阻塞到互斥锁解锁。
参数:
mutex:互斥锁地址。
返回值:
成功返回0,失败返回错误码。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

功能:
对互斥锁上锁,若已经上锁,则上锁失败,函数立即返回。
参数:
mutex:互斥锁地址。
返回值:
成功返回0,失败返回非0

int pthread_mutex_unlock(pthread_mutex_t * mutex);

功能:
对指定的互斥锁解锁。
参数:
mutex:互斥锁地址。
返回值:
成功返回0,失败返回非0。

示例:pthread_mutex.c

线程同步-条件变量

互斥对象是线程程序必需的工具,但它们并不是万能的。例如,如果线程正在等待共享数据内某个条件出现,那会发生什么?代码需要反复对互斥对象锁定和解锁,以检查值的任何变化。同时,还要快速将互斥对象解锁,以便其他线程能够进行任何必需的更改。这是一种非常可怕的方法,因为线程需要在合理的时间范围内频繁地循环检测变化。

在每次检查之前,可以让调用线程短暂地进入睡眠,但是因此线程代码就无法最快作出响应。真正需要的是这样一种方法,当线程在等待满足某些条件时使线程进入睡眠状态。一旦条件满足,还需要一种方法以唤醒因等待满足条件而睡眠的线程。如果能够做到这一点,线程代码将是非常高效的,并且不会占用宝贵的互斥锁对象。这正是POSIX条件变量能做的事。

条件变量需要由互斥锁保护,线程在改变条件状态之前必须首先锁住互斥量。在使用条件变量之前,需要对它进行初始化,由pthread_cond_t数据类型表示条件变量,和互斥量一样,条件变量也可以有两种初始化方式,分别对应静态内存分配的条件变量和动态内存分配的条件变量。使用pthread_cond_destroy函数对条件变量进行反初始化。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

功能:初始化静态内存分配的条件变量

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

功能:初始化动态内存分配的条件变量,attr表示属性,一般填NULL

int pthread_cond_destroy(pthread_cond_t *cond);

功能:反初始化条件变量。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

功能:
把锁住的互斥量传给函数,函数自动把线程放到等待条件的线程列表上,对互斥量解锁,然后线程投入睡眠。当条件发生,函数返回,同时将互斥量再次锁住。

int pthread_cond_signal(pthread_cond_t *cond);

功能:
发送信号,至少唤醒一个等待条件满足的线程,对应于pthread_cond_wait函数将生效。

int pthread_cond_broadcast(pthread_cond_t *cond);

功能:
给所有等待条件满足的线程发送信号。

示例:pthread_cond.c

最后编辑:
作者:rs232
这个作者貌似有点懒,什么都没有留下。

留下一个回复

你的email不会被公开。