这些天家里格外的热,同时自己还偷懒了几天。博客一直没来得及更新,一想到为了冲刺实习生,自己还是不能堕落。希望和老铁们一起过一个充实的暑假~今天我们来学习Linux多线程~它和进程有什么区别呢?
目录
线程概念
Windows下线程的概念
一般教材线程的概念:线程是进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化。
但是,今天我们讲的线程和上面的不一样,Linux下设计的线程是复用了进程PCB,具体是怎么设计的呢?我们今天来重点学习一下:
Linux下的线程
实际上,Linux下的并没有真正意义上的线程,而是对进程PCB的复用。每创建一个线程时,就在当前进程内创建一个PCB,和主线程指向同一个地址空间。但是在CPU看来,它只关心该进程的PCB,对于进程内部的"线程",CPU就不去看,线程是被当前进程的主线程进行管理。
总结
CPU此时看到的PCB <= 之前讲的PCB的概念
一个PCB就是一个需要被调度的执行流!
Linux中没有专门为线程设计TCB,而是用进程的PCB来模拟线程的,这样做有什么好处?
不用维护复杂的进程和线程之间的关系,不用单独为线程设计任何算法,直接使用进程的一套相关的方法。OS只需要聚焦在进程的资源分配上就可以了。
Linux 线程vs进程
区别一
所有的轻量级进程(可能是"线程")都是在进程内部执行(地址空间:表示进程所能看到的大部分资源)
进程:具有独立性,大部分资源是私有的,可以有部分资源共享(管道、ipc资源)
线程:大部分资源是共享的,可以有部分资源是私有的(栈,上下文)
区别二
Linux线程控制
Linux下的线程是用进程模拟的,所以Linux下不会给我们提供直接操作线程的接口,而是给我们提供的,在同一个地址空间内创建PCB的方法,分配资源给指定的接口,这样做对用户使用很不友好,所以就有人实现第三方库来供我们使用!
线程创建
pthread_create
pthread_t* thread:输出型参数,返回创建线程的id
const pthread_attr_t* arr:线程属性,我们在这里设置成NULL就可以
void *(*start_routine) (void *):创建的线程执行方法,函数指针
void* arg:给回调函数传的参数
pthread_self
返回当前线程的id(注意:和下面的LWP不一样!)
代码测试:
void* thread_run(void* args)
{
const char* id = (const char*)args;
while(1)
{
printf("我是%s线程: %d\n", id, getpid());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"thread 1");
while(1)
{
printf("我是main主线程: %d\n", getpid());
sleep(1);
}
return 0;
}
运行结果:
查看线程方法
[cyq@VM-0-7-centos 线程控制]$ ps -aL
这时候我们就发现了两个线程,它们的PID都是一样的,说明他们属于同一个进程!LWP是线程ID。
线程等待
为什么要有线程等待?
一般而言,线程也是需要被等待的,如果不等待,可能导致类似于"僵尸进程"问题。
pthread_join
pthread_t thread:要等待线程的id
void** retval:输出型参数,用来获取新线程退出的时候,线程函数的返回值。
返回值:等待成功返回0,等待失败,返回错误码。
举个栗子:
void* thread_run(void* args)
{
int id = *(int*)args;
while(1)
{
printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
sleep(5);
break;
}
return (void*)123;
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM; i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
void* status = NULL;
for(i = 0; i < NUM; i++)
{
pthread_join(tid[i], &status); //阻塞式等待
}
printf("ret: %d\n", (int)status);
sleep(3);
return 0;
}
运行结果:
这时候我们发现等待可以获取创建线程的返回结果。
注意
pthread_join:是一种阻塞式等待,必须是阻塞式等待,直到新线程退出才能拿到对应的返回值。
printf("ret: %d\n", (int)status):(int)status,不能写成*(int*)status,linux这里是64位机,8个字节,这样写会越界,而我们只需要一个整数而已。
pthread_join:需要处理代码异常的问题吗??不需要!
线程终止
线程终止有三种方法:
我们只介绍方法2和方法3:
pthread_exit
void* retval:退出参数 ,给pthread_join的输出参数一个返回值。echo $?不受retval影响。
代码测试:
void* thread_run(void* args)
{
int id = *(int*)args;
while(1)
{
printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
sleep(5);
break;
}
pthread_exit((void*)123);
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM; i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
void* status = NULL;
for(i = 0; i < NUM; i++)
{
pthread_join(tid[i], &status); //阻塞式等待
}
printf("ret: %d\n", (int)status);
sleep(3);
return 0;
}
运行结果:
我们发现通过进程等待可以拿到pthread_exit的退出结果。
pthread_exit和exit
依照上面的代码,将pthread_exit改为exit:
测试结果:
我们发现退出结果没有打印,也就是说,在新线程中遇到exit后,整个进程(所有线程)全部退出。所以我们以后要想终止某一个线程就使用pthread_exit,而不是exit。
终止线程的第三个方法:
pthread_cancel
pthread_t thread:要分离线程的id
返回值:成功就返回0,失败1返回退出码。
函数调用成功,退出码为-1。
这个-1是什么呢?
[cyq@VM-0-7-centos 线程控制]$ grep -ER "PTHREAD_CANCELED" /usr/include/pthread.h
实际上就是pthread_exit的退出结果,-1就是宏:PTHREAD_CANCELED。
举个栗子:
void* thread_run(void* args)
{
int id = *(int*)args;
while(1)
{
printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
sleep(1);
}
}
#define NUM 5
int main()
{
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM; i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread...\n");
int val = pthread_cancel(tid[0]);
printf("cancel thread...\n");
void* status = NULL;
pthread_join(tid[0], &status); //阻塞式等待
printf("val: %d\n", (int)status);
sleep(3);
return 0;
}
我们用一个命令脚本来看一下结果:
[cyq@VM-0-7-centos test]$ while :; do ps -aL | head -1 && ps -aL | grep mytest; sleep 1; echo "###########################"; done
我们让一个线程被pthread_cancel后,通过监视,发现该线程确实退出了。同时join到取消线程的退出结果是-1,说明取消成功了。
注意
pthread_cancel主线程可以取消子线程,但是子线程不能取消主线程;主线程也不能取消自己,否则会造成(该进程会成为僵尸进程)内存泄漏的问题。
线程分离
主线程要等待创建的新线程,否则会造成内存泄漏的问题,如果不想等待呢?有没有办法?
pthread_detach
pthread_thread:要分离线程的id
注意:
举个栗子:
void *thread_run(void *args)
{
int id = *(int *)args;
int cnt = 3;
while (cnt--)
{
printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"new thread");
sleep(3);
pthread_detach(tid);
int ret = pthread_join(tid, NULL); //阻塞式等待
printf("ret: %d\n", ret);
sleep(3);
return 0;
}
运行结果:
我们发现pthread_join的等待结果不是0,说明等待线程失败了。同时最后线程都退出时我们发现没有<default>这样的标志,说明没有僵尸进程,所以不存在内存泄漏。
线程id和LWP
线程id代码打印:
void* thread_run(void* args)
{
int num = *(int*)args;
while(1)
{
printf("我是[%d]线程, 我的线程ID是: %lu\n", num, pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid[5];
printf("我是主线程, 线程id: %lu\n", pthread_self());
for(int i = 0; i < 5; i++)
{
sleep(1);
pthread_create(tid+i, NULL, thread_run, (void*)&i);
}
sleep(3);
return 0;
}
注意:pthread_self要对应%lu打印。
打印结果:
我们发现同样是用来标识线程id的,但为什么两者差别这么大呢?
我们用%x打印线程id:
我们查看到的线程id是pthread库中的线程id,不是linux内核中的LWP,pthread库的线程id是一个内存地址(虚拟地址)! !
如何理解线程tid?
实际上tid就是对应地址空间上共享区里的地址。因为用地址来做id,是可以最快找到映射的对应线程库的代码和数据。
用户级线程和内核轻量级线程的关系
大部分的操作系统都是这样设计的,不排除特别情况。
看到这里,支持博主一下吧~