目录
③某个对等线程调用Linux地exit函数,该函数终止进程以及所有与该线程相关的线程。
④另一个对等线程通过以当前线程ID作为参数调用pthread_cancel函数来终止当前进程。
①同时使用pthread_detach和pthread_join
一、线程的概念
1.线程
线程就是运行在进程上下文中的逻辑流。更准确地是“线程是进程中的控制序列”。
线程由内核自动调度。每个线程都有它自己的线程上下文,包括唯一的整数线程ID(Thread ID,TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。
同基于I/O多路复用的流一样,多个线程运行在同一个进程的上下文中,因此共享这个进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。
2.线程的优点
创建一个新线程的代价要比一个新进程小得多。
线程之间切换,与进程切换相比,操作系统做的工作要少很多。
线程占用的资源要比进程占用的要少。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
3.线程的缺点
性能损失:增加了额外的同步和调度开销,而可用的资源不变。
缺乏访问控制:进程是访问控制的基本粒度,进程是OS申请在一个线程中调用某些OS函数会对整个进程造成影响。
健壮性降低:编写一个多线程需要更全面的更深入的思考,在一个多线程程序中,因时间分配的细微差异,而导致了线程之间共享了不该共享的变量而造成不良影响的可能性很大,说明线程之间是缺乏保护的。
4.进程vs线程
进程:内核数据结构 + 进程对应的数据和代码
从内核角度来看,进程就是承担系统分配资源的实体。也就是说它是向系统资源申请的基本单位。
进程不止一个PCB结构,它是一个整体,它有task__struct,虚拟地址空间和页表对物理内存的映射关系。
上图为单一执行流的进程,为单执行流进程。而线程作为执行流的基本单位,本质是在进程内部运行。透过虚拟地址空间,可以看到进程的大部分资源,然后每个执行流执行自己的进程资源,这就是线程。
所以在CPU眼里看到的是线程看到的是一个个执行流,看到的PCB要比传统的进程更加轻量化。
①如何证明,进程里包含着线程,线程组成了进程?
我们先证明一下:
代码:
void *fuc(void *a)
{
char *msg = static_cast<char *>(a);
while (1)
{
sleep(1);
cout << "我是" << msg << endl;
}
}
void *fuc1(void *a)
{
char *msg = static_cast<char *>(a);
while (1)
{
sleep(1);
cout << "我是" << msg << endl;
}
}
void *fuc2(void *a)
{
char *msg = static_cast<char *>(a);
while (1)
{
sleep(1);
cout << "我是" << msg << endl;
}
}
int main()
{
pthread_t tid, tid1, tid2;
pthread_create(&tid, NULL, fuc, (void *)"thread");
pthread_create(&tid1, NULL, fuc1, (void *)"thread1");
pthread_create(&tid2, NULL, fuc2, (void *)"thread2");
while (1)
{
sleep(1);
cout << "我是主线程" << endl;
}
pthread_join(tid, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
出现打印的问题,会是线程在使用显示器这个临界资源的时候,线程被切换导致。
随即我们去查看该进程的pid。
pid为3296 。再查看这些线程的归属哪一个进程
会发现我们的线程都归属于一个进程,就是创建它们的进程。
所以证明它们是一个进程内的4个执行流。
②线程独享
线程共享进程数据但也有自己独立的数据:
栈
线程ID
一组寄存器
信号屏蔽字
调度优先级
errno
③线程共享
线程共享进程很多资源:
虚拟地址空间
文件描述表
信号处理方式
当前工作目录
用户ID和组ID
主线程使用虚拟内存中的栈空间。新线程的栈空间为库中提供的栈结构。
二、线程控制
1.创建线程![](https://i-blog.csdnimg.cn/blog_migrate/a0cfffd2e0f5b6cf9dc987ec8a7674a2.png)
thread:输出型参数,会返回出一个线程的tid。
attr:线程的属性。我们目前可以设为NULL。
start_routine :传入函数指针,该函数返回值是void * 参数为void *。
arg:传入一个你想传入,它会作为该函数第三个参数的参数传入。
return value :成功返回线程id,失败返回对应的错误码,并且*thread的内容会未被定义。
我们先简单的使用下:
void *thread_fuc(void *argc)
{
char *msg = static_cast<char *>(argc);
while (1)
{
cout << msg << endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_fuc, (void *)"愿家人健健康康");
while (1)
{
cout << "疫情早日结束" << endl;
sleep(2);
}
return 0;
}
2.获取线程ID
在线程中调用该函数,返回调用函数的线程的tid。
return value :成功返回0 ,失败返回对应的错误码
这个数到底是什么?
我们首先看一下它的类型pthread_t。
实际上这个数就是地址,这个地址指向用户级创建的线程的结构体,可以通过这个地址快速访问结构体,而获得该线程的相关属性。
3.终止线程
一个线程是以下列方式之一来终止的:
①当顶层的线程例程返回时,线程会隐式地终止。
②通过调用pthread_exit函数,线程会显示地终止。
如果主线程调用pthread_exit,它会等待其他所有对等线程终止,然后再终止主线程和终止整个进程,返回值为retval。
void *fuc(void *argc)
{
int cnt = 3;
while (cnt -- )
{
cout << "我是线程1" << endl;
sleep(1);
}
pthread_exit((void *)1);
}
void *retval;
int ret = pthread_join(tid, &retval);
cout<<(long long)retval<<endl;
结果:
③某个对等线程调用Linux地exit函数,该函数终止进程以及所有与该线程相关的线程。
④另一个对等线程通过以当前线程ID作为参数调用pthread_cancel函数来终止当前进程。
⑤线程出异常,进程也会退出
4.回收线程
类似于回收子进程,如果创建出线程而不回收会导致内存泄露等问题。
thread:传入线程的tid。
retval:如果retval不是NULL,那么目标线程的退出码会被copy到*retval所指向的位置。如果目标线程被canceled,然后PTHREAD_CANCELED会被放入*retval中。
return value :成功返回0 ,失败返回对应的错误码 。
pthread_join函数会阻塞,知道线程thread终止,将线程例程返回的通用(void *)指针赋值为reval指向的位置,然后回收终止线程占用的所有内存资源。
使用:
//主线程和要回收的线程都在两秒之后结束
pthread_join(tid,NULL);
cout<<"pthread_join success!"<<endl;
我们再看一下,目标线程被取消,retval的值。
void *retval;
sleep(3);
pthread_cancel(tid);
int ret = pthread_join(tid, &retval);
cout<<ret<<endl;
cout<<(long long)retval<<endl;
实际上这个PTHREAD_CANCELED就是-1。
下面的结果图,是线程正常结束的情况。
void *fuc(void *argc)
{
int cnt = 3;
while (cnt -- )
{
cout << "我是线程1" << endl;
sleep(1);
}
return (void *)1;
}
5.分离线程
在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回和杀死。在被其他线程回收之前,他的内存资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收和杀死的。它的内存资源在它终止时由系统自动释放。
一个线程要么被显示的收回,要么通过调用pthread_detach函数被分离。
pthread_detach函数分离结合该线程的tid使用,线程能够通过以pthread_self()为参数的pthread_detach调用来分离它们自己。
例子:一个高性能Web服务器可能在每次收到Web浏览器的连接请求时都创建一个新的对等线程。因为每个连接都是由一个单独的线程独立处理的,所以对于服务器而言,就很没有必要的显示地等待每个对等线程终止。在这种情况下,每个对等线程都应该在它开始处理请求之前去分离,就可以在线程结束之后回收它的资源。
①同时使用pthread_detach和pthread_join
使用:如果我们同时使用pthread_detach和pthread_join会怎么样?这次我们先让pthread_detach被调用。
void *fuc(void *argc)
{
pthread_detach(pthread_self());
while (1)
{
cout << "我的tid为 " << pthread_self() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, fuc, (void *)"thread 1");
sleep(1); //为了先让线程运行
cout << "我是主线程" << endl;
int n = pthread_join(tid, NULL);
cout << n << "::" << strerror(n) << endl;
sleep(10); //为了避免主线程退出导致其他对等线程退出
return 0;
}
结果:
这次我们先让pthread_join被调用。
void *fuc(void *argc)
{
sleep(1);
pthread_detach(pthread_self());
int cnt = 5;//让线程自动退出
while (cnt--)
{
cout << "我的tid为 " << pthread_self() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, fuc, (void *)"thread 1");
cout << "我是主线程" << endl;
int n = pthread_join(tid, NULL);
if (n == 0)
{
cout << "join success!" << endl;
}
else
{
cout << n << "::" << strerror(n) << endl;
}
sleep(10); //为了避免主线程退出导致其他对等线程退出
return 0;
}
结果:
这俩现象说明两个函数pthread_detach和pthread_join谁先执行,则线程就会按先执行的函数的处理方式来处理线程。
②在线程中调用exit
我们让主线程不断打印,对等线程在2秒之后,通过exit函数退出,退出码为2。然后通过echo $? 来查进程退出码。
代码:
void *fuc(void *argc)
{
sleep(2);
cout << "My tid : " << pthread_self() << endl;
exit(2);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, fuc, NULL);
while (1)
{
sleep(1);
cout << "I'm come back" << endl;
}
return 0;
}
结果:
在任何一个线程内调用exit,都会致使进程退出。
避免篇幅过长,关于线程的更多知识,我们下篇见。