Linux 线程控制

devtools/2025/3/20 18:59:17/

目录

1、创建线程

2、线程等待

3、终止线程

4、线程等待

5、传递对象

6、C++11中的线程

7、线程ID与线程地址空间布局

8、验证结论

(1)各个线程有独立的栈结果

(2)主线程能拿到其中一个线程的变量

9、__thread关键字

10、分离线程


1、创建线程

  • thread:输出型参数,返回线程 ID。
  • attr:设置线程的属性,attr 为 NULL 表示使用默认属性。
  • start_routine:想让线程执行的任务,它是一个返回值 void*,参数 void* 的一个函数指针。
  • arg:回调函数的参数,若线程创建成功,在执行 start_routine 时,会把 arg 传入start_routine

 返回值:成功返回0,失败返回非0,数字是几,代表什么原因出错。

  • 传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量 errno 赋值以指示错误。
  • pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做),而是将错误代码通过返回值返回。
  • pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于pthreads 函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的 errno 变量的开销更小

代码如下:

#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;
void* threadrun(void* args)
{while(1){cout << "pthread is running,pid is:" << getpid() << endl;sleep(1);}return nullptr;
}
int main()
{pthread_t rid;pthread_create(&rid,nullptr,threadrun,nullptr);while(1){cout << "main thread is running,pid is" <<getpid() << endl;sleep(1);}return 0;
}

这里让新线程执行除 0 操作,我们发现它会影响整个进程。线程是进程的一个执行分支,除 0 错误操作会导致线程退出的同时,也意味着进程触发了该错误,进而导致进程退出。这也就是线程会使用代码健壮性降低的一个表现。

 

我们能看到这个线程崩了,整个进程会一起被干掉。

我们看这段代码

运行结果如下:

两个执行流都进入了show函数,就如我们上一篇写的,我们把show函数称作为可重入函数。

我们再来看一段代码

运行结果:

我们可以看到,主线程和新线程都可以看到这个变量被修改了。说明两个线程共享这个变量。

所以两个线程想要进行通信实在是太容易了

这里我们注意,如果我们设置gal为int的时候,会出现这样的报错

这是因为int是4个字节,我们传入第四个参数的时候,是void* 64机器下为8个字节,强转的类型大小不一样可能会报错,所以我们用long,long类型在32位下是4个字节,64位下是8个字节。

2、线程等待

那么这两个线程谁先进行退出呢?一般来说是新线程先退出的,然后主线程才能退出的,因为是主线程创建的它,它要对这个新线程进行管理。

如果我们主线程是一个死循环,而新线程一直不退出,那么也会造成类似于进程中的僵尸进程的问题(当然线程里没有这个说法)。所以新线程被创建出来以后,一般也要被等待,如果不等待,可能会造成类似于僵尸进程的问题。当然这个问题我们是无法验证出来的,因为新线程一退,我们查也就查不到了。但是确确实实会存在这个问题。

更重要的是,我们将新线程创建出来,就是让他就办事的,我们得知道它办的怎么样,结果数据是什么?

所以我们线程等待的两个目的:

  1. 防止内存泄漏
  2. 如果需要,我们也可以获取一下子进程的退出结果


下面是线程等待的函数

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//Compile and link with -pthread.

如果成功返回0,失败返回错误码。注意:线程里面所有的函数都不用errno错误码,而是直接返回一个错误码。这就保证了所有的线程都可以有一个返回的错误码,不需要去抢占全局的那个变量 

关于参数:

第一个参数是线程的tid

第二个参数是该线程结束时的返回值。注意*retval才是void*类型,也就是*retval才是函数的返回值。

如下图所示,当void*通过pthread_join的方式传递的时候,会产生一个临时变量。比如说,我们调用函数的时候传递&x,那么&x其实会被拷贝一份,我们这里暂且记作retavl。然后在pthread_join内部执行,*retval = z这一步。最终就成功的为x赋值了。即x就相当于一个输入型参数。

 

下面我们用代码演示一下

