前言: 本节内容是线程的控制部分的第二个小节。 主要是列出我们的线程控制部分的几个细节性问题以及我们的线程分离。这些都是需要大量的代码去进行实验的。所以, 准备好接受新知识的友友们请耐心观看。 现在开始我们的学习吧。
ps:本节内容适合了解线程的基本控制(创建, 等待, 终止)的友友们进行观看哦。
目录
线程的栈
准备文件
makefile
核心代码
创建test_i栈区变量
利用全局变量拿到别的执行流数据
局部性存储
线程分离
主线程分离
自己分离自己
首先我们的系统之中,有下面四种情况。
左上角是只有一个线程一个进程的情况, 右上角是一个进程多个线程的情况。 左下角是多个进程里面有一个线程的情况。 右下角是多个进程里面有多个进程的情况。
那么, 其实我们的linux当中, 其实是分为用户级线程和内核LWP。 这两个加起来, 才是我们的linux下真正的线程。 其中, 我们的linux其实是属于用户级线程。 里面的用户级线程与内核LWP的比率为 1 : 1。
线程的栈
现在我们谈一谈这个栈, 这个栈并不是简简单单的用来入栈出栈, 定义变量。 实际上, 我们的每一条执行流的本质就是一条调用链, 从main函数开始从上往下执行, 我们会依次执行各种函数, 当我们进行调用函数时, 本质上就是在栈当中先为该函数形成一个独立的栈帧结构。 所以这个栈其实就是被整体使用的, 依次把一个一个地调用链所对应的栈帧结构宏观上在栈上依次开辟。 然后我们每一次定义变量, 都是在栈帧结构里面去定义的, 这个栈结构, 本质是为了支持我们在应用层来完成我们的整个的调用链所对应的临时空间的开辟和释放。 所以, 这些线程为了能够拥有独立的调用链, 就必须拥有属于自己的调用栈!
现在我们利用代码来测试一下:
准备文件
准备好两个文件
makefile
再将makefile准备出来
mythread.exe:mythread.cpp
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf mythread.exe
核心代码
这串代码分为几个板块: 定义线程的信息的结构体、线程信息的初始化、将整形转化为字符串类型、线程的执行代码、主函数
#include<iostream>
using namespace std;
#include<pthread.h>
#include<vector>
#include<unistd.h>
#define NUM 5 //创建多个执行流, NUM为执行流个数
using namespace std;
//线程的数据信息。
struct threadData
{
string threadname;
};
//将整形以十六进制转化为字符串类型
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", tid);
return buffer;
}
//线程信息的初始化
void InitthreadData(threadData* td, int number)
{
td->threadname = "thread-" + to_string(number);
}
//新线程的执行代码
void* threadRuntine(void* args)
{
threadData* td = static_cast<threadData*>(args);
int i = 0;
while (i < 5)
{
cout << "pid: " << getpid() << ", tid: "
<< toHex(pthread_self()) << ", name: "
<< td->threadname << endl;
i++;
sleep(2);
}
delete td;
return nullptr;
}
int main()
{
vector<threadData*> tids;
//我们创建多个执行流, 为了能够验证每个线程都有一个独立的栈结构
for (int i = 0; i < NUM; i++)
{
//每一个线程都要有一个线程的信息, 并且这个线程的信息我们在堆区开辟, 那么所有的线程其实都能够看到这个线程的信息, 因为堆区是共享的。
threadData* td = new threadData();
pthread_t tid;
InitthreadData(td, i); //初始化线程的信息。
pthread_create(&tid, nullptr, threadRuntine, td);
tids.push_back(tid);
sleep(2);
}
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
然后我们就能看到这种情况。
创建test_i栈区变量
在线程的执行代码块里面添加一个test_i变量, 然后打印这个变量。
//新线程的执行代码
void* threadRuntine(void* args)
{
threadData* td = static_cast<threadData*>(args);
int test_i = 0;
int i = 0;
while (i < 5)
{
cout << "pid: " << getpid() << ", tid: "
<< toHex(pthread_self()) << ", name: "
<< td->threadname
<< ", test_i: " << test_i << ", &test_i: " << &test_i << endl;
i++;
test_i++;
sleep(2);
}
delete td;
return nullptr;
}
下面就是运行结果, 从图中我们可以看到, 每一个执行流都有自己的独有的一份test_i, 并且他们的值都是从零开始, 一直加到4。而且, 每个变量的地址都不一样, 所以每个线程都会有自己独立的栈结构。当我们的线程执行到threadRuntine, 就会在自己的栈结构里面开辟自己的栈帧, 然后创建test_i也是在自己刚刚创建的栈帧中创建。
利用全局变量拿到别的执行流数据
创建一个全局变量p
然后在线程执行的代码里面, 写上要拿哪一个线程的什么数据:
为了确认真正的拿到了这个数据, 在程序的最后打印这个数据:
下面是运行结果:
由上面的结果我们其实就能够知道:在线程中根本没有秘密, 只不过要求线程有独立的栈, 但是这个独立的栈本质上还是在地址空间的共享区中。 所以, 我们每个线程叫做都有一个独立的栈结构, 而不是一个私有的栈结构。 就是因为这个栈结构能够被别人访问到, 而私有的意思是别人看不到。 ——所以, 线程与线程之间没有秘密。 线程的栈上的数据,也是可以被其他线程看到并访问的。
局部性存储
我们之前说过, 全局变量是可以被所有线程看到并访问的。但是如果线程想要一个私有的全局变量呢? 那么我们就需要在全局变量前面加一个__thread。 下面用代码来进行验证:
我们的核心代码还是上面写的代码。
并且为了方便观察, 将创建线程每隔1000微秒(使用usleep函数)创建一个线程。 然后每隔2秒打印一次数据:
#include<iostream>
using namespace std;
#include<pthread.h>
#include<vector>
#include<unistd.h>
#define NUM 5 //创建多个执行流, NUM为执行流个数
using namespace std;
int* p = nullptr;
__thread int g_val = 0;
//线程的数据信息。
struct threadData
{
string threadname;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", tid);
return buffer;
}
void InitthreadData(threadData* td, int number)
{
td->threadname = "thread-" + to_string(number);
}
//新线程的执行代码
void* threadRuntine(void* args)
{
threadData* td = static_cast<threadData*>(args);
int i = 0;
while (i < 5)
{
cout << "pid: " << getpid() << ", tid: "
<< toHex(pthread_self()) << ", name: "
<< td->threadname
<< ", g_val: " << g_val
<< ", &g_val: " << &g_val << endl;
i++;
g_val++;
sleep(2);
}
delete td;
return nullptr;
}
int main()
{
vector<pthread_t> tids;
//我们创建多个执行流, 为了能够验证每个线程都有一个独立的栈结构
for (int i = 0; i < NUM; i++)
{
threadData* td = new threadData();
pthread_t tid;
InitthreadData(td, i);
pthread_create(&tid, nullptr, threadRuntine, td);
tids.push_back(tid);
usleep(1000);
}
//
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
下面是运行结果, 运行结果中g_val都是从0开始, 然后各自加各自的,互不影响。 而且每个g_val的地址也不相同。这里的这个__thread, 叫做编译选项。每一个线程都访问同一个全局变量, 但是在访问的时候, 每一个全局变量对于每一个线程来说, 都是各自私有一份的。 这种技术叫做线程的局部性存储!
另外, 我们需要知道的一点就是__thread只能修饰内置类型, 不能修饰自定义类型。
那么, 这个局部性存储有什么作用呢? 就比如我们的线程要进行多次函数调用并且函数都要用到它,而且又不想和别的线程共享这份资源的时候, 我们就可以使用线程的局部性存储。
线程分离
在我们的默认情况下, 新创建的线程是joinable的, 线程退出后, 需要对其进行pthread_join操作, 否则无法释放资源造成内存泄露。 但是我们可以告诉操作系统, 当进程退出的时候, 不需要主线程等待, 而是自动释放资源, 这个操作就是线程分离。
接口如下:
参数就是线程的tid。 返回值和之前一样,就是成功零被返回, 失败返回错误码。
主线程分离
然后我们测试一下线程分离, 代码只改变main函数里面的就可以。 主要就是在进行线程等待之前先将线程分离。 然后等待的时候就会等待错误, 返回错误码。同时我们也可以打印一下错误码观察错误信息。
int main()
{
vector<pthread_t> tids;
//我们创建多个执行流, 为了能够验证每个线程都有一个独立的栈结构
for (int i = 0; i < NUM; i++)
{
threadData* td = new threadData();
pthread_t tid;
InitthreadData(td, i);
pthread_create(&tid, nullptr, threadRuntine, td);
tids.push_back(tid);
usleep(1000);
}
//
for (auto e : tids)
{
pthread_detach(e);
}
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i], nullptr);
cout << "n = " << n << ", who: " << toHex(tids[i])
<< ", " << strerror(n) << endl;
}
return 0;
}
运行结果如下, 可以发现运行结果如同我们的猜测, 都是返回错误码。 然后我们可以打印一下
自己分离自己
上面的情况是在主线程分离新线程。 我们也可以在新线程里面自己分离自己。
//新线程的执行代码
void* threadRuntine(void* args)
{
pthread_detach(pthread_self());
//
threadData* td = static_cast<threadData*>(args);
number = pthread_self();
int i = 0;
while (i < 5)
{
cout << "pid: " << getpid() << ", tid: "
<< toHex(number) << ", name: "
<< td->threadname
<< ", g_val: " << g_val
<< ", &g_val: " << &g_val << endl;
i++;
g_val++;
sleep(2);
}
delete td;
return nullptr;
}
然后我们的结果其实和上面的是一样的:
其实线程的分离, 线程是否分离其实是一种属性状态。 一开始默认线程是不分离的,是joinable的。本质上就是线程库里面的线程数据结构里有一个是否可分离的标记位, 开始默认是joinable的,一旦设置由零变一, 就是线程分离。 而线程分离呢, 说是分离, 但是其实和原本的进程还是在共享一份资源, 只是这个线程处于分离状态, 线程退出和进程没有关系了!
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!