线程与进程
Linux下程序的执行被称为进程,而在一个进程内的一条执行路线就被称为线程,线程是一个进程内部的控制序列。每个进程最少有一个线程,这个线程被称为主线程。线程在进程内部运行,本质也是在进程地址空间内运行。
进程是系统资源分配的基本单位,而线程是调度的基本单位,虽然线程共享进程数据,但是各个线程也有自己独有的一部分数据例如:上下文数据,栈,线程ID,寄存器等。
进程和线程的关系如下图所示:
线程的优点
线程作为一个执行流,理论上来说他也需要进程控制块类似的东西来进行控制线程的执行。但是Linux下的线程与Windows的不同,Linux下的线程操作系统并没有额外创建数据结构来设置线程的控制块。线程作为进程中的一个执行分支,操作系统没有给他创建控制模块,那操作系统是怎么控制线程的呢?实际上Linux借用了进程的进程控制块,每个线程都是该进程程序控制块的一部分,因此Linux下的线程也被称为轻量级进程,线程可以和进程访问同样的程序地址空间,但是也有独立的结构。因此Linux下的线程有如下的优点:
- 创建一个新线程的代价比创建一个新进程的代价小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作少得多
- 线程占用的资源比进程少
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,可以将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。
线程的缺点
除了以上线程的优点以外,与进程相比线程也存在一定的缺点,例如:
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
线程异常
线程异常指的是单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃 。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。如下代码可以看到,这里的子线程中出现野指针问题,线程退出的时候进程也直接退出。
void* thread_run(void* args)
{
char* p = NULL;
char a = 'c';
*p = a;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x\n",pthread_self());//打印主线程id
sleep(1);
}
return 0;
}
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
线程控制
由于Linux操作系统并没有特别给线程创建对应的接口操作函数,因此想要对线程进行各种操作,需要有经验的工程师对线程进程操作。因此也诞生了pthread库,其中有各个接口函数可以对线程进行各项操作,但是由于该库并不是系统库,因此在使用时,在编译时需要加上**-lpthread**来指定使用的库。
创建线程
使用pthread_create接口函数可以创建一个新的线程,成功返回1,失败则返回错误码下面介绍各个参数的意义:
第一个参数是一个pthread_t类型的参数,这个参数可以认为是一个无符号的长整型类型,他是一个输出型参数,返回所创建的线程的id。
第二个参数是输入线程属性,但是操作系统会处理,设置为NULL即可。
第三个参数是函数指针,传入线程所要执行的函数指针
第四个参数传递给回调函数的参数,传给第三个函数指针
测试代码如下:
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x\n",pthread_self());//打印主线程id
sleep(1);
}
return 0;
}
获取线程id
使用pthread_self可以获取自己的线程id,具体代码如下:
这里可以看到新的线程有自己的线程id和主线程区别开。
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
}
return 0;
}
线程等待
一般而言,线程与进程一样也是需要被等待的,如果不等待可能会出现类似于僵尸进程的问题。
线程等待也有相应的函数:pthread_join。这个函数等待成功则返回0,失败则返回错误码。该函数传入的两个参数,第一个参数为需要等待的线程的线程id,第二个参数则是一个输出型参数,用来获取新线程退出时候的函数的返回值。测试代码如下:
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
break;
}
return (void*)111;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为:0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
break;
}
void* status = NULL;
pthread_join(tid,&status);
printf("退出返回值为:%d\n",(int)status);
return 0;
}
这里的线程等待函数不需要处理代码异常情况,只需要关心代码跑完结果对或者不对,线程出现异常是进程的问题。
线程终止
return函数在main函数中代表主线程以及进程退出,对于其他线程函数中使用return,则只代表当前线程退出。如果使用exit等函数则会直接退出整个进程
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
exit(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为:0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
}
return 0;
}
因此在线程中也有自己的线程退出函数
pthread_exit
在当前线程函数中使用pthread_exit函数会退出当前线程,并不会影响其他线程,其中传入的参数为退出的退出码
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
break;
}
pthread_exit((void*)111);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为:0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
break;
}
void* status = NULL;
pthread_join(tid,&status);
printf("退出返回值为:%d\n",(int)status);
return 0;
}
pthread_cancel
pthread_cancel该函数的功能是取消线程,退出结果为-1,可以在新创建的线程中取消主线程但是可能会导致该线程僵尸化。这里传入的参数是线程id。测试代码如下:
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
pthread_cancel(pthread_self());
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为:0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
break;
}
void* status = NULL;
pthread_join(tid,&status);
printf("退出返回值为:%d\n",(int)status);
return 0;
}
线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
这里传入的参数是线程id,可以传入自己的id将自己与其他线程分离,也可以将其他线程对目标线程进行分离。分离和等待是矛盾的,分离后的进程就不能被等待了。测试代码如下:
void* thread_run(void* args)
{
pthread_detach(pthread_self());
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印主线程id
sleep(2);
}
pthread_exit((void*)111);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为:0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
break;
}
void* status = NULL;
int ret = pthread_join(tid,&status);
printf("退出返回值为:%d,等待函数返回值为%d\n",(int)status,ret);
return 0;
}
线程id
每个进程有唯一的进程标识符PID,但是一个进程有多个线程,他们在Linux下的有着相同的PID,但是LWP(Light Weight Process)标识符却不一样,这也用来区分不同线程的标识符。但是这里的LWP和在程序中获得的pthread_t 获得的id值却是不一样的
void* thread_run(void* args)
{
while(1)
{
printf("new thread id :0x%x\n",pthread_self());//打印线程id
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("main thread id :0x%x 创建的线程id为:0x%x\n",pthread_self(),tid);//打印主线程id
sleep(1);
}
return 0;
}
那么这两个值有什么区别呢?使用ps -aL所查看的LWP值是Linux内核用来区别不同线程的标识符,而pthread_self()所获取的pthread_t类型的线程id是pthread库用来标识不同线程的id,它实际上是一个内存地址(虚拟地址).
pthread库中包含了用户层的描述线程的属性和数据结构,其中需要包含LWP将两者联系起来
PCB->LWP是内核级的,是一一对应的。