我们让新创建的线程退出,让主线程等待。主线程等待5秒,主线程也退出

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>using namespace std;void show(const string& name)
{ cout << name << "is running" << endl;
}
void* threadrun(void* args)
{long gal = (long)args;int cnt = 8;while(cnt--){show("[new pthread]");// cout << "pthread is running,pid is:" << getpid() << "gal:" << gal++ <<"   "<< "&gal:" << &gal << endl;sleep(1);}// int a = 10;// a /= 0;return nullptr;
}
int main()
{long gal = 0;pthread_t rid;pthread_create(&rid,nullptr,threadrun,(void*)gal);void* retval;pthread_join(rid,&retval);sleep(5);cout<< "main pthread quit!" << endl;// while(1)// {//     show("[main pthread]");//     // cout << "main thread is running,pid is" <<getpid() << "gal:" << gal++ <<"   "<< "&gal:" << &gal << endl;//     sleep(1);// }return 0;
}

运行结果如下:

我们现在利用一下这个*retavl

运行结果如下:

这里我们让新线程退出为什么不能用exit接口呢?

我们先来看看运行结果是什么样的

 我们看到主线程都没等待就全部退出了。

其实exit是用来终止进程,不能用来终止线程。

接下来我们来看终止线程的接口

3、终止线程

#include <pthread.h>
void pthread_exit(void *retval);
//Compile and link with -pthread.

它的作用是终止调用这个函数的线程,谁调用它就终止谁。参数是void*,和这个函数的返回值的含义是一样的。

运行结果如下

上面是新线程去调用pthread_exit接口,那么只有这个线程会退出,如果主线程去调用这个接口退出的话,那么整个进程都会终止 

运行结果人如下:进程直接退出了,没有进行等待。

4、线程等待

#include <pthread.h>
int pthread_cancel(pthread_t thread);
//Compile and link with -pthread.
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>using namespace std;void* runpthread(void*args)
{   string name = (char*)args;int cnt = 5;while(cnt--){cout << name << "pid:" << getpid() << endl;sleep(1);}
}
int main()
{pthread_t rid;pthread_create(&rid,nullptr,runpthread,(void*)"pthread-");sleep(1);pthread_cancel(rid);void* retval;pthread_join(rid, &retval); //main thread等待的时候,默认是阻塞等待的cout << "main thread quit..., ret: " << (long)retval << endl;return 0;
}

 

我们可以注意到,此时这个线程等待以后的返回值为-1

其实是因为一个线程如果被取消的话,会有这样一个宏

#define PTHREAD_CANCELED ((void *) -1)

换句话说,如果线程是被取消的,那么它退出时的返回码就是-1,即上面的宏

5、传递对象

其实线程的参数和返回值,不仅仅可以用来传递一般参数,也可以传递对象

我们可以用下面的代码来看

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>using namespace std;class Request
{
public:Request(int start,int end,string name):start_(start),endl_(end),pthread_name(name){}
public:int start_;int endl_;string pthread_name;
};
class Response
{
public:Response(int result,int exitcode):result_(result),exitcode_(exitcode){}
public:int result_;int exitcode_;
};
void* sumcount(void* args)
{Request* rq = (Request*)args;Response* rep = new Response(0,0);for(int i = rq->start_; i <=rq->endl_; i++){cout << rq->pthread_name << " is running calling  "<< i<< endl;rep->result_ += i;}   delete rq;return (void*)rep;
}
int main()
{pthread_t tid;Request* rq = new Request(1,100,"pthread-1");pthread_create(&tid,nullptr,sumcount,rq);void* ret;pthread_join(tid,&ret);Response* rep = (Response*)ret;cout << "rep->result_:" << rep->result_ << ",exitcode:" << rep->exitcode_ <<endl;delete rep;return 0;
}

运行结果如下:

所以它就可以用来求出和。让每一个线程只执行其中的一部分计算,然后我们自己在将这些结果合并起来。

并且我们发现,我们的这些对象都是在堆区创建的。并且我们是交叉使用的,说明堆空间的也是被线程共享使用的

6、C++11中的线程

目前,我们使用的是原生线程库(pthread库)

其实C++11 语言本身也已经支持多线程了,它与我们的原生线程库有什么关系呢?

C++11的线程需要用下面的库

#include<thread>

代码如下:

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
using namespace std;
void threadrun()
{while(true){cout << "I am a new thread for C++" << endl;sleep(1);}
}int main()
{thread t1(threadrun);int cnt = 5;while(cnt--){cout << "main pthread is running " <<endl;sleep(1);}t1.join();return 0;
}

运行结果:

 

我们需要注意的是,C++11中的线程库其实底层还是封装了linux提供的系统调用接口,所以我们编译的时候还是需要使用-lpthread选项的。

而C++11其实是有跨平台性的。因为它在不同平台下已经写好了不同版本的库。所以对我们而言,不同的平台写代码是没有感觉的。

我们最好使用C++的多线程。因为具有跨平台性

7、线程ID与线程地址空间布局

我们先来看一段代码

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
using namespace std;void* runpthread(void* args)
{string name = (char*)args;printf("%s, tid:%p\n", name.c_str(), pthread_self());return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,runpthread,(void*)"pthread-1");printf("main create a new pthread id is:%p\n", pthread_self());pthread_join(tid,nullptr);return 0;
}

