Linux多线程开发

1.进程与线程的区别

Linux多线程开发

2.线程的创建

函数原型:

int  pthread_create(pthread_t *thread, const pthread_attr_t *attr,void* (*start_routine)(void *), void *arg))
功能:创建一个线程。

返回值:成功创建返回值为0,错误返回错误号。注意:由于创建线程函数是一个库函数,不是系统调用函数。所以其错误信息不能用perror()进行打印,采用strerror(错误号)可以将错误信息打印出来。其中strerror函数是包含#include<string.h>之中的一个库函数。

参数: 
参数1:是一个传出参数,用于保存成功创建线程之后对应的线程id。 
参数2:表示线程的属性,通常默认传NULL,如果想使用具体的属性也可以修改具体的参数。 
参数3:函数指针,一个指向函数的指针。指向创建线程所执行函数的入口地址,函数执行完毕,则线程结束。 
参数4:线程主函数执行期间所使用的参数。 
3.获取线程id 
函数原型:

pthred_t pthread_self(void);

功能:获取当前线程的id。 
参数:无参。

4.单个线程退出

函数原型: void pthread_exit(void *retval) 
参数:retval表示线程的退出状态,通常穿NULL。当要求传出具体的退出状态时,可以使用retval。

当使用exit函数退出线程时,存在的问题是如果当前还有线程没有执行相应的任务,但是由于进程的退出,强制使得线程被迫退出。因为线程依赖与进程这是非常危险的退出方式,因此提出来了单线程的退出。不会影响到其他线程的撤销以及进程的撤销。
5.阻塞等待线程退出,回收线程的资源。 
函数原型:int pthread_join(pthread_t thread, void **retval)

参数: pthread为线程id,retval为线程的状态。可以与pthread_exit()结合使用。

调用该函数的线程将挂起等待,为阻塞的状态。直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下: 
1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。 
2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。 
3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。 
4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。 
6.线程资源正确释放

每个进程创建以后都应该调用pthread_join 或 pthread_detach 函数,只有这样在线程结束的时候资源(线程的描述信息和stack)才能被释放.

如果在新线程里面没有调用pthread_join 或 pthread_detach会导致内存泄漏, 如果你创建的线程越多,你的内存利用率就会越高, 直到你再无法创建线程,最终只能结束进程。

解决方法有三个:

1.   线程里面调用 pthread_detach(pthread_self()) 这个方法最简单

2. 在创建线程的设置PTHREAD_CREATE_DETACHED属性

3. 创建线程后用 pthread_join() 一直等待子线程结束。

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
通常情况下,若创建一个线程不关心它的返回值,也不想使用pthread_join来回收(调用pthread_join的进程会阻塞),就可以使用pthread_detach,将该线程的状态设置为分离态,使线程结束后,立即被系统回收。

7.取消线程与取消点

取消操作允许线程请求终止其所在进程中的任何其他线程。不希望或不需要对一组相关的线程执行进一步操作时,可以选择执行取消操作。

取消线程的一个示例是异步生成取消条件,例如,用户请求关闭或退出正在运行的应用程序。另一个示例是完成由许多线程执行的任务。其中的某个线程可能最终完成了该任务,而其他线程还在继续运行。由于正在运行的线程此时没有任何用处,因此应当取消这些线程。

取消点:

仅当取消操作安全时才应取消线程。pthreads 标准指定了几个取消点,其中包括:

  • 通过 pthread_testcancel 调用以编程方式建立线程取消点。

  • 线程等待 pthread_cond_wait 或 pthread_cond_timedwait(3C) 中的特定条件出现。

  • 被 sigwait(2) 阻塞的线程。

  • 一些标准的库调用。通常,这些调用包括线程可基于其阻塞的函数。有关列表,请参见 cancellation(5) 手册页。

缺省情况下将启用取消功能。有时,您可能希望应用程序禁用取消功能。如果禁用取消功能,则会导致延迟所有的取消请求,直到再次启用取消请求。有关禁用取消功能的信息,请参见pthread_setcancelstate 语法

取消模式:

pthread支持三种取消模式,模式为两位二进制的编码,称为“取消状态”和“取消类型”。每种模式提供开,关两种状态(编码技术提供四种状态,最后一种是冗余的)

取消状态可以是“启用(enable)”和“禁用(disable)

取消类型可以是被“推迟”或“异步”。

pthread_setcancelstate 语法

int	pthread_setcancelstate(int state, int *oldstate);
#include <pthread.h>

int oldstate;

int ret;

/* enabled */

ret = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);


/* disabled */

ret = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);

pthread_setcancelstate 返回值

pthread_setcancelstate() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,pthread_setcancelstate() 函数将失败并返回相应的值

EINVAL

描述:

状态不是 PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE。

pthread_setcanceltype 语法

int	pthread_setcanceltype(int type, int *oldtype);
#include <pthread.h>

int oldtype;

int ret;

/* deferred mode */

ret = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);

/* async mode*/

ret = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);

创建线程时,缺省情况下会将取消类型设置为延迟模式。在延迟模式下,只能在取消点取消线程。在异步模式下,可以在执行过程中的任意一点取消线程。因此建议不使用异步模式。

pthread_setcanceltype 返回值

pthread_setcanceltype() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

EINVAL

描述:

类型不是 PTHREAD_CANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS。

pthread_testcancel 语法

void pthread_testcancel(void);
#include <pthread.h>


pthread_testcancel(); 

当线程取消功能处于启用状态且取消类型设置为延迟模式时,pthread_testcancel() 函数有效。如果在取消功能处于禁用状态下调用 pthread_testcancel(),则该函数不起作用。

请务必仅在线程取消操作安全的序列中插入 pthread_testcancel()。除通过 pthread_testcancel() 调用以编程方式建立的取消点以外,pthread 标准还指定了几个取消点。有关更多详细信息,请参见取消点

