线程基本函数
当一个程序被启动时,只有一个主线程,若要实现对其他线程的基本操作,首先必须创建新的线程,新的线程创建可以使用 pthread_create 函数实现,该函数的 API 定义如下:
/* 函数功能:创建新的线程; * 返回值:若成功则返回0,若出错则返回正的错误码; * 函数原型: */ #include <pthread.h> int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void*(*func)(void*), void *arg); /* 说明: * 当该函数成功返回时,由tid指向的内存单元被设置为新创建线程的线程ID; * attr参数用于设置线程的属性,若为NULL时,表示采用默认属性创建该线程; * 新创建的线程从func函数的地址开始运行,该函数只有一个参数arg; * 若需要向func函数传递多个参数,则必须把需要传递的参数包装成一个结构体, * 然后把该结构体的地址作为arg参数传入; */
在进程中,若调用了函数 exit,_exit,或_Exit 时,则该进程会终止,同样,若进程中的线程调用这三个函数时也会使线程所在的进程终止。那么若只是想退出线程,而不终止线程所在的进程有什么办法?下面是在单线程模式下退出线程的三种方式(不会终止线程所在的进程):
- 线程只是从启动例程中返回,返回值是线程的退出码;
- 线程被同一进程的其他线程取消;
- 线程调用 pthread_exit 函数;
当一个线程被创建好之后,执行完任务之后,我们可以调用 pthread_exit 函数终止一个线程,该函数的 API 定义如下:
/* 函数功能:终止一个线程; * 返回值:无; * 函数原型: */ #include <pthread.h> void pthread_exit(void *status); /* 说明: * 若本线程不处于脱离状态,则其线程ID和退出状态码将一直保留到调用进程内的某个其他线程对它调用pthread_join函数; * status是向 线程的回收者传递其退出信息,执行完之后该信息不会返回给调用者; */
通常父进程需要调用 wait 函数族等待子进程终止,避免子进程成为僵尸进程。在线程中为确保终止线程的资源对进程可用,即回收终止线程的资源,应该在每个线程结束时分离它们。一个没有被分离的线程终止时会保留其虚拟内存,包括它们的堆栈和其他系统资源。分离线程意味着通知系统不再需要此线程,允许系统将分配给它的资源回收。
调用 pthread_join 函数将自动分离指定的线程,被分离的线程就再也不能被其他线程连接了,即恢复了系统资源。若线程已处于分离状态,调用pthread_join 会失败,将返回EINVAL。所以,如果多个线程需要知道某个特定的线程何时结束,则这些线程应该等待某个条件变量而不是调用 pthread_join。一个进程中的所有线程都可以调用 pthread_join 函数来等待其他线程终止(即回收其他线程的系统资源),该函数的 API 定义如下:
/* 函数功能:等待一个线程终止; * 返回值:若成功则返回0,若错误则返回正的错误码; * 函数原型: */ #include <pthread.h> int pthread_join(pthread_t tid, void **status); /* 说明: * 一个进程中的所有线程可以调用该函数回收其他线程,即等待其他线程终止; * tid参数是目标线程的标识符,status参数是保存目标线程返回的退出码; * 该函数会一直阻塞,直到被回收的线程终止; */
每个线程都有自身的线程 ID ,线程 ID 由创建线程的函数 pthread_create 返回,可以使用函数 pthread_self 获取自身的线程 ID ,其 API 定义如下:
/* 函数功能:获取自身的线程ID; * 返回值:返回调用线程的线程ID; * 函数原型: */ #include <pthread.h> pthread_t pthread_self(void);
一个线程或是可汇合状态(默认),或是脱离状态,当一个可汇合状态的线程终止时,它的线程 ID 和退出状态将保留到另一个线程对其调用 pthread_join。脱离状态的线程像守护进程一样,当它终止时,所有相关资源都被释放,我们不能等待他们终止。若一个线程需要知道另一个线程啥时候终止,那最好保持第二个线程的可汇合状态。
调用 pthread_detach 函数可将指定的线程变为脱离状态。其定义如下:
/* 函数功能:把指定的线程变为脱离状态; * 返回值:若成功则返回0,若出错则返回正的错误码; * 函数原型: */ #include <pthread.h> int pthread_detach(pthread_t tid);
同一个进程中的所有线程可以调用 pthread_cancel 函数向请求取消其他线程,被请求取消的线程可以选择允许取消或如何取消。取消线程相当于线程异常终止,该函数定义如下:
/* 函数功能:请求取消同一进程的其他线程; * 返回值:若成功则返回0,出错则返回正的错误码; * 函数原型: */ #include <pthread.h> int pthread_cancel(pthread_t tid); /* 说明: * tid参数是目标线程的标识符; * 虽然可以请求取消某个线程,但是该线程可以决定是否允许被取消或者如何取消,这分别由以下两个函数完成: */ #include <pthread.h> int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype); /* 说明: * 这两个函数中的第一个参数是分别用于设置取消状态(即是否允许取消)和取消类型(即如何取消),第二个参数则是分别记录线程原来的取消状态和取消类型; * state有两个可选的取值: * 1、PTHREAD_CANCEL_ENABLE 允许线程被取消(默认情况); * 2、PTHREAD_CANCEL_DISABLE 禁止线程被取消,这种情况,若一个线程收到取消请求,则它会将请求挂起,直到该线程允许被取消; * * type也有两个可选取值: * 1、PTHREAD_CANCEL_ASYNCHRONOUS 线程随时可以被取消; * 2、PTHREAD_CANCEL_DEFERRED 允许目标线程推迟执行; */
线程属性
在前面介绍的线程操作中都是采用线程的默认属性进程操作。在创建新的线程时,我们可以使用系统默认的属性,也可以自己指定线程的主要属性。我们可以指定 pthread_attr_t 结构修改线程的默认属性,并把这个属性与创建线程联系起来。下面先看下线程的主要属性:
/* 结构体定义 */ #include <bits/pthreadtypes.h> #define __SIZEOF_PTHREAD_ATTR_T 36 typedef union { char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align; }pthread_attr_t;
线程属性主要包括以下四种属性:
/* * 线程的主要属性: * (1)detachstate 线程的脱离状态属性; * (2)guardsize 线程栈末尾的警戒缓冲区大小(字节数); * (3)stackaddr 线程栈的最低地址; * (4)stacksize 线程栈的大小(字节数); */
在进行线程属性操作之前必须对其进行初始化,初始化函数定义如下:
/* * 函数功能:初始化属性结构; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> /* 初始化线程属性对象 */ int pthread_attr_init(pthread_attr_t *attr); /* 若线程属性是动态分配内存的,则在释放内存之前,必须调用该函数销毁线程属性对象 */ int pthread_attr_destroy(pthread_attr_t *attr);
脱离状态属性
我们可以通过 pthread_attr_t 结构修改线程脱离状态属性 detachstate,下面是关于对该属性操作的函数:
/* * 函数功能:修改线程的分离状态属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);//获取当前线程的分离状态属性; int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//设置当前线程的分离状态属性; /* * 说明: * detachstate的值为以下两种: * (1)PTHREAD_CREATE_DETACHED 以分离状态启动线程; * (2)PTHREAD_CREATE_JOINABLE 正常启动线程(默认,即可汇合状态); */
栈属性
可以通过下面的操作函数来获取或者修改线程的栈属性。
/* * 函数功能:获取或修改线程的栈属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_attr_getstack(const pthread_attr_t *attr, void ** stackaddr, size_t stacksize);//获取线程栈信息; int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);//修改线程栈信息; int pthread_attr_getstackaddr(const pthread_attr_t *attr, void ** stackaddr);//获取线程栈起始地址信息; int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);//修改线程栈起始地址信息; int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);//获取栈大小的信息; int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);//设置栈大小; int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize); int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
互斥锁
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。当多个线程对可修改变量进行访问时,就会出现变量的一致性问题,这时就会涉及到线程同步的问题。
互斥锁也称为互斥量。可以通过使用 pthread 的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量本质上就是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成运行状态,第一个变为运行状态的线程可以对互斥量进行加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
互斥变量使用 pthread_mutex_t 数据类型来表示,在使用互斥量以前,必须先对它进行初始化,可以把它设置为常量 PTHREAD_MUTEX_INITIALIZER (只对静态分配的互斥量),也可以通过调用 pthread_mutex_init 函数进行初始化。如果动态地分配互斥量,那么在释放内存前需要调用 pthread_mutex_destroy。
/* 互斥量 */ /* * 函数功能:初始化互斥变量; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> /* 初始化互斥锁 */ int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); /* 销毁互斥锁,释放系统资源 */ int pthread_mutex_destroy(pthread_mutex_t *mutex); /* * 说明: * attr表示互斥锁的属性,若attr为NULL,表示初始化互斥量为默认属性; */ /* * 函数功能:对互斥量进行加、解锁; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ int pthread_mutex_lock(pthread_mutex_t *mutex);//对互斥量进行加锁,线程被阻塞; int pthread_mutex_trylock(pthread_mutex_t *mutex);//对互斥变量加锁,但线程不阻塞; int pthread_mutex_unlock(pthread_mutex_t *mutex);//对互斥量进行解锁; /* 说明: * 调用pthread_mutex_lock对互斥变量进行加锁,若互斥变量已经上锁,则调用线程会被阻塞直到互斥量解锁; * 调用pthread_mutex_unlock对互斥量进行解锁; * 调用pthread_mutex_trylock对互斥量进行加锁,不会出现阻塞,否则加锁失败,返回EBUSY。 */
互斥锁属性
互斥锁属性可以用 pthread_mutexattr_t 数据结构来进行操作,属性的初始化操作如下:
/* 互斥量属性 */ /* * 函数功能:初始化互斥量属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); /* * 说明: * pthread_mutexattr_init函数用默认的互斥量属性初始化pthread_mutexattr_t结构; * 两个属性是进程共享属性和类型属性; */ /* * 函数功能:获取或修改进程共享属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);//获取互斥量的进程共享属性 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);//设置互斥量的进程共享属性 /* * 说明: * 进程共享互斥量属性设置为PTHREAD_PROCESS_PRIVATE时,允许pthread线程库提供更加有效的互斥量实现,这在多线程应用程序中是默认的; * 在多个进程共享多个互斥量的情况下,pthread线程库可以限制开销较大的互斥量实现; * * 若设置为PTHREAD_PROCESS_PRIVATE,则表示互斥锁只能被和锁初始化线程隶属于同一进程的线程共享; */ /* * 函数功能:获取或修改类型属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);//获取互斥量的类型属性 int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);//修改互斥量的类型属性 /* 说明: * type取值如下: * 1、PTHREAD_MUTEX_NORMAL 普通锁(默认) * 2、PTHREAD_MUTEX_ERRORCHECK 检错锁 * 3、PTHREAD_MUTEX_RECUSIVE 嵌套锁 * 4、PTHREAD_MUTEX_DEFAULT 默认锁 */
互斥锁类型:
- 普通锁:当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获取该它。这种锁容易引发死锁,即当同一个线程对一个已经加锁的普通锁再次加锁时,就会引发死锁。
- 检错锁:一个线程若对一个已经加锁的检错锁再次加锁时,则加锁操作返回 EDEADLK,对一个已被其他线程加锁的检错锁解锁,或对一个已经解锁的检错锁再次解锁,则解锁操作返回 EPERM。
- 嵌套锁:这种锁允许一个线程在释放锁之前多次对它加锁而不发生死锁。但是其他线程若要获得该锁,则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程加锁的嵌套锁,或对一个已经解锁的嵌套锁再次解锁,则解锁操作返回 EPERM。
- 默认锁:一个线程若对一个已经加锁的默认锁再次加锁,或对一个已经被其他线程加锁的默认锁解锁,或对一个已经解锁的默认锁再次解锁,将导致不可预期的后果。这种锁的实现可能会被映射为上面三种锁之一。
条件变量
互斥锁是用于同步线程对共享数据的访问,条件变量则是用于在线程之间同步共享数据的值。互斥锁提供互斥访问机制,条件变量提供信号机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程。条件变量可以将一个或多个线程进入阻塞状态,直到收到另外一个线程的通知,或者超时,或者发生了虚假唤醒,才能退出阻塞状态。
条件变量与互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生,条件本身是由互斥量保护。线程在改变条件状态前必须先锁住互斥量,条件变量允许线程等待特定条件发生。条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。条件变量类型为
pthread_cond_t,使用前必须进行初始化。可以有两种初始化方式:把常量 PTHREAD_COND_INITIALIZER 赋给静态分配的条件变量,对于动态分配的条件变量,可以使用 pthread_cond_init 进行初始化。操作函数如下:
/* 条件变量 */ /* * 函数功能:初始化条件变量; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); int pthread_cond_destroy(pthread_cond_t *cond); /* 说明: * cond参数指向要操作的目标条件变量,attr参数指定条件变量的属性; */ /* * 函数功能:等待条件变量变为真; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *timeout); /* * 说明: * 传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数; * 函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两操作是原子操作; * 这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样就不会错过条件变化; * pthread_cond_wait返回时,互斥量再次被锁住; */ /* * 函数功能:唤醒等待条件的线程; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ int pthread_cond_signal(pthread_cond_t *cond);//唤醒等待该条件的某个线程,具体唤醒哪个线程取决于线程的优先级和调度策略; int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒等待该条件的所有线程;
条件变量属性
条件变量也只有进程共享属性,其操作如下:
/* 条件变量属性 */ /* * 函数功能:初始化条件变量属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destroy(pthread_condattr_t *attr); /* * 函数功能:获取或修改进程共享属性; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared); int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
线程与信号
当线程被创建时,它会继承进程的信号掩码,这个掩码就会变成线程私有的,所以每个线程可以独立设置信号掩码。进程中的所有线程都是共享该进程的信号。多个线程是共享进程的地址空间,每个线程对信号的处理函数是相同的,即如果某个线程修改了与某个信号相关的处理函数后,所在进程中的所有线程都必须共享这个处理函数的改变。也就是说,当在一个线程设置了某个信号的信号处理函数后,它将会覆盖其他线程为同一个信号设置的信号处理函数。
每个信号只会被传递给一个线程,即进程中的信号是传递到单个线程的,传递给哪个线程是不确定的。如果信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中去。但是alarm 定时器是所有线程共享的资源,所以在多个线程中同时使用alarm 还是会互相干扰。
在进程中可以调用 sigprocmask 来阻止信号发送,但在多线程的进程中它的行为并没有定义,它可以不做任何事情。在主线程中调用 pthread_sigmask 使得所有线程都阻塞某个信号,也可以在某个线程中调用它来设置自己的掩码。
/* 线程与信号 */ /* * 函数功能:设置线程的信号屏蔽字; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <signal.h> int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset); /* * 说明: * 该函数的功能基本上与前面介绍的在进程中设置信号屏蔽字的函数sigprocmask相同; */ /* * 函数功能:等待一个或多个信号发生; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ int sigwait(const sigset_t *set, int *signop); /* * 说明: * set参数指出线程等待的信号集,signop指向的整数将作为返回值,表明发送信号的数量; */ /* * 函数功能:给线程发送信号; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ int pthread_kill(pthread_t thread, int signo); /* * 说明: * signo可以是0来检查线程是否存在,若信号的默认处理动作是终止整个进程,那么把信号传递给某个线程仍然会杀死整个进程; */
如果信号集中的某个信号在sigwait 调用的时候处于未决状态,那么sigwait 将立即无阻塞的返回,在返回之前,sigwait 将从进程中移除那些处于未决状态的信号。为了避免错误动作的发生,线程在调用sigwait 之前,必须阻塞那些它正在等待的信号。sigwait 函数会自动取消信号集的阻塞状态,直到新的信号被递送。在返回之前,sigwait 将恢复线程的信号屏蔽字
线程与进程
多线程的父进程调用 fork 函数创建子进程时,子进程继承了整个地址空间的副本。子进程里面只有一个线程,它是父进程中调用 fork 函数的线程的副本。在子进程中的线程继承了在父进程中相同的状态,即有相同的互斥锁和条件变量。如果父进程中的线程占用锁,则子进程也同样占有这些锁,只是子进程不包含占有锁的线程的副本,所以并不知道具体占有哪些锁并且需要释放哪些锁。
如果子进程从 fork 返回之后没有立即调用 exec 函数,则需要调用 fork 处理程序清理锁状态。可以调用 pthread_atfork 函数实现清理锁状态:
/* 线程和 fork */ /* * 函数功能:清理锁状态; * 返回值:若成功则返回0,否则返回错误编码; * 函数原型: */ #include <pthread.h> int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); /* * 说明: * 该函数最多可以安装三个帮助清理锁的函数; * prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁; * * parent fork处理程序是在fork创建子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序 * 获取的所有锁进行解锁; * * child fork处理程序在fork返回之前在子进程环境中调用,与parent fork处理程序一样,child fork处理程序必须释放prepare fork处理程序获得的所有锁; */
参考资料:
《Unix 网络编程》