运行结果如下:

我们能看到这个tid地址很大,那它具体存放在哪呢?


我们知道的是,内核中并没有明确的线程的概念,只有轻量级进程的概念

而轻量级进程接口是这样的

这个接口我们一般是不用的,包括fork的底层其实用的也是这个接口

这个的第一个参数是一个函数指针,第二个参数是自定义的一个栈…

这个接口是被pthread线程库封装了。

所以我们采用的是pthread_create,pthread_join这些接口。

如下图所示,这个clone这个接口它需要提供一个回调函数,独立栈结构等,用它去维护线程。而这些都是线程库在做的事情,也就是线程的概念是库给我们维护的,我们用的原生线程库,也要加载到内存中,因为都是基于内存的。线程库是一个动态库,经过页表映射后,也要到共享区的。这些栈都是在共享区创建的。我们的线程库只需要维护线程的概念即可,不用维护线程的执行流,不过线程库注定了要维护多个线程属性集合,线程也要管理这些线程,先描述在组织。而这个线程控制块它就要可以找到这些回调函数,独立栈,以及在内部的LWP。这个线程控制块就是用户级线程

 

所以我们就将这个下面的这个叫做线程的tcb。而每一个tcb的起始地址,叫做线程的tid 

所以拿着这个tid,就可以找到库里面的属性了。

而我们前面打印出来的这个地址,我们也可以看到,它是比较大的,其实它就是介于堆栈之间的共享区

每一个线程都必须要有自己的独立栈结构,因为它有独立的调用链,要进行压栈等操作。其中主线程用的就是地址空间中的这个栈。剩下的轻量级进程在我们创建的时候会先创建一个tcb,它里面的起始地址作为线程tid,它的里面有一个默认大小的空间,叫做线程栈,然后内核中调用clone创建好执行流。在clone中形成的临时数据都会压入到这个线程库中的栈结构中。

所以除了主线程,所有其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向的用户tcb中,这个tid是这个线程所在动态库的起始地址,tid是虚拟地址。

总结:

  • Linux OS 没有真正意义上的线程,而是用进程 PCB 模拟的,这就叫作轻量级进程。其本身没有提供类似线程创建、终止、等待、分离等相关 System Call 接口,但是会提供轻量级进程的接口,如 clone。所以为了更好的适配,系统基于轻量级进程的接口,模拟封装了一个用户层的原生线程库 pthread。这样,系统通过 PCB 来进行管理,用户层也得知道线程 ID、状态、优先级等其它属性用来进行用户级线程管理。
  • pthread_create 函数会产生一个线程 ID,存放在第一个参数指向的地址中,该线程 ID 和前面说的线程 ID LWP 不是一回事。LWP 属于进程调度的范畴,因为线程是轻量级进程,是 OS 调度器的最小单位,所以需要一个数值来唯一表示该线程。pthread_create 函数的第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID,属于 NPTL 线程库的范畴,线程库的后续操作,就是根据该线程 ID 来操作线程。
  • 原生线程库是一个库,它在磁盘上就是一个 libpthread.so 文件,运行时加载到内存,然后将这个库映射到共享区,此时这个库就可以被所有线程执行流看到了。此时有两个 ID 概念,一个是在命令行上看到的 LWP,一个是在用户层上看到的 tid。前者是在系统层面上供 OS 调度的,后者是 pthread_create 获得的线程 ID,它是一个用户层概念,本质是一个地址,就是 pthread 库中某一个起始位置,也就是对应到共享区中的某一个位置。所以线程数据的维护全都是在 pthread 线程库中去维护的,上图所示,其中会包含每个线程的局部数据,struct pthread 就是描述线程的 TCB,线程局部存储可以理解是不会在线程栈上保存的数据,我们在上面说过线程会产生各种各样的中间数据,如上下文数据,此时就需要独立的栈去保存,它就是线程栈。而下图中拿到的 tid 就是线程在共享区中线程库内的相关属性的起始地址,所以只要拿到了用户层的 tid,就可以在库中找到线程相关的属性数据,很明显 tid 和 LWP 是 1 : 1 的,而主线程不使用库中的栈结构,直接使用地址空间中的栈区,称为主线程线。 

