前面一篇我们已经谈及主线程和子线程之间的关系,以及线程在运行时的线程状态,本篇我会讨论到如何优雅地连接线程,并且通过一个具体的示例来结合前一篇所说的线程状态来分析不合理使用连接线程带来的负面影响。
线程的连接
我们通过man命令查看一下pthread_join的文档
int pthread_join(pthread_t thread, void **retval);
- 参数thread 就是传入线程的ID
- 参数retval 就是传入线程的返回码,如果线程被取消,那么rval被重置为PTHREAD_CANCELED, 如果调用成功,返回0,失败就返回大于0的整数。
再进一步之前,我们需要了解一下前面没有谈及的线程属性--分离
- 默认情况下,pthread_create创建的进程是非分离的,分离是指一个运行时的线程的一个特定属性,只是告知系统内核该线程结束时,其使用的资源可以回收,其中包括释放所有该线程结束时未释放的系统的资源(包括返回值的内存空间,堆,栈,寄存器等内存空间)。
- 一个没有被分离的线程在结束时,系统内核会保留它的虚拟内存,当中包括它们的堆和栈,寄存器(这种线程,通常叫僵尸线程,那么该僵尸线程的宿主进程,自然就成为僵尸进程)
还有其他线程属性,请参考这个文档
pthread_join调用注意事项
- 调用该系统调用会使传入的线程拥有分离属性,如果该线程已经处于分离的状态,那么调用就会失败。
- 调用该系统调用的线程会一直阻塞,直到指定的线程(用线程ID来标识)调用pthread_exit,从启动的线程回调函数中返回或者调用pthread_cancel(传入该线程的ID)
下面的例子是一个非常好的例子,非常形象地解析Linux内核的线程管理机制的,首先我们创建了一个代表非负整数序列的结构体Seque,然后在1到200分成三个长度不同的数据区间,分别传递给三个线程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include<string.h>
typedef struct{
unsigned long begin;
unsigned long end;
unsigned long sum;
} Sequen;
void* calc(void* args){
Sequen* se=(Sequen*)args;
unsigned long beg=se->begin;
unsigned long end=se->end;
pthread_t tid=pthread_self();
//计算奇数和
for(unsigned long i=beg;i<=end;i+=2){
se->sum+=i;
}
printf("子线程0x%lx ->计算结果:%lu\n",tid,se->sum);
return NULL;
}
int main(int argc,char* argv){
pthread_t tids[3];
Sequen seq[3]={
{1,50,0},
{51,100,0},
{101,200,0}
};
int err;
for(int i=0;i<3;i++){
printf("创建地第%d个子线程\n",i+1);
err=pthread_create(&tids[i],NULL,calc,&seq[i]);
if (err){
printf("创建线程失败!!\n");
continue;
}
pthread_join(tids[i],NULL);
}
printf("主线程计算结果:%ld\n",seq[0].sum+seq[1].sum+seq[2].sum);
}
输出结果:你觉得很奇怪是吗?为什么三个子线程的ID是一样的呢?
我们分析一下这里的三个线程发生了什么事情,首先我们在for循环中分别使用pthread_create创先后创建了三个进程(注意:字眼是“先后”代表是有顺序)
- 第一个被创建的线程,系统从线程ID的编号空间中分配一个线程ID(十六进制)0x7f1657e2e700给tids[0],随后立即将该子线程传递给pthread_join处理,此时第一个线程创建之后,但未被pthread_join处理完之前,该线程的状态是“运行”了。注意,这个phread_join是主线程调用的,因此主线程进入“阻塞”状态,此时主线程只能等第一个子线程结束并且什么事都做不了,因此也无法创建剩下的两个子线程。
- 当第一个线程终止后,主线程从“阻塞”回到“运行”状态,而且在第一个子线程结束后,系统内核回收了第一个子线程ID:0x7f1657e2e700,并将该ID再“转租”给第二个子线程。因此第一个子线程和第二个子线程的线程ID是一样的。当主线程再次调用pthread_join后,那么主线程再次从“运行”状态切换到“阻塞”状态,一直等到第二个子线程执行完毕,才能接着创建第三个子线程。
- 同理,第三个子线程也和前面两个步骤的分析一样。
你因为这个分析不合理吗?我也查阅了gnu官网的相关文档说明https://www.gnu.org/software/libc/manual/html_node/Process-Identification.html
中文翻译:
在Linux上,由pthread_create创建的线程也会收到线程ID。 初始(主)线程的线程ID与整个进程的进程ID相同。 随后创建的线程的线程ID是不同的。 它们是从与进程ID相同的编号空间分配的。 有时,进程ID和线程ID也统称为任务ID。 与进程相比,线程从不显式等待,因此线程ID在线程退出或取消后立即可以重用。 即使对于可连接线程,也不仅仅是分离线程,都是如此。 线程被分配给线程组。 在Linux上运行的GNU C库实现中,进程ID是进程中所有线程的线程组ID。
phread_join的副作用
OK,phread_join调用虽然能够使改变目标子线程的线程属性,但它明显的副作用是调用它的函数上下文所处的线程处于阻塞状态,调用phread_join的线程无法执行接下来的其他指令。
试问上面的示例跟一个单线程的同步调用没什么区别!~因为主线程的间隔性的堵塞,令到子线程没办法连续创建和并发运行。因此,我们使用在多线程编程中,要谨慎使用phread_join调用,我们应该结合我们具体的上下问逻辑来合理使用phread_join系统调用.
Join vs Detach
这是本篇的主题,我们之前本篇开头已经说了pthread_detach能够线程属性改变为分离状态,在退出时自动释放其分配的资源。 不需要其他线程需要连接它。 但是默认情况下所有线程都是可连接(Joinable),因此要分离一个线程,我们需要使用指定的线程ID传递给pthread_detach系统调用,并且在主线程中调用它,我们上面的main线程,将pthread_joint替换成pthread_detach即可,如下代码
int main(int argc,char* argv){
pthread_t tids[3];
Sequen seq[3]={
{1,50,0},
{51,100,0},
{101,200,0}
};
int err;
for(int i=0;i<3;i++){
printf("创建地第%d个子线程\n",i+1);
err=pthread_create(&tids[i],NULL,calc,&seq[i]);
if (err){
printf("创建线程失败!!\n");
continue;
}
pthread_detach(tids[i]);
}
printf("主线程计算结果:%ld\n",seq[0].sum+seq[1].sum+seq[2].sum);
return 0;
}
我们编译后尝试运行一下,从下面的输出,三个线程ID是不一样的证明子线程可以并发运行了,但是主线程是提前return了,因为整个程序计算的整数序列的输出结果是不对的。
那么究竟有没有方法可以保证所以子线程可以并发运行,同时主线程在确认所有子线程返回计算结果后,主线程再执行剩余的操作再返回?你可能会想到pthread_exit调用吗!非常抱歉pthread_exit在这里不适用,因为pthread_exit之后的所有语句都是不会再执行的。
我们先将这个问题,留给下一篇文章来解决.后续继续更新....
网友评论