目录
1、线程与进程的关系
2、线程的优缺点
3、创建线程
4、查看启动的线程
5、验证线程是共享地址空间的
6、pthread_create的重要形参
6.1 线程id
6.2 线程实参
7、线程等待
8、线程退出
9、线程取消
10、线程tcb
10.1 线程栈
11、创建多线程
12、__thread
13、线程分离
结语
前言:
线程是操作系统进行调度的基本单位,他属于进程的子集。在Linux下,通过实现轻量化进程来实现线程,因此线程具有进程的相关特性,比如线程必须有自己的代码资源,有属于自己独立的数据空间,并且同一个进程下的线程所看到的地址空间是属于该进程的,因为创建线程实际上就是在该进程下创建task_struct结构体(该结构体的作用是方便操作系统对该执行流的调度),这些task_struct结构体跟进程共用空间资源,只不过线程可以在单一进程执行流的基础上实现多执行流并发式的运行代码,以至于提高cpu的效率。
1、线程与进程的关系
说到线程就离不开进程的概念,因为线程是在进程的基础上实现的,多线程是底层就是创建了多个task_struct结构体作为进程的执行分支,但是他们依然是共用进程的数据资源,线程示意图如下:
当系统里创建一个进程,则系统需要给该进程分配新的地址空间、页表、物理内存等等空间资源,所以说进程是系统分配资源的实体。但是当系统里有了新的线程则不会给线程分配新的空间资源,而是给让线程使用进程的空间资源。
线程的独立部分:
线程虽然和进程共用地址空间,但是线程也有自己独立的部分,比如:线程ID, 保存上下文的寄存器,线程栈,errno,block信号集,调度优先级。
2、线程的优缺点
线程的优点:
1、当我们需要并发执行代码时,创建一个线程的工作比创建一个进程的工作要小得多。
2、当cpu切换PCB时,切换线程的效率比切换进程的效率略高。
3、线程所占用的资源小于进程。
4、线程之间的通信代价比进程的要小。
5、提高程序的并发性。
线程的缺点:
1、若单个线程收到信号退出,则会导致整个进程都退出。
2、多线程访问共同资源时是不受保护的,会导致意料之外的错误。
3、编写多线程的难度很高。
4、若单个线程因为异常崩溃,则会导致整个进程崩溃。
3、创建线程
创建线程需要用到的接口如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//thread是一个执行类型为pthread_t的变量的指针
//attr是一个指针,他指向的结构体包含新线程的各种属性,设为nullptr则表示采用默认属性
//start_routine是新线程要执行的函数,他接收一个void*,返回值一个void*
//arg表示新线程要执行的函数的实参
创建线程前需要先定义一个类型为pthread_t的变量作为实参传递给函数pthread_create,该变量的作用是让用户可以通过他找到对应的线程,创建线程的代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *threadRun(void* args)
{
while(1)
{
cout << "子线程: " << getpid() << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;//先定义一个pthread_t类型的变量
//该函数调用完成后会赋予tid新的值,表示该线程的id
pthread_create(&tid, nullptr, threadRun, nullptr);
while(1)
{
cout << "主线程: " << getpid() << endl;
sleep(1);
}
}
测试结果:
注意:使用线程的接口时要在编译的时候要手动链接pthread库,如下图:
4、查看启动的线程
在Linux下,使用指令:ps -aL,就可以查看用户启动的线程了。如下图:
LWP表示轻量级进程的pid,即线程的pid,LWP是给系统调度线程专门设置的标识符。
5、验证线程是共享地址空间的
定义一个全局变量,若所有线程只能看到唯一一份全局变量,那么就可以证明进程下的所有线程用的是同一个地址空间,代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 10;
// 新线程
void *threadRoutine(void *args)
{
while (true)
{
printf("子线程 pid: %d, g_val: %d, \
&g_val: 0x%p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
while (true)
{
printf("主线程 pid: %d, g_val: %d,\
&g_val: 0x%p,\n", getpid(), g_val, &g_val);
sleep(1);
g_val++;
}
return 0;
}
运行结果:
从结果可以看到,哪怕主线程对全局变量进行更改,其他线程拿到的值是更改后的值,若是父子进程关系,则另一方会发生写时拷贝,线程之间没有这么做,说明线程是共享地址空间的。
6、pthread_create的重要形参
6.1 线程id
线程有两个标识符,一个是LWP,是给系统看的,另一个是线程id,是给用户看的。线程id就是创建线程时定义的pthread_t类型的变量,该变量作为pthread_create的输出型参数,在调用完pthread_create后该变量保存的就是线程id了。
查看线程id的代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 10;
// 新线程
void *threadRoutine(void *args)
{
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
while (true)
{
printf("主线程 pid: %d, g_val: %d,\
&g_val: 0x%p,子线程id:%p \n", getpid(), g_val, &g_val,tid);
sleep(1);
g_val++;
}
return 0;
}
运行结果:
从测试结果发现,线程id实际上就是一串地址,这个地址就是线程在地址空间内的映射,间接说明了线程的管理是在用户空间内的进行,并不是由操作系统像管理进程PCB一般在内核空间进行,具体看下文线程tcb。
6.2 线程实参
线程的任务就是执行pthread_create的函数指针,并且该函数具有一个void*的形参,那么如何使用该void*的形参呢?
测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 新线程
void *threadRoutine(void *args)
{
char *name = static_cast<char *>(args);//需要强转
while (true)
{
cout << name << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"线程1");//需要强转
while (true)
{
}
return 0;
}
运行结果:
7、线程等待
子线程退出时主线程也要对其进行等待,等待的原因和父子进程一样,防止内存泄漏和回收退出信息,若不等待线程,则线程的tast_struct会一直存在,会造成不必要的资源浪费,进行线程等待的接口介绍如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//thread表示要等待的线程id
//retval是一个二级指针,是一个输出型参数,目的是拿到线程返回的void*
线程等待的测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 新线程
void *threadRoutine(void *args)
{
int count = 5;
while (count)
{
count--;
sleep(1);
}
return (void*)1;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
void* retval;
pthread_join(tid,&retval);//等待子线程
cout<<"线程等待成功:"<<(long long)retval<<endl;
return 0;
}
运行结果:
由于线程执行的函数虽然可以返回一个void*的变量,但是我们却没有办法接收该返回值,因为这不是一个简单的函数调用。所以只能通过调用pthread_join,然后传递一个二级指针给他,pthread_join就可以通过输出型参数把void*变量给带出来。
8、线程退出
进程退出常常用exit函数,只要一个进程调用了exit函数,则该进程就直接结束了。但是若想仅仅退出一个线程,则不能用exit,因为当一个线程用exit退出,就会把整个进程退出。线程退出有专门的退出函数,该函数介绍如下:
#include <pthread.h>
void pthread_exit(void *retval);
//该函数会退出当前线程,并且返回一个void*变量
线程退出测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 新线程
void *threadRoutine(void *args)
{
int count = 5;
while (count)
{
count--;
sleep(1);
}
//return (void*)1;
pthread_exit((void*)100);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
void* retval;
pthread_join(tid,&retval);
cout<<"线程等待成功:"<<(long long)retval<<endl;
return 0;
}
运行结果:
9、线程取消
可以调用函数pthread_cancel可以在线程退出前取消该线程,该函数介绍如下:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//thread表示要取消的线程
线程取消测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 新线程
void *threadRoutine(void *args)
{
int count = 5;
while (count)
{
count--;
sleep(1);
}
pthread_exit((void*)100);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);
pthread_cancel(tid);
void* retval;
pthread_join(tid,&retval);
cout<<"线程等待成功:"<<(long long)retval<<endl;
return 0;
}
运行结果:
如果一个线程被取消,则该线程的退出码为-1。
10、线程tcb
上文讲到线程id是给用户看的,通过上述代码打印线程id,可以观察到线程id实际上就是一串虚拟地址,线程id的值如下:
分析id的地址,可以发现该地址数属于进程地址空间的共享区内,说明线程本身就是存储在用户空间内的,但是我们自己并没有对线程做任何管理,那么线程是如何被管理的呢?可以从函数pthread_create得知,当我们调用pthread_create后,该函数就会返回一个线程id给到我们,说明该函数内部会自己维护线程,而该函数的实现是存储在线程库(pthread.so)里的,而线程库会在程序运行起来时加载到内存并映射在共享区内,所有可以得出一个结论:线程由线程库维护,并映射在地址空间的共享区内。
具体示意图如下:
从上图可以发现,tcb就是管理线程的结构体,线程库以维护tcb从而维护线程,而tid就是线程id,他就是tcb的首地址,这也就很好的解释了为什么可以通过线程id找到对应的线程了,这个地址是线程库为用户申请,也就是用户调用函数pthread_create后所得到的地址。并且不同的进程创建的线程都会被线程库在内存中统一管理,只是这些线程会分别映射到他们的进程共享区中。
10.1 线程栈
从上图可以发现除了地址空间的栈空间外,线程tcb中也维护一个名为线程栈的空间,而线程栈是采用数组的方式模拟出来的,这些模拟栈被保存在共享区,由线程库来维护。栈与栈之间相互独立,不可直接访问,但是同一进程下的其他线程采用特别的方式也可以访问到对方的栈,因为毕竟都在同一个地址空间内。
11、创建多线程
创建多线程的思路:利用循环定义多个线程id,但是由于新的循环会覆盖旧的线程id,因此需要把每个线程id存入容器中,方便后续等待线程时能够找到他们的线程id,创建多线程代码如下:
#include <iostream>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 4
struct threadData//给每个线程做标记
{
string threadname;
};
// 所有的线程,执行的都是这个函数?
void *threadRoutine(void *args)
{
threadData *td = static_cast<threadData *>(args);
int i = 5;
while (i)
{
cout << "我是" << td->threadname << ", pid: " << getpid() << endl;
sleep(1);
i--;
}
delete td;
return nullptr;
}
void InitThreadData(threadData *td, int number)
{
td->threadname = "线程-" + to_string(number);
}
int main()
{
// 创建多线程!
vector<pthread_t> tids;
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData *td = new threadData;
InitThreadData(td, i);//标记线程
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
}
//线程等待
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
运行结果:
12、__thread
__thread只能修饰内置类型,不能修饰自定义类型,他修饰的变量对于所有线程是可见的,有点类似全局变量,但是他跟全局变量的区别在于:__thread修饰的变量对于每个线程而言是独立的,换句话说,线程对该变量的修改不会影响其他线程所看到的值。
测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
__thread int a = 10;
void *threadRun(void* args)
{
while(1)
{
cout << "子线程: " << getpid() <<" a的值:"<<a++<<endl;//线程对a进行++
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;//先定义一个pthread_t类型的变量
//该函数调用完成后会赋予tid新的值,表示该线程的id
pthread_create(&tid, nullptr, threadRun, nullptr);
while(1)
{
cout << "主线程: " << getpid() <<" a的值:"<<a<< endl;//此处a的值还是10吗?
sleep(1);
}
}
运行结果:
从结果可以看到,主线程的a是独立于子线程的a,原因就是a作为全局变量被__thread修饰了,因此所有线程都有一份独立的a。
13、线程分离
主线程创建的子线程退出后,主线程若想拿到子进程退出的void*返回值,则主线程要对其进行join等待操作,但是若主线程不关心其返回值,则就没必要进行等待,因为等待也是一种负担。因此在这种情况下,可以让该子线程自行分离,即分离的子线程在退出后会自动释放空间资源。
分离的函数介绍如下:
#include <pthread.h>
int pthread_detach(pthread_t thread);
//thread表示要分离的线程
线程分离测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void *threadRun(void* args)
{
pthread_detach(pthread_self());//pthread_self返回该线程的id
int count = 3;
while(count--)
{
cout << "子线程: " << getpid() <<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, nullptr);
sleep(1);//要保证pthread_detach在pthread_join之前触发
int n = pthread_join(tid, nullptr);
printf("n = %d, who = 0x%x, why: %s\n", n, tid, strerror(n));
}
测试结果:
n = 22表示等待失败了,说明这些线程已经被分离了。
结语
以上就是关于使用线程的讲解,线程是核心思想是创建多个执行流让程序实现并行运行,目的就是提高程序执行的效率,本文主要讲述如何创建线程和使用线程,包括线程的基本用法和概念,线程本身涉及的知识非常广,细节也特别多,在复杂的多线程下往往要考虑更多的东西。
最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!