💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:Linux初窥门径⏪
🚚代码仓库:Linux代码练习🚚
🌹关注我🫵带你学习更多Linux知识
🔝
目录
前言
1.线程现象
2.线程等待
3.线程终止
4.重新理解线程
前言
在Linux 线程概念 中 我们知道Linux中的线程是轻量级的,这就意味着Linux中是没有线程这种概念的。所以用户要想用线程,OS不会提供,Linux程序员在应用层给我们提供了第三方库 pthread库
在线程概念章节中,最后简单演示了创建线程的代码,这里就有个问题了?一个进程中什么是主线程,一个线程要被调度,cpu怎么知道该调度谁了?
1.线程现象
最后线程概念简单演示了创建线程。
简单了说了 LWP 就是轻量级进程 也是线程的标识符
cpu通过LWP调用线程。 如果一个线程的LWP和PID的值是一样的。说明该线程就是主线程。
如果一个线程的LWP和PID的值不相等说明它是被新创建出来的线程。
那以前CPU调度进程是错的吗? 当然不是,以前我们学的是进程中单线程。所以给人感觉是CPU调度进程。说白了,我们以前学的进程单线程只是线程的子集。
这里代码和pthread_create函数在线程概念有详细解释Linux 线程概念
那如果 我们给其中一个线程发送线程会怎么样?
从上面结论我们可以得出 线程中只要有一个线程出现异常,那么其他线程都要被干掉。
讲个故事
进程就好比家庭,线程就好比家庭的成员。 家庭的成员各自干各自的事。爸妈上班,你上学,你爷爷奶奶安享晚年。大家干各自的事都是一个目的。让日子过的更好。
如果你在学校出了问题,老师是不是要请你爸妈来学校一趟,所以他们就会放下手中的工作。来学校处理你的事情。
所以一个线程出了问题,其他线程也会跟着出问题。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void show(const string &name)
{cout << name << "Say# " << "hello thread" << endl;
}
void *threadRoutine(void *args)
{while (true){cout << "new thread , pid: " << getpid() << endl;show("[新线程]");sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);while (true){cout << "main thread , pid: " << getpid() << endl;show("[主线程]");sleep(1);}return 0;
}
这段代码运行结果来看, 两个线程同时都在执行函数show,说明show函数被重入了
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 0;void *threadRoutine(void *args)
{while (true){printf("new thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);while (true){printf("main thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());g_val++;sleep(1);}return 0;
}
我们定义一个全局变量,可以看到两个线程都可以同时访问这个全局变量。主线程++ 新线程打印的值也是++后的值。地址也是一样。
这就说明一个问题 代码未初始区,已初始区。都是共享的。
那两个线程通信岂不是非常容易?
是的。
2.线程等待
一个进程中,能确定那个线程先运行吗?这个还真的不能确定是主线程先运行还是新线程先运行。
但是我们能确定主线程一定是最后退出的。为什么?
因为新线程就是主线程创建出来的。主线程如果不等待新线程那么就会出现类似父子进程中的僵尸进程情况。造成资源浪费。
而原生线程库中帮我们提供了线程等待的接口函数。pthread_join
老规矩先来了解函数原型、参数、返回值。
pthread_join
- 等待一个已终止的线程
函数原型
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
同样和pthread_create一样,需要在编译时链接-pthread
选项。
描述
pthread_join()
函数用于等待由thread
参数指定的线程终止。如果该线程已经终止,则pthread_join()
会立即返回。被指定的线程必须是可加入的(joinable)。
如果retval
参数不是NULL,则pthread_join()
会将目标线程的退出状态(即目标线程传递给pthread_exit(3)
的值)复制到*retval
指向的位置。(说人话就是**retval这个参数设置为空,不关心线程的退出情况)
如果目标线程被取消,则在*retval
中放置PTHREAD_CANCELED
。
如果多个线程同时尝试加入同一个线程,则结果未定义。如果调用pthread_join()
的线程被取消,则目标线程将保持可加入状态(即不会被分离)。
返回值
成功时,pthread_join()
返回0;出错时,返回一个错误编号。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 0;
void show(const string &name)
{cout << name << "Say# " << "hello thread" << endl;
}
void *threadRoutine(void *args)
{int cnt = 5;while (true){// cout << "new thread , pid: " << getpid() << endl;// show("[新线程]");printf("new thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());cnt--;sleep(1);if(cnt == 0) break;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);pthread_join(tid, nullptr);cout << "main thread quit..." << endl;return 0;
}
从运行结果来看,前4秒钟,主线程和新线程都是同时在跑的,第5秒新线程退出,最后才是主线程退出。这里我们可以得出一个结论:主线程在等待新线程退出的这个过程中,主线程其实是阻塞等待。
这里就有一个问题,新线程退出了,执行结果主线程是怎么拿到的?
为什么要传一个二级指针? 如果是传一个一级指针。pthread_join能拿到void*这个返回值吗?
就好比 int a = 0;函数能改变外面 a的值吗?肯定不能。 如果一个函数要改变a的值,那只能传递a变量的指针, 也就是所谓 int*。 这里void*就如同a变量。,void** 就如同 int* 。
新线程返回的是void*的指针,pthread_join就要拿到 取地址void* ,通过解引用。最后就能拿到void*了。
那我们直接改造代码
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 0;
void show(const string &name)
{cout << name << "Say# " << "hello thread" << endl;
}
void *threadRoutine(void *args)
{int cnt = 5;while (true){// cout << "new thread , pid: " << getpid() << endl;// show("[新线程]");printf("new thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());cnt--;sleep(1);if(cnt == 0) break;}return (void*)1;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);// while (true)// {// // cout << "main thread , pid: " << getpid() << endl;// // show("[主线程]");// printf("main thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());// g_val++;// sleep(1);// }void* retval;pthread_join(tid, &retval);cout << "main thread quit..." << " " << " retval: "<< (long long int)retval << endl;return 0;
}
3.线程终止
在进程中进程退出用的exit()和 _exit() 那线程也能用吗?
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 0;
void show(const string &name)
{cout << name << "Say# " << "hello thread" << endl;
}
void *threadRoutine(void *args)
{int cnt = 5;while (true){// cout << "new thread , pid: " << getpid() << endl;// show("[新线程]");printf("new thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());cnt--;sleep(1);if(cnt == 0) break;}exit(1);return (void*)1;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);while (true){// cout << "main thread , pid: " << getpid() << endl;// show("[主线程]");printf("main thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());g_val++;sleep(1);}void* retval;pthread_join(tid, &retval);cout << "main thread quit..." << " " << " retval: "<< (long long int)retval << endl;return 0;
}
新线程退出主线程也跟着退出了,也就是说,线程只要有一个退出了,其他线程都要跟着退出,这种情况并不是我们想要的。 原生线程库也是帮我们提供线程终止的接口 pthread_exit
谁调用就会终止谁,其他线程不会受影响。
函数原型:void pthread_exit(void *retval);
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 0;
void show(const string &name)
{cout << name << "Say# " << "hello thread" << endl;
}
void *threadRoutine(void *args)
{int cnt = 5;while (true){// cout << "new thread , pid: " << getpid() << endl;// show("[新线程]");printf("new thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());cnt--;sleep(1);if(cnt == 0) break;}pthread_exit((void*)100);//exit(1);//return (void*)1;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);// while (true)// {// // cout << "main thread , pid: " << getpid() << endl;// // show("[主线程]");// printf("main thread g_val:%d &_val:%p pid:%d\n", g_val, &g_val, getpid());// g_val++;// sleep(1);// }void* retval;pthread_join(tid, &retval);cout << "main thread quit..." << " " << " retval: "<< (long long int)retval << endl;return 0;
}
结果如我们所预期的一样我们拿到线程回调函数的返回值。但是回调参数是void*,这意味什么?意味着我们不仅仅只能传递字符常量,函数指针,整型、等等。我们还可以传递对象啊。下面就来一个优雅的写法!!!
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>using namespace std;
class Response
{
public:Response(int exitcode, int result): _exitcode(exitcode), _result(result) {}public:int _exitcode; // 计算结果是否可靠int _result; // 计算结果
};
class Request
{
public:Request(int start, int end, const string &threadname): _start(start), _end(end), _threadname(threadname) {}static void *SumCount(void *args){Request *rq = static_cast<Request *>(args);Response *rsp = new Response(0, 0);for (int i = rq->_start; i <= rq->_end; i++){rsp->_result += i;}return (void *)rsp;}public:int _start;int _end;string _threadname;
};int main()
{pthread_t tid;Request *rq = new Request(1, 100, "线程 1"); // 修正构造函数参数void *ret = nullptr;// 创建线程if (pthread_create(&tid, nullptr, Request::SumCount, rq) != 0){cerr << "Error creating thread" << endl;delete rq; // 如果线程创建失败,释放 rqreturn 1;}// 等待线程结束if (pthread_join(tid, &ret) != 0){cerr << "Error joining thread" << endl;}else{Response *rsp = static_cast<Response *>(ret);cout << "Result: " << rsp->_result << " " << "Exitcode: " << rsp->_exitcode << endl;delete rsp; // 释放 Response 对象}delete rq; // 释放 Request 对象return 0;
}
这样就符合面向对象的特性,和以前比起来就优雅太多了!!!
获取线程id的函数接口和线程分离接口比较简单就一起介绍了
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* arg)
{pthread_detach(pthread_self());printf("%s\n", (char*)arg);return NULL;
}
int main(void)
{pthread_t tid;if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0) {printf("create thread error\n");return 1;}int ret = 0;sleep(1);//很重要,要让线程先分离,再等待if (pthread_join(tid, NULL) == 0) {printf("pthread wait success\n");ret = 0;}else {printf("pthread wait failed\n");ret = 1;}return ret;
}
这段代码是单独说明线程分离。和前面的代码不同。C写的
string toHex(pthread_t tid)
{char hex[64];snprintf(hex, sizeof(hex), "%p", tid);return hex;
}//获取的ID是十进制的,我们转换成16进制。所以写了这个函数
打出的来的ID怎么和我们预想不一样?不应该是LWP吗? 这里就需要我们重新理解线程了。
4.重新理解线程
画了一个简单的图,这里的用户指的是程序员。由于Linux是没有线程的概念,所以说内核中OS根本就不知道线程的存在。所谓的LWP指的是轻量级进程ID,而不是线程的ID,而这个ID是OS方便管理轻量级进程,是给OS用的,而不是给用户用的。
由于我们用户是需要线程的概念,那这个所谓的线程又是谁给用户提供的?
很简单,由于我们使用的是原生线程库,这里简单解释一下什么是原生,线程库在我们安装Linux系统时,它就存在了。就如同你出生时,你的五官也是同时就存在了。所以线程库给我提供了线程的概念,而刚才打出来的tid像什么?像个地址。而且这个地址还是个高地址,那就说明了这个tid是地址空间中堆栈之间的地址,那不就是共享区吗?而线程库也是要加载内存中的。线程库又不是只有一个进程要用,其他进程也要用。这不就对上了?
那既然是线程库提供的,线程的那一套结构也是线程库维护的吗?
当然也是线程维护,为什么这么说?还是先看两个图
这个函数看参数就很恶心,对用户使用成本太大,轻量级进程就是用的这个创建的,线程库帮我们进行了封装。变成pthread_create这些接口。
pthread_create 对clone进行封装,所以每个线程,都会有个栈,而且这个栈还是独立的。而刚才讲的tid是个地址,其实是线程的起始地址,这样我们拿到了tib就能快速找到线程。
为什么线程的栈要是独立的?
线程局部存储:这是一种特殊的存储机制,允许每个线程拥有自己的变量副本,这些副本对于该线程是私有的,但对于其他线程则是不可见的。这不同于普通的局部变量,因为TLS变量的生命周期不仅限于函数调用的持续时间,而是与线程的生命周期相同。
由于线程有了独立栈的存在,所以栈就能存储线程执行过程中的局部变量、函数调用的参数、返回地址等信息。
关于线程控制,我们学习了5个接口函数,如何创造一个线程,为什么要等待一个线程,如何终止一个线程,线程分离之后的变化,以及获取线程的tid的函数接口。 最后通过线程的tid重新认识了线程的概念。