pthread_testcancel 返回值

pthread_testcancel() 没有返回值。

pthread_setcancelstate()函数只是改变本线程(注意是本线程)的cancel state。所以T1进入fun()函数,
执行到pthread_setcancelstate()函数时,只是改变了T1本身的cancel state,并不能改变T2的cancel state。
 
线程执行到pthread_testcancel()函数时,并不一定会马上取消(退出)。
 
描述一下取消一个线程的过程:
1) 其他线程通过调用pthread_cancel()函数,向目标线程发送取消请求(cancellation request)。
2) 取消请求发出后,根据目标线程的cancel state来决定取消请求是否会到达目标线程:
  a. 如果目标线程的cancel state是PTHREAD_CANCEL_ENABLE(默认),取消请求会到达目标线程。
  b. 如果目标线程的cancel state是PTHREAD_CANCEL_DISABLE,取消请求会被放入队列。直到目标线程的cancel state变为PTHREAD_CANCEL_ENABLE,取消请求才会从队列里取出,发到目标线程。
3) 取消请求到达目标线程后,根据目标线程的cancel type来决定线程何时取消:
  a. 如果目标线程的cancel type是PTHREAD_CANCEL_DEFERRED(默认),目标线程并不会马上取消,而是在执行下一条cancellation point的时候才会取消。有很多系统函数都是cancellation point,、
  详细的列表可以在Linux上用man 7 pthreads查看。除了列出来的cancellation point,pthread_testcancel()也是一个cancellation point。就是说目标线程执行到pthread_testcancel()函数的时候,
  如果该线程收到过取消请求,而且它的cancel type是PTHREAD_CANCEL_DEFERRED,那么这个线程就会在这个函数里取消(退出),这个函数就不再返回了,目标线程也没有了。
  b. 如果目标线程的cancel type是PTHREAD_CANCEL_ASYNCHRONOUS(也就是异步取消),目标线程会立即取消(这里的“立即”只是说目标线程不用等执行到属于cancellation point的函数的时候才会取消,
  它会在获得调度之后立即取消,
  因为内核调度会有延时,所以并不能保证时间上的“立即”)。

8.linux多线程信号处理

在linux下,每个进程都有自己的signal mask,这个信号掩码指定哪个信号被阻塞,哪个不会被阻塞,通常用调用sigmask来处理。同时每个进程还有自己的signal action,这个行为集合指定了信号该如何处理,通常调用sigaction来处理。

使用了多线程后,便有些疑问:

信号发生时,哪个线程会收到
是不是每个线程都有自己的mask及action
每个线程能按自己的方式处理信号么

首先,信号的传递是根据情况而定的:

Linux多线程开发


如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理。
如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理。
如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到。

其次,每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。另外,注意子线程的mask是会从主线程继承而来的。

第三个问题,因为signal action共享的问题,已经知道不能。

pthread & signal 
 
pthread线程和信号


所有的异步信号发到整个进程的所有线程(异步信号如kill, lwp_kill, sigsend, kill等调用产生的都是,异步信号也称为中断),而且所有线程共享信号的处理行为(即sigaction的设置,对于同一信号的设置,某一线程的更改会影响到所有线程)。但每个线程可以有自己的mask来阻止信号的发送,所以可以通过线程对mask的设置来决定信号发送到哪个线程。设置mask的函数为:

#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)

此外,线程可以通过sleep(超过指定时间或调用的进程/线程捕捉到某个信号并从信号处理程序返回时,sleep返回)或者sigwait来等待一个或多个信号发生。

#include <signal.h>
int pthread_sigwait(const sigset_t *restrict set, int *restrict signop);

给进程发送信号可以调用kill,同样给线程调用信号可以使用pthread_kill

#include <signal.h>
int pthread_kill(pthread_t thread, int signo);

可以发送一个0的signo来检查线程是否存在,如果信号的默认行为是终止进程(例如SIGARLM),那么把该信号发送给某个线程会杀掉整个进程的所有线程。

另外注意ALARM是进程资源,并且所有线程共享相同的ALARM,设置一个alarm()会发送SIGARLM信号给所有线程,所以他们不可能互补干扰的使用alarm()。

4. 相关函数 

sigaction(查询或设置信号处理方式)


#include<signal.h>
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);

sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。


如参数结构sigaction定义如下

struct sigaction
{
   void (*sa_handler) (int);
   sigset_t sa_mask;
   int sa_flags;
   void (*sa_restorer) (void);
}

sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal()。
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号搁置。
sa_restorer 此参数没有使用。
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。

 
sigfillset(将所有信号加入此信号集)

#include<signal.h>
int sigfillset(sigset_t * set);
 
sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里。
 
sigemptyset(初始化信号集)  

#include<signal.h>
int sigemptyset(sigset_t *set);
 
sigemptyset()用来将参数set信号集初始化并清空。
 
pthread_sigmask(更改或检查调用线程的信号掩码)
 
#include <pthread.h>
#include<signal.h>
int pthread_sigmask(int how, const sigset_t *new, sigset_t *old);
 
how用来确定如何更改信号组,可以为以下值之一:

    SIG_BLOCK:向当前的信号掩码中添加new,其中new表示要阻塞的信号组。
    SIG_UNBLOCK:向当前的信号掩码中删除new,其中new表示要取消阻塞的信号组。
    SIG_SETMASK:将当前的信号掩码替换为new,其中new表示新的信号掩码。

pthread_kill(向线程发送信号)
 
#include <pthread.h>
#include<signal.h>
int pthread_kill(thread_t tid, int sig);
 
pthread_kill()将信号sig发送到由tid指定的线程。tid所指定的县城必须与调用线程在同一个进程中。