线程是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度比进程更加轻量级。一个进程可能存在多个线程,进程:线程 = 1:n,操作系统需要管理进程,操作系统管理的方式是先描述,再组织,线程就应该有自己的线程控制块TCB来组织和管理线程,这是常规操作系统的做法,比如Windows。
那么Linux是怎么管理线程的呢?
Linux没有单独为线程制定线程控制块,而是用进程的PCB来模拟线程。Linux创建线程,只创建task_struct,共享同一个地址空间,进程的代码和数据被划分成若干份,分配给各个线程去执行。站在CPU的角度,调度的还是一个个的PCB,但是今天CPU看到的PCB比我们之前讲的PCB更加轻量级,一个PCB就是一个需要被调度的执行流。Linux这样管理线程,就不用维护线程和进程的关系,不用单独为线程设计任何算法,直接复用进程的管理方式,操作系统只需要聚焦在线程间的资源分配上。
之前我们学习的进程是内部只有一个执行流的进程,今天我们学习的进程里面可以具有多个执行流。操作系统创建进程的成本是非常高的,成本主要体现在时间和空间上,创建进程需要使用的资源是很多的,这是一个从0到1的过程,而创建线程的代价比进程小得多。
站在内核的视角:
线程共享进程数据,但也拥有自己的一部分数据:
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
线程是越多越好吗?
不一定。计算密集型的应用,线程太多会导致线程间被过度调度切换,线程调度是有成本的。IO密集型应用则允许多一点线程,因为大部分都是在等待IO就绪的。
因为Linux线程是用进程复用模拟的,所以Linux没有提供直接操作线程的接口,而是提供了在同一进程地址空间创建PCB的方法,分配资源给指定PCB的接口,但是这些接口为用户不友好,所以一些系统级别的工程师就在用户层对Linux轻量级进程接口进行封装,打包成库,让用户直接使用库接口(线程创建,释放线程,等待线程等等),这个库叫做原生线程库(POSIX库)。
pthread_create接口介绍
#include 功能:创建一个新的线程
原型int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数thread:返回线程IDattr:设置线程的属性,attr为NULL表示使用默认属性start_routine:是个函数地址,线程启动后要执行的函数arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
测试代码:
#include
#include
#include void* pthread_run(void* args){while(1){printf("I am %s pthread\n", (const char*)args);sleep(1);}
}int main()
{pthread_t pid;pthread_create(&pid, NULL, pthread_run, (void*)"new thread"); //创建新线程执行pthread_runwhile(1){printf("I am main pthread\n");sleep(1);}return 0;
}运行结果:
[cwx@VM-20-16-centos pthread]$ make
gcc -o mytest mytest.c -pthread
[cwx@VM-20-16-centos pthread]$ ./mytest
I am main pthread
I am new thread pthread
I am main pthread
I am new thread pthread
...
#include 功能:获取线程自身id
原型:pthread_t pthread_self(void);
测试代码,查看主线程和新线程的进程id和线程id:
#include
#include
#include void* pthread_run(void* args)
{while(1){printf("I am %s, pid: %d, id: %lu\n", (const char*)args, getpid(), pthread_self());sleep(1);}
}int main()
{pthread_t pid;pthread_create(&pid, NULL, pthread_run, (void*)"new thread"); //创建新线程执行pthread_runwhile(1){printf("I am main thread, pid: %d, id: %lu\n", getpid(), pthread_self());sleep(1);}return 0;
}运行结果:
[cwx@VM-20-16-centos pthread]$ ./mytest
I am main thread, pid: 31377, id: 139738719135552
I am new thread, pid: 31377, id: 139738710800128
ps -aL查看线程详情
[cwx@VM-20-16-centos pthread]$ ps -aLPID LWP TTY TIME CMD
31377 31377 pts/4 00:00:00 mytest
31377 31378 pts/4 00:00:00 mytest
为什么需要线程等待?
pthread_join接口介绍
功能:等待线程结束
原型:int pthread_join(pthread_t thread, void **value_ptr);
参数:thread:线程IDvalue_ptr:输出型参数,它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
测试代码:
#include
#include
#include void* pthread_run(void* args)
{printf("%s running...\n", (const char*)args);return (void*)123; // 返回退出码123
}int main()
{pthread_t tid;pthread_create(&tid, NULL, pthread_run, (void*)"new thread");void* status = NULL; // 获取退出码pthread_join(tid, &status);printf("exit code: %d\n", (int)status);return 0;
}运行结果:
[cwx@VM-20-16-centos pthread]$ ./mytest
new thread running...
exit code: 123
调用该函数的线程将挂起等待,直到id为thread的线程终止。线程获取退出码,得知代码的运行结果的对错,但是如果程序异常了呢?pthread_join需要或者有能力处理异常吗?
pthread_join根本不需要处理程序异常,程序异常的处理是进程的任务,线程出现崩溃了,其他线程包括线程都崩溃了,pthread_join也没有意义了,线程出现异常是进程的问题而不是线程的问题。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
pthread_exit接口介绍
#include 功能:线程终止
原型:void pthread_exit(void *value_ptr);
参数:value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
测试代码:
#include
#include
#include void* pthread_run(void* args)
{printf("%s running...\n", (const char*)args);pthread_exit((void*)123);
}int main()
{pthread_t tid;pthread_create(&tid, NULL, pthread_run, (void*)"new thread");void* status = NULL; // 获取退出码pthread_join(tid, &status);printf("exit code: %d\n", (int)status);return 0;
}运行结果:
[cwx@VM-20-16-centos pthread]$ ./mytest
new thread running...
exit code: 123
pthread_cancel接口介绍
#include 功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
参数:thread:线程ID
返回值:成功返回0;失败返回错误码
测试代码(主线程取消新线程):
#include
#include
#include void* pthread_run(void* args)
{while(1){printf("%s runing...\n", (const char*)args);sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, NULL, pthread_run, (void*)"new thread");printf("3s cancel new thread...\n");sleep(3);pthread_cancel(tid); // 主线程取消新线程void *status = NULL;pthread_join(tid, &status);printf("exit code: %d\n", (int)status);return 0;
}运行结果:
[cwx@VM-20-16-centos pthread]$ ./mytest
3s cancel new thread...
new thread runing...
new thread runing...
new thread runing...
exit code: -1
如果进程被正常取消,退出码返回-1,这个值代表PTHREAD_CANCELED,
通过grep命令查找这个宏定义:
测试代码(新线程取消主线程):
#include
#include
#include pthread_t g_tid;void* thread_run(void* args)
{while(1){printf("5s after cancel main thread, %s running...\n", (const char*)args);sleep(5);pthread_cancel(g_tid); // 新线程取消主线程}
}int main()
{g_tid = pthread_self();pthread_t tid;pthread_create(&tid, NULL, thread_run, (void*)"new thread");sleep(20);pthread_join(tid, NULL);return 0;
}
启动监控脚本查看线程详情:
我们可以看到,新线程把主线程取消,主线程的状态就变成
该进程已经变成了僵尸进程。
线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
#include 原型:int pthread_detach(pthread_t thread);
功能:进行线程分离。
参数:thread:要分离的线程id
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
线程等待和线程分离是矛盾的,一个线程不能既等待又分离。
测试代码:
#include
#include
#include void* thread_run(void *args)
{pthread_detach(pthread_self()); // 新线程分离printf("%s running...\n", (const char*)args);
}int main()
{pthread_t tid;pthread_create(&tid, NULL, thread_run, (void*)"new thread");sleep(1);if(pthread_join(tid, NULL) == 0){printf("pthread join success\n");}else{printf("pthread wait fail\n");}return 0;
}运行结果:
[cwx@VM-20-16-centos pthread]$ ./mytest
new thread running...
pthread wait fail
创建五个线程,打印线程id,并用ps -aL查看详情:
LWP是内核的线程id,而我们打印的是pthread库的线程id,这两个的值不一样。
那么pthread_t到底代表的是什么呢?
我们查看的线程id是pthread库的id,不是Linux内核中的LWP。pthread库的id是一个内存地址。
ldd mytest查看进程的链接详情:
pthread库要被进程访问,libpthread-2.17.so就必须从磁盘上被加载到内存里,并通过页表映射到进程地址空间中。创建线程后,每个线程都要有运行时的临时数据,就要求每个线程都要有自己私有的栈结构,而且还要有描述线程的用户级控制块,但是进程地址空间中只有一个主线程栈,其他线程的栈结构实际上是在pthread共享库里被库维护的,pthread库的id就是描述每个线程的各种结构、线程局部存储和线程栈的起始内存地址,只要拿到了pthread库的id就可以找到线程运行的用户级数据。只要在库创建了用户层的描述线程的属性和数据结构,对应的在内核PCB就会创建LWP,它们是1:1的关系,相当于用户级线程和内核级线程1:1对应,struct pthread包含对应的LWP,就如同struct FILE中包含文件描述符fd一般。
轻量级进程ID与进程ID之间的区别:
LWP与pthread_create创建的线程之间的关系:
简述什么是LWP:
上一篇:python中的模块与包详解