线程是参与系统调度的最小单位,它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中的一个单一顺序的控制流,或者说是执行流,一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程,因为它是程序一开始就运行的线程。应用程序通常是以 main() 作为入口开始运行的,所以 main() 就是主线程的入口函数。任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,多线程的其它线程通常是由主线程调用 pthread_create 创建的。主线程通常会在最后结束运行,执行各种清理工作。
线程是程序最基本的运行单位,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。同一进程中的多个线程将共享该进程中的全部系统资源,但同一进程中的多个线程有各自的调用栈,自己的寄存器环境、自己的线程本地存储。
线程的特点如下:
进程也可以创建多个进程来并发执行,但是进程间切换开销大,而且进程间通信较为麻烦。线程创建的速度远大于进程创建的速度,多线程在多核处理器上更有优势。
串行是指必须完成上一个任务才能去做下一个任务;
并行是指多个任务可以同时执行;
并发强调的是时分复用,是指可以打断当前执行的任务而去执行另一个任务
进程 ID 在整个系统中都是唯一的,但线程 ID 只有在它所属的进程上下文中才有意义。
#include
pthread_t pthread_self(); // 获取自己的线程 ID
int pthread_equal(pthread_t t1,pthread_t t2); // 检查两个线程 ID 是否相等,相等返回非0,否则返回0
#include
int pthread_create(pthread_t *thread,const pthread_attr_t *attr, void*(*start_routine)(void*),void*arg);
/*
* thread:函数成功返回时,新创建的线程 ID 会保存在thread指向的内存中
* attr:指向一块缓冲区,该缓冲区定义里线程的各种属性,如果是NULL,表示以默认属性
* start_routine:函数指针,指向一个函数,新创建的线程从该函数开始运行。返回值和参数都为void*,参数就是第四个参数arg
* arg:传递给函数的参数。一般情况下,需要将arg指向一个全局或堆变量,也就是说在线程的生命周期内,该参数必须存在。也可以设置为NULL,表示不需要传参。
*/
static void *routine(void *arg)
{
cout << "新线程创建成功,进程 ID: "<<getpid()<<" 线程 ID: "<<pthread_self()<<endl;
return (void*)0;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
cout << "我是主线程,进程ID: "<<getpid()<<" 线程 ID: "<< pthread_self()<<endl;
sleep(1);
return 0;
}
休眠 1 秒是因为,如果主线程不进行休眠,它就可能立马退出,新线程可能就没有机会运行
运行之后可以发现,两个线程的进程 ID 都是一样的,但是线程 ID 不同
#include
void pthread_exit(void *retval);
线程退出码可由另一个线程调用 pthread_join() 获取。但是要注意,retval 不能分配在线程栈中,因为不能保证该空间是否有效。
#include
int pthread_join(pthread_t thread,void **retval);
/*
* thread:需要等待的线程
* retval:如果不为NULL,将线程的退出状态保存在*retval中。如果目标线程被取消,将PTHREAD_CANCELED保存。如果对退出状态不关系,可以设置为NULL
*/
该函数会阻塞式等待指定的线程终止。如果该线程已经终止,则立即返回。如果多个线程同时调用,那么结果是不确定的。
若线程未分离(detached),则必须使用该函数来等待线程终止,回收线程资源;如果线程终止后,没有其他线程调用该函数回收该线程,那么该线程将变成僵尸线程。如果僵尸进程过多,会导致系统无法创建新的线程,但是进程终止后,进程会被其父进程回收,所以僵尸进程同样也会被回收。
pthread_join() 和 waitpid() 的区别在于:
向一个线程发送一个信号,要求它立刻退出,就叫做取消线程。
#include
int pthread_cancel(pthread_t thread);
发出取消请求后,函数立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立即退出。但是,线程可以设置自己不被取消或者控制如何被取消,所以该函数并不会等待线程终止,仅仅是提出请求。
#include
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
// 如果对线程取消之前的状态及类型不感兴趣,可以将第二个参数设置为NULL
/* state:
* PTHREAD_CANCEL_ENABLE:线程可以取消,也是新创建线程取消状态的默认值
* PTHREAD_CANCEL_DISABLE:线程不可以被取消,接收到取消请求后会将请求挂起,直到取消状态改变
* /
/* type:
* PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点,这也是默认类型
* PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点取消线程,一般不用。
如果线程的取消状态是默认的,那么对取消请求的操作取决于取决类型。当某个线程调用 fork() 创建子进程之后,子进程会继承调用线程的取消状态和取消类型,而当某线程调用 exec 函数时,会将新程序主线程的取消状态和类型设为默认值。
将线程的取消类型设置为默认时,收到其它线程发送来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。
取消点就是一些列函数,当执行到这些函数时,才会真正相应取消请求。在没有出现取消点时,取消请求是无法得到处理的,因为此时正在执行的代码不能被停止。
取消点可以使用man 7 pthreads
查看
如果正在执行的是一个不含取消点的循环,可以使用 pthread_testcancel() 产生一个取消点,如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会终止。
#include
void pthread_testcancel();
有时,不关心线程的返回状态,只希望系统在线程终止时能够自动回收线程资源并将其移除。
#include
int pthread_detach(pthread_t thread);
一个线程可以将别的线程分离,也可以分离自己。一个线程一旦处于分离状态,就不能使用 pthread_join() 来获取终止状态,当其终止后,会自动回收线程资源。
与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构。
#include
void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时,清理函数栈中的函数才会被执行:
创建线程时,可以设置属性,当定义 pthread_attr_t 对象之后,需要使用 pthread_attr_init() 和 pthread_attr_destroy() 执行初始化和销毁工作。
#include
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
该结构体中的属性比较多,这里不详细介绍。Linux 为该结构体对象的每种属性提供了设置属性的接口以及获取属性的接口。
#include
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr,size_t *stacksize);
// 单独设置或获取大小地址等信息
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
#include
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
/* detachstate:
* PTHREAD_CREATE_DETACHED:新建线程一开始就处于分离状态,无法被其他线程回收
* PTHREAD_CREATE_JOINABLE:正常启动线程,可以被其他线程回收
信号在一些方面是属于进程层面(由进程中的所有线程共享)的,而在另一些方面是属于单个线程层面的。
#include
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
每个刚创建的线程,都会从创建者处继承信号掩码,这个新的线程可以通过该函数改变它的信号掩码
#include
int pthread_kill(pthread_t thread, int sig);
应用程序中涉及信号处理函数时必须要非常小心,因为信号处理函数可能会在程序执行的任意时间点被调用,从而打断主程序。
前面介绍了线程安全函数,可以被多个线程同时调用,每次都能得到预期的结果,但是这里有前提条件,那就是不能在信号处理函数中调用。
异步信号安全函数(async-signal-safe function)指的是可以在信号处理函数中被安全调用的线程安全函数。可以使用 man 7 signal
查看。
对于一个安全的信号处理函数,需要做到以下几点: