目录
Linux线程概念
1. 什么是线程
2. 重新定义线程和进程
3. 重讲地址空间
4. 线程的优点
5. 线程的缺点
6. 线程异常
7. 线程用途
Linux进程VS线程
1. 进程和线程
2. 进程的多个线程共享
3. 线程为什么进程要更加轻量化?
Linux线程控制
1. POSIX线程库
2. 创建线程
3. 线程等待
4. 线程终止
5. 重谈线程的参数和返回值
6. C++11多线程vs原生线程库
7. 创建多个线程
8. 线程分离
9. 线程ID及进程地址空间布局
一般在Linux教材里面线程是这样定义的:
线程是在进程内部运行的一个执行分支,线程的执行粒度,要比进程更加细。
也就是一个进程内可能存在多个线程。所以进程和线程的比例关系是进程:线程 = 1:n
在OS中存在这么多的线程,那么OS要不要管理线程呢?如何管理呢?—— 先描述,再组织。
因此我们就可以得出一个推论:线程也应该要有线程控制块TCB。我们window下的多线程就是这样子做的。但是操作系统管理进程已经非常复杂,同样的方式管理线程会更复杂。
所以Linux下的多线程并没有像上面说的那样通过创建数据结构,然后通过管理数据结构从而达到管理线程的目的。那我们Linux下的多线程是怎么做的呢?
Linux管理线程采用的是复用进程数据结构和管理算法。
我们知道创建一个进程,我们需要为它创建一些列的数据结构,例如:PCB(进程控制块)、mm_struct(进程地址空间)、页表和file_struct等
那如果我们在创建进程时,只创建task_struct,将那个创建出来的进程的task_struct和父进程的task_struct共享虚拟地址空间和页表,并将父进程的资源(代码+数据),划分为若干份,让每个task_struct使用会是怎么样的呢?
CPU此时看到的PCB是<=我们之前讲的PCB的概念的,CPU只有调度执行流的概念,在CPU看来一个PCB就是一个需要被调度的执行流。(如果进程只有一个线程,线程 = 执行流 = 进程,如果进程有多个线程,线程 = 执行流 < 进程)
这就是我们Linux下的线程,Linux中并没有像windows下为线程专门设计TCP,而是使用进程PCB来模拟线程。
Linux管理线程的方法比Windows的方法好在哪里呢?
- 不用维护复杂的进程和线程的关系,不用单独为线程实现管理算法,直接使用进程的一套相关的方法,OS只需要关注在线程间的资源分配上就可以了。
什么叫线程?
我们认为,线程就是操作系统调度的基本单位!!
我们上面说线程是在进程内部运行的一个执行分支,这里的内部是什么意思呢?那什么又叫做一个执行分支呢?
这里的内部指的是线程是在进程的虚拟地址空间中运行的。执行分支指的是CPU调度的时候只看PCB,每一个PCB曾经被指派过指向方法和数据,CPU是可以直接调度的。
什么叫进程?
我们之前认为的进程:进程 = 内核数据结构(task struct) + 代码和数据
了解了Linux下的线程之后,我们又该如何理解我们之前讲的进程呢?
学习了线程后,我们把下面用红色方框圈起来的内容,我们将这个整体叫做进程!!
我们从内核视角来看进程就是承担分配系统资源的基本实体!!
之前我们讲的进程,内部只有一个执行流。学习了线程之后,我们重新定义的进程,内部可以具有多个执行流。创建进程的 "成本非常高",成本:时间+空间,创建一个进程要使用的资源是非常多的。
小结:我们从内核视角来看进程就是承担分配系统资源的基本实体!! 而线程就是CPU调度的基本单位,承担进程资源的一部分的基本实体,进程划分资源给线程。总得来说 Linux下的线程就是轻量级进程。
前面我们说线程就是CPU调度的基本单位,承担进程资源的一部分的基本实体,进程划分资源给线程,那么如何理解资源分配各个线层呢?下面我们通过重谈地址空间来解决这个问题。
我们先来解决下面这个问题——虚拟地址是如何转换到物理地址的? ? ? 32位虚拟地址为例
虚拟地址是多少位的? 32位
我们先来解释下面几个概念:
虚拟地址的32位地址划分成三部分:10+10+12,他们从全0到全1进行穷举,并且转化成10进制数
虚拟地址是如何转换到物理地址的过程:
访问任何变量都是:起始地址+类型 = 起始地址 + 偏移量(X86的特点)
一个整型有4个字节,每个字节一个地址,我们&a只拿到了最低位地址,然后根据整型是4个字节,我们往后取4个地址就可以取到整个整型了。
也就是说,我们c语言中变量取到的地址都是他众多字节当中的最低位地址,然后CPU可以根据变量的类型,通过起始地址加偏移量的方式就可以知道每次我们要读取多少字节,加载多少字节。
类也一样,编译完之后就没有类的概念了。也就是说类也是内置类型的集合。
我们来看下面这个图加深理解:
缺页中断:当软件试图访问已映射在虚拟地址空间中,但是并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断,称为缺页中断。(中间10位得到的地址找不到对应的二级页表表项,或者二级页表表项存放的页框的起始地址根本就没有建立映射关系。)
我们CPU当中还有CR2寄存器,当我们缺页中断时,CR2寄存器可以保存最后一次出现缺页中断的全32位线性地址。在缺页中断发生时,CPU会通过读取CR2来获取导致缺页中断的线性地址,以便进行错误处理和恢复操作。因此,CR2寄存器对于CPU的错误处理和内存管理具有重要的意义。
性能损失
健壮性降低
缺乏访问控制
编程难度提高
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程
中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 资源消耗:线程是进程内的一部分,因此线程的创建、切换和销毁等操作相对于进程更加轻量级。在操作系统中,创建新的进程需要分配独立的地址空间和虚拟地址空间,并且建立众多的数据表来维护其代码段、堆栈段和数据段,开销较大。而线程则共享进程的资源,所以线程的创建、切换和销毁等操作对系统资源的消耗较小。
- 并发性:线程可以共享进程的资源,使得多个线程之间可以直接通信和协作,而无需通过操作系统进行复杂的切换和通信操作。这种并发性使得线程在处理大量任务时更加高效。
CPU的cache存放的是CPU刚用过或循环使用的一部分数据,如果CPU需要再次使用该部分数据时可从Cache中直接调用,这样就避免了重复存取数据。对于进程和线程,CPU的cache对它们的执行效率有重要影响。线程的切换不需要重新cache数据,大大提高了效率。
创建线程函数原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
功能:创建一个新的线程
参数:
返回值:成功返回0;失败返回错误码
错误检查:
下面我们来使用一下这个函数创建线程:
void *threadRoutine(void *args)
{
while (true)
{
cout << "new thread, pid: " << getpid() << endl;
sleep(1);
}
return nullptr //走到这里默认线程退出了!
}
int main()
{
// PTHREAD_CANCELED;
// 是一个很大的数字
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1"); // 不是系统调用
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
makefile文件:
mythread:mythread.cc
g++ -o $@ $^ -lpthread
clean:
rm -f mythread
注意要加上-lpthread选项
运行结果:
线程id跟进程id一样的线程是主线程。我们看到每个线程的id不一样,说明线程是操作系统调度的基本单位。
我们再来做几个实验验证一下上面讲的理论:
1.线程的健壮性差:一个线程被kill整个进程就被kill
我们看到,不管是kill新线程还是主线程,都会把整个进程给kill掉。验证了线程健壮性差的缺点。
一个线程出现异常,整个进程都退出:
#include
#include
#include
using namespace std;
// new thread
void *threadRoutine(void *args)
{
while (true)
{
cout << "new thread, pid: " << getpid() << endl;
sleep(1);
int a = 10;
a /= 0;
}
}
int main()
{
// PTHREAD_CANCELED;
// 是一个很大的数字
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1"); // 不是系统调用
sleep(1); // 只是为了保证新线程已经启动
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
可以看到只要其中一个线程出现除零错误,整个进程都退出了,这也说明了线程的健壮性差。
2. 进程的多个线程共享
同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程
中都可以调用,如果定义一个全局变量
#include
#include
#include
using namespace std;
int g_val = 100;
void show(const string &name)
{
cout << name << "say# "
<< "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
const char *name = (const char*)args;
while (true)
{
printf("%s, pid: %d, g_val: %d, &g_val: 0x%p\n", name, getpid(), g_val, &g_val);
show("[new thread]");
sleep(1);
}
}
int main()
{
// PTHREAD_CANCELED;
// 是一个很大的数字
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1"); // 不是系统调用
sleep(1); // 只是为了保证新线程已经启动
while (true)
{
printf("main thread pid: %d, g_val: %d, &g_val: 0x%p, create new thread tid: %p\n", getpid(), g_val, &g_val, tid);
show("[main thread]");
sleep(1);
g_val++;
}
return 0;
}
运行结果:
我们看到两个线程都可以调用show函数和使用g_val全局变量,且主线程对g_val进行修改,新线程也可以看到修改后的值。
为什么需要线程等待?
线程也是需要被等待的,如果不等待,可能会导致类似于“僵尸进程”的问题。
下面来给大家介绍一个线程等待的函数——pthread_join()
功能: 等待线程结束
pthread_join函数的函数原型如下:
参数:
返回值:成功返回0;失败返回错误码
下面我们就来使用一下这个函数:
#include
#include
#include
using namespace std;
// new thread
void *threadRoutine(void *args)
{
// const char *name = (const char*)args;
int cnt = 5;
while (true)
{
cout << "new thread, pid: " << getpid() << endl;
sleep(1);
cnt--;
if(cnt == 0) break;
}
return (void*)100; //走到这里默认线程退出了!
}
int main()
{
// PTHREAD_CANCELED;
// 是一个很大的数字
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1"); // 不是系统调用
sleep(7);
void *retval;
pthread_join(tid, &retval);// main thread等待的时候,默认是阻塞等待的!为什么我们在这里join的时候不考虑异常呢??做不到!
cout << "main thread quit ..., ret: " << (long long int)retval << endl;
return 0;
}
运行结果:
我们通过监控脚本查看线程的运行状态:
while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1; done
我们看到主线程成功等待另一个线程退出,通过retval指针收到函数的返回值。通过监控脚本我们看到新线程退出后对主线程没有影响,说明线程等待的时候,默认是阻塞等待的!(不关心线程的退出码,
将join函数的第二个参数设置为nullptr即可
。)
我们之前学习进程等待的时候,我们可以通过wait函数或者是waitpid函数的输出型参数status,获取到进程的退出码、退出信号以及core dump标志。
我们的线程和进程一样,退出有以下三种情况:
我们可以通过join的第二个参数拿到线程的返回值,从而知道线程跑完,结果正确还是不正确。那我们的pthread_join能或者需要处理代码异常的情况嘛?根本就不需要,因为线程是进程的一个执行分支,如果进程中的某个线程崩溃了,会导致整个进程都崩溃,因此这根本就不是我们线程该管的事情,要管也是交给进程去管。
如果需要只终止某个线程而不终止整个进程,有以下三种办法:
下面我们就来介绍一下上面的三种方法
return退出
在线程函数中使用return表示当前线程退出,如果在main函数中使用return则代表进程退出,也就是说只要主线程退出了就相当于整个进程也就退出了,此时我们进程曾经申请的那些资源都会被释放,然后它缩创建的那些线程也会自动退出。
上面线程等待我们就用了return退出线程,这里就不再进行演示,可以看一下上面线程等待的代码和运行结果。
pthread_exit函数终止线程
除了上面return可以终止线程外,下面我们再来介绍一个函数——pthread_exit()
功能: 终止一个线程
函数原型如下:
void pthread_exit(void *retval);
参数:
返回值:
注意:
#include
#include
#include
using namespace std;
// new thread
void *threadRoutine(void *args)
{
// const char *name = (const char*)args;
int cnt = 5;
while (true)
{
cout << "new thread, pid: " << getpid() << endl;
sleep(1);
cnt--;
if(cnt == 0) break;
}
pthread_exit((void*)100);
}
int main()
{
// PTHREAD_CANCELED;
// 是一个很大的数字
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1"); // 不是系统调用
sleep(7);
void *retval;
pthread_join(tid, &retval);// main thread等待的时候,默认是阻塞等待的!为什么我们在这里join的时候不考虑异常呢??做不到!
cout << "main thread quit ..., ret: " << (long long int)retval << endl;
return 0;
}
运行结果:
pthread_cancel函数
功能: 取消一个正在执行中的线程
函数原型如下:
int pthread_cancel(pthread_t thread);
参数:
返回值:
下面我们来使用一下这个函数:
#include
#include
#include
#include
using namespace std;
// new thread
void *threadRoutine(void *args)
{
while (true)
{
cout << "new thread, pid: " << getpid() << endl;
sleep(1);
}
pthread_exit((void*)100);
}
int main()
{
// PTHREAD_CANCELED;
// 是一个很大的数字
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1"); // 不是系统调用
sleep(1); // 只是为了保证新线程已经启动
pthread_cancel(tid); // 不常见
void *retval;
pthread_join(tid, &retval); // main thread等待的时候,默认是阻塞等待的!为什么我们在这里join的时候不考虑异常呢??做不到!
cout << "main thread quit ..., ret: " << (long long int)retval << endl;
return 0;
}
运行结果:
可以看到线程被退出了,且收到的退出码为-1.
既然主线程可以取消新线程,那我们的新线程能不能取消我们的主线程呢?其实是可以的,但是并不建议这么做。因为主线程会出现类似于“僵尸进程”的问题。
注意:
线程之间地位都是对等的,因此我们既可以使用主线程去取消新线程,也可以使用新线程去取消主线程。我们使用主线程去取消新线程,只会导致新线程终止,其他线程还会正常执行。但是如果我们使用新线程去终止主线程,会导致主线程不再执行后续代码,并且出现类似于僵尸进程的问题。因此即使我们可以使用新线程去终止主线程,但是不建议这么做。
上面我们创建线程的时候,给线程函数传递的参数只是传递一些一般参数。其实线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!!
我们来看下面这段代码:
#include
#include
#include
#include
using namespace std;
class Request
{
public:
Request(int start,int end,string threadname)
:_start(start),_end(end),_threadname(threadname)
{}
public:
int _start;
int _end;
string _threadname;
};
class Response
{
public:
Response(int result,int exitcode)
:_result(result),_exitcode(exitcode)
{}
public:
int _result;
int _exitcode;
};
void* SumCount(void* args)
{
Request* rq = static_cast(args);
Response* rsp = new Response(0,0);
for(int i = rq->_start; i <= rq->_end; ++i)
{
cout << rq->_threadname << "is running calling... " << i <_result+=i;
usleep(100000);
}
delete rq;
return rsp;
}
int main()
{
pthread_t tid;
Request* rq = new Request(1,100,"thread 1");
pthread_create(&tid,nullptr,SumCount,rq);
void *ret;
pthread_join(tid,&ret);
Response* rsp = static_cast(ret);
cout << "rsp->result: " << rsp->_result << ", exitcode: " <_exitcode << endl;
delete(rsp);
return 0;
}
通过上面的例子我们验证了线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!! 我们还看到我们在主线程和新线程都申请了堆空间的变量,而且作为参数传递和使用。说明堆空间也是线程共享的!
我们上面讲的都是原生线程,pthread库,也叫原生线程库
其实C++11本身也支持多线程了,使用起来也比原生线程库要方便,下面我们来简单使用一下C++11的多线程:
void threadrun()
{
while(true)
{
cout << "I am a new thead for C++" << endl;
sleep(1);
}
}
int main()
{
thread t1(threadrun);
t1.join();
return 0;
}
makefile文件:
mythread:mythread.cc
g++ -o $@ $^ -g -std=c++11 -lpthread
clean:
rm -f mythread
注意要加上c++11和lpthread选项。
运行结果:
可以看到我们成功创建出新线程。我们还发现主线程的id和进程的pid是一致的,和我们原生线程库是一样的。这是因为其实C++11多线程库就是用原生线程库进行封装的。
C++11多线程vs原生线程库:
C++11多线程:
原生线程库:
总结:原生线程库具有较高的性能和灵活性,但需要处理底层细节,且跨平台兼容性较差。 而C++11线程库的跨平台性较好,如果我们需要跨平台编程,建议使用C++11线程库。
(1)前面如何创建一个线程我已经学会了,那我们应该一次如何创建多个线程呢?
下面我们来尝试创建多个线程:
#include
#include
#include
#include
using namespace std;
#define NUM 10
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* threadRountine(void* args)
{
int test_i = 0;
threadData* td = static_cast(args);
string tid = toHex(pthread_self());
int pid = getpid();
int i = 0;
while (i < 10)
{
// cout << "tid:" << tid << ",pid:" << pid << endl;
cout << "pid: " << getpid() << ", tid : " << tid
<< ", threadname: " << td->threadname < tids;
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData* td = new threadData;//这里要用new在堆上创建。如果直接定义,这里是在主线程的栈上创建,而且是在for循环,循环结束其它线程就没办法访问了
InitThreadData(td,i);
pthread_create(&tid,nullptr,threadRountine,td);
tids.push_back(tid);
// sleep(1);
}
sleep(1); // 确保复制成功
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i],nullptr);
}
return 0;
}
运行结果:
可以看到我们成功创建出来十个线程。
(2)前面我们说过每一个线程都有自己的栈结构,保护各个线程运行时所形成的临时数据,独立的栈结构保证线程调度的过程中不会出现线程错乱的问题
下面我们利用多线程来进行验证:
#include
#include
#include
#include
using namespace std;
#define NUM 3
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* threadRountine(void* args)
{
int test_i = 0;
threadData* td = static_cast(args);
string tid = toHex(pthread_self());
int pid = getpid();
int i = 0;
while (i < 10)
{
cout << "pid: " << getpid() << ", tid : " << tid
<< ", threadname: " << td->threadname
<< " test_i: " << test_i << " &test_i: " << &test_i << endl;
sleep(1);
i++; test_i++;
}
delete td;
return nullptr;
}
int main()
{
//创建多线程
vector tids;
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData* td = new threadData;//这里要用new在堆上创建。如果直接定义,这里是在主线程的栈上创建,而且是在for循环,循环结束其它线程就没办法访问了
InitThreadData(td,i);
pthread_create(&tid,nullptr,threadRountine,td);
tids.push_back(tid);
// sleep(1);
}
sleep(1); // 确保复制成功
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i],nullptr);
}
return 0;
}
运行结果:
我们看到虽然每个线程调用的是同一个函数,且test_i的值都是依次从0开始增长。但是我们看到每个线程的test_i的地址却是不一样的。
这是因为每个线程都有自己独立的栈结构,各个线程调用这个函数时都要在自己独立的栈结构开辟栈帧,各自在自己的栈上创建了一个test_i。
其实线程和线程之间,几乎没有秘密,线程的栈上的数据,也是可以被其他线程看到并访问的。
下面我们来进行验证:
#include
#include
#include
#include
using namespace std;
#define NUM 3
int *p = NULL;
int g_val = 100;
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* threadRountine(void* args)
{
int test_i = 0;
threadData* td = static_cast(args);
if(td->threadname == "thread-2") p = &test_i;
string tid = toHex(pthread_self());
int pid = getpid();
int i = 0;
while (i < 10)
{
cout << "pid: " << getpid() << ", tid : " << tid
<< ", threadname: " << td->threadname
<< " test_i: " << test_i << " &test_i: " << &test_i << endl;
sleep(1);
i++; test_i++;
}
delete td;
return nullptr;
}
int main()
{
//创建多线程
vector tids;
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData* td = new threadData;//这里要用new在堆上创建。如果直接定义,这里是在主线程的栈上创建,而且是在for循环,循环结束其它线程就没办法访问了
InitThreadData(td,i);
pthread_create(&tid,nullptr,threadRountine,td);
tids.push_back(tid);
}
sleep(1); // 确保复制成功
cout << "main thread get a thread local value, val: " << *p << ", &val: " << p << endl;
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i],nullptr);
}
return 0;
}
运行结果:
(3)我们前面验证过全局变量是可以被所有线程同时看到并访问的,那如果我们想要私有一个全局变量呢?
我们只需要在全局变量的前面加上__thread就可以完成对全局变量进行线程的局部存储了:
#include
#include
#include
#include
using namespace std;
#define NUM 3
int *p = NULL;
__thread int g_val = 100;
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* threadRountine(void* args)
{
int test_i = 0;
threadData* td = static_cast(args);
string tid = toHex(pthread_self());
int pid = getpid();
int i = 0;
while (i < 10)
{
cout << "pid: " << getpid() << ", tid : " << tid
<< ", threadname: " << td->threadname
<< ", g_val: " << g_val << " ,&g_val: " << &g_val < tids;
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData* td = new threadData;//这里要用new在堆上创建。如果直接定义,这里是在主线程的栈上创建,而且是在for循环,循环结束其它线程就没办法访问了
InitThreadData(td,i);
pthread_create(&tid,nullptr,threadRountine,td);
tids.push_back(tid);
}
sleep(1); // 确保复制成功
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i],nullptr);
}
return 0;
}
运行结果:
可以看到在全局变量加上__thread之后每个线程的g_val地址都不一样了。
这样定义有什么作用呢?
减少系统调用和实现线程的局部存储。如果我们要用一个变量保存进程的pid或者线程的id,只需要用__thread在全局定义一个变量,然后再调用一次系统调用进行保存。后面我们就只需要调用这个变量就可以了,不需要再调用系统调用了
有人会问线程不是有独立的栈结构吗?那我们在线程函数直接定义局部变量不是一样的吗?
其实这种方法也可以,但是如果我们还要在线程函数里面再调用其他函数,还需要使用到这些变量,就需要将这些变量传进去,就会非常麻烦。
注意:__thread这个方法只能定义内置类型,不用用来修饰类等自定义类型。
所以我们如果不想等待该线程并且也不想造成内存泄漏,我们可以采用线程分离,分离之后的线程不需要被join,运行完毕之后,会自动释放该线程的资源。
下面来为大家介绍一个函数——pthread_detach()
int pthread_detach(pthread_t thread);
参数:
返回值:
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
我们下面首先来使用线程组内其他线程对目标线程进行分离:
#include
#include
#include
#include
#include
using namespace std;
#define NUM 3
int *p = NULL;
__thread int g_val = 100;
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* threadRountine(void* args)
{
int test_i = 0;
threadData* td = static_cast(args);
string tid = toHex(pthread_self());
int pid = getpid();
int i = 0;
while (i < 10)
{
cout << "pid: " << getpid() << ", tid : " << tid
<< ", threadname: " << td->threadname
<< ", g_val: " << g_val << " ,&g_val: " << &g_val < tids;
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData* td = new threadData;//这里要用new在堆上创建。如果直接定义,这里是在主线程的栈上创建,而且是在for循环,循环结束其它线程就没办法访问了
InitThreadData(td,i);
pthread_create(&tid,nullptr,threadRountine,td);
tids.push_back(tid);
}
sleep(1); // 确保复制成功
for(auto i : tids)
{
pthread_detach(i);
}
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i],nullptr);
printf("n = %d, who = 0x%x, why: %s\n", n, tids[i], strerror(n));
}
return 0;
}
运行结果:
我们可以看到这次三个线程只执行了一次。我们还可以看到join的返回值是22,不是0,此时证明我们的join是失败的。这是为什么?
因为joinable和分离是冲突的,一个线程不能既是joinable又是分离的。所以我们三个线程就只执行了一次。
我们也可以使用线程自己分离:
运行结果:
下面我们来使用一下pthread_ self函数:
#include
#include
#include
#include
#include
using namespace std;
string toHex(pthread_t tid)
{
char Hex[64];
snprintf(Hex,sizeof(Hex),"%p",tid);
return Hex;
}
void* threadRountine(void* args)
{
while (true)
{
cout << "thread id:" << toHex(pthread_self()) << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRountine,(void*)"thread 1");
cout << "main thread id : " << toHex(pthread_self()) << endl;
cout << "main thread create thead done, new thread id : " << toHex(tid) << endl;
pthread_join(tid,nullptr);
return 0;
}
我们查看到的线程id是pthread库的线程id,不是Linux内核中的LWP,pthread库的线程id是一个内存地址。
我们通过ldd命令可以看到,我们采用的线程库实际上是一个动态库:
我们知道要想创建线程,首先你得要有一个进程,创建进程就需要创建一堆数据结构,进程创建好了之后我们还需要使用pthread动态库。而pthread动态库在磁盘上面是一个文件,那既然是文件如果我们想使用它,我们就需要把它加载到内存中才行。
进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时我们进程内部的所有线程就都可以看到这个动态库。
线程库注定了要维护多个线程属性集合。每个线程都要有运行时的临时数据,这也就意味着每个线程都要有自己的私有栈结构。那我们创建了这么多的用户级线程,线程库如何管理这些线程呢?先描述再组织。
因此我们还需要有描述线程的用户级控制块,这个控制块叫做struct pthread,其中包含了对应线程的各种属性,每个线程还有自己的线程局部存储,当中包含了线程被切换时的上下文数据。
那这个用户级控制块具体是怎么样的呢,我们如何快速找到一个用户级线程呢?
每个线程在共享区都有这样的一块区域对其进行描述,因此我们要找到一个用户级线程我们只需要找到该线程的用户级控制块的起始地址,就可以获取到该线程的各种信息了。
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
主线程使用的栈是进程地址空间中原生的栈。新线程采用的栈是在共享区中开辟的,具体来说是在pthread库中的,tid指向的用户tcb中!
我们上面使用的各种线程函数,本质都是在线程内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理是在共享区的。