8、验证结论

(1)各个线程有独立的栈结果

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
#include <vector>
using namespace std;void* threadrun(void* args)
{long gal = (long)args;while(true){cout << "pthread is running,tid is:" << pthread_self() << "gal:" << gal++ <<"   "<< "&gal:" << &gal << endl;sleep(1);}
}
int main()
{vector<pthread_t> tids;long flag = 0;for(int i = 0; i < 5; i++){pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)flag);tids.push_back(tid);}for(auto& e:tids){pthread_join(e,nullptr);}return 0;
}

运行结果如下:

我们能看到传入到每个线程的falg地址不一样,但它们地址又很相近。

说明了每一个线程都有自己的栈,该栈存放该执行流创建的变量等,并且他们都在共享区的动态库中。

(2)主线程能拿到其中一个线程的变量

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
#include <vector>
using namespace std;int *p = NULL;
struct pthreadname
{string threadname;
};
void InitThreadData(pthreadname *td, int number)
{td->threadname = "pthread-" + to_string(number); // thread-0
}
void* threadrun(void* args)
{int test_i = 0;pthreadname* name = (pthreadname*)args;if(name->threadname == "pthread-2") {p = &test_i;}int cnt = 3;while(cnt--){cout << name << "tid is:" << pthread_self() << "  test_i:" << test_i << "   &test_i" << &test_i <<endl;// cout << "pthread is running,tid is:" << pthread_self() << "gal:" << gal++ <<"   "<< "&gal:" << &gal << endl;sleep(1);}
}
int main()
{vector<pthread_t> tids;for(int i = 0; i < 5; i++){pthreadname* name = new pthreadname;pthread_t tid;InitThreadData(name,i);pthread_create(&tid,nullptr,threadrun,name);tids.push_back(tid);}sleep(5);cout << "p:" << p << endl;for(auto& e:tids){pthread_join(e,nullptr);}return 0;
}

运行结果如下:

9、__thread关键字

上面我们验证了主线程能拿到其中一个线程的变量,说明进程与进程之间没有秘密。

我们可以用__thread关键字让一个线程有自己的私有全局变量。

__thread int g_iThreadCount = 0;

这实际是线程的局部存储。

让我们写段代码验证一下

#include <iostream>
#include <pthread.h>
using namespace std;//一个用__thread关键字修饰的全局变量
__thread int g_iThreadCount = 0;void *pthreadFunc1(void *pArg)
{g_iThreadCount += 1;cout << "pthreadFunc1::g_iThreadCount = " << g_iThreadCount << endl;pthread_exit((void *)1);
}void *pthreadFunc2(void *pArg)
{g_iThreadCount += 2;cout << "pthreadFunc2::g_iThreadCount = " << g_iThreadCount << endl;pthread_exit((void *)2);
}int main(void)
{int iRet;pthread_t pthreadId1;pthread_t pthreadId2;pthread_create(&pthreadId1, NULL, pthreadFunc1, NULL);pthread_create(&pthreadId2, NULL, pthreadFunc2, NULL);pthread_join(pthreadId1, NULL);pthread_join(pthreadId2, NULL);return 0;
}

运行结果如下:

注意:__thread只能够定义内置类型,不能定义自定义类型。 

10、分离线程

  • 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成内存泄漏。
  • 如果不关心线程的返回值,join 则是一种负担,这个时候,可以使用分离,此时就告诉系统,当线程退出时,自动释放线程资源,这就是线程分离的本质。
  • joinable 和 pthread_detach 是冲突的,也就是说默认情况下,新创建的线程是不用 pthread_detach。
  • 就算线程被分离了,也还是会和其它线程影响的,因为它们共享同一块地址空间。

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离: 

joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的。

注意:没有线程替换这种操作,但可以在线程中执行进程替换系列函数。这是因为新线程内部执行进程替换函数,这看起来像是把新线程中的代码替换了,但实际会把主线程中的代码也替换了,因为主线程和新线程共享地址空间,所以新线程内部进程替换后,所有的线程包括主线程都会被影响。所以轻易不要在多线程中执行进程替换函数。

我们来看一段代码

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
#include <vector>
#include <cstdio>
#include <cerrno>
#include <cstring>
using namespace std;int flag = 0;
void* runthread(void*args)
{pthread_detach(pthread_self());while(true){cout << (char*)args << ":  " << flag++ << "&" <<&flag<<endl;sleep(1);break;}pthread_exit((void*)11);
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,runthread,(void*)"pthread-1");while(true){cout<<"main thread:"<<flag<<"  &" <<&flag<<endl;sleep(1);break;}int n =pthread_join(tid,nullptr);cout << "n:" << n << "errstring: " << strerror(n) <<endl;return 0;
}

结果如下:

 

pthread_join 返回的是 22,说明等待失败了,然后返回,进程终止。其实一个线程被设置为分离状态,则该线程不应该被等待,如果被等待了,结果是未定义的,至少一定会等待出错。

 


http://www.ppmy.cn/devtools/168700.html

相关文章

(二)Reactor核心-前置知识1

本章是Reactor核心-前置知识&#xff08;第一期&#xff09;&#xff0c;主要讲解Lambda表达式。回忆上一章我们学习了什么是响应式编程、基础概念、必读知识。一篇文章我不想写得太长了&#xff0c;文章不像是视频或者图片比较生动&#xff0c;文章太长了容易犯困。所以我直接…

在处理欧拉函数时如何使用逆元

1. 逆元的引入 在计算欧拉函数时&#xff0c;如果 (n) 是质数&#xff0c;那么 (\phi(n) n - 1)&#xff0c;这是直接的结果。然而&#xff0c;当 (n) 是合数时&#xff0c;我们需要处理分母中的质因数 (p_i)。 为了高效计算 (\phi(n))&#xff0c;尤其是在编程实现中&#…

MSP430 Proteus 仿真作品

https://www.dong-blog.fun/post/1998 1 、 电子万年历&#xff08;采用 DS1302 及 及 TC72 等芯片&#xff09; 基本要求&#xff1a; 可显示年、月、日、星期、时、分、秒&#xff1b; 有温度显示功能。 发挥部分&#xff1a; 可调节时间和日期&#xff1b; 有农历显示功能 &…

汇能感知高品质的多光谱相机VSC02UA

VSC02UA概要 VSC02UA是一款高品质的200万像素的光谱相机&#xff0c;适用于工业检测、农业、医疗等领域。VSC02UA 包含 1600 行1200 列有源像素阵列、片上 10 位 ADC 和图像信号处理器。它带有 USB2.0 接口&#xff0c;配合专门的电脑上位机软件使用&#xff0c;可进行图像采集…

使用Python进行数据分析时,CSV文件导入的两种方法

在使用 Python 进行数据分析时&#xff0c;有多种方法可以导入 CSV 文件&#xff0c;下面详细介绍两种常用的方法&#xff1a; 1. 使用csv模块 csv模块是 Python 标准库的一部分&#xff0c;无需额外安装。它提供了一种简单且基础的方式来读取和写入 CSV 文件。 python imp…

Excel(函数进阶篇):函数与控件、定义名称、OFFSET函数、动态抓取图片

目录 函数与控件定义名称制作二级下拉菜单OFFSET函数动态抓取数据 生成折线图OFFSET函数与数据透视表让文本公式重新运算动态抓取图片透视表切片器 抓取照片制作带照片的抽奖小工具条件格式创建甘特图 函数与控件 实现员工信息表的查询&#xff0c;最终效果↓↓↓ 详细实现步骤…

C语言每日一练——day_8

引言 针对初学者&#xff0c;每日练习几个题&#xff0c;快速上手C语言。第八天。&#xff08;连续更新中&#xff09; 采用在线OJ的形式 什么是在线OJ&#xff1f; 在线判题系统&#xff08;英语&#xff1a;Online Judge&#xff0c;缩写OJ&#xff09;是一种在编程竞赛中用…

Java面试黄金宝典3

1. 什么是 NIO 原理 缓冲区&#xff08;Buffer&#xff09;&#xff1a; 它是一个线性的、有限的基本数据元素序列&#xff0c;本质上是一块内存区域&#xff0c;被包装成了一个对象&#xff0c;以便于进行高效的数据读写操作。不同类型的基本数据都有对应的Buffer子类&#xf…