本片博客会粘贴部分代码,想要了解更多代码信息,可访问小编的GitHub关于本篇的代码
- 对比进程与线程的区别
线程概念及特点
- Linux下线程是以进程模拟的,Linux下的进程控制块pcb实际就是一个线程,其他系统不一定
- Linux对进程和线程不作区分,Linux下的线程以进程PCB模拟,也就是说task_struct其实就是线程,CPU调度的基本单位是线程,进程就是线程组。
- Linux下的pcb其实就是线程的描述,Linux下的线程以进程pcb模拟,因此也叫轻量级进程。
- 一个进程中至少有一个线程,线程是进程内的一条执行流。
线程共享进程数据,但也拥有自己的部分数据:
线程ID
组寄存器(上下文数据)
栈
errno
信号屏蔽字
调度优先级
如果定义1个函数,在各线程中都可以调用,如果定义1个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者?定义的信号处理函数)
当前工作目录
用户id和组id
Text Segment、Data Segment都是共享的。
进程和线程的比较,线程优缺点
线程的优点:
一个进程中有可能会有多个线程,而这些线程共享同一个虚拟地址空间,因此有时候也会说线程是在进程中的
a、 因此他们共享了整个代码段、数据段,线程间通信变得极为方便
b、创建或销毁一个线程相比较于进程来说成本更低(不需要额外创建虚拟地址空间)
c、线程的调度切换相较于进程也较低
d、线程占用的资源比进程少很多
e、能够充分利用多处理器的可并行数量
f、在等待IO操作、CPU计算等任务时候,线程支持多处理器并行处理,任务分摊,提高效率。
线程的缺点:一个进程中有可能会有多个线程,而这些线程共享同一个虚拟地址空间
a、 因为线程间的数据访问变得简单,因此数据安全访问问题更加突出,要考虑的问题增多,编码难度增多。资源争抢问题更加突出。
b、 一些系统调用和异常都是针对整个进程的,因此一个线程中出现了异常,那么,整个进程都会受到影响,以及一些系统调用的使用也是需要注意的。
进程的优点:安全、稳定(因为进程的独立性)
- 线程相关代码,总结线程属性
线程的控制
进程是操作系统资源分配的一个基本单位(通过页表)
线程是CPU调度的一个基本单位(CPU调度的是pcb)
线程创建:线程共享进程地址空间,但是每一个线程都有自己相对独立的一个地址空间
操作系统并没有提供系统调用来创建线程,所以就在posix标准库中,实现了一套线程控制(创建、终止、等待…)的接口,因为这套接口创建的线程是库函数,是用户态的线程创建,因此创建的线程也叫做用户线程。 一个用户线程对应了一个轻量级进程来进行调度运行的。(对操作系统来说,创建了一个轻量级进程)
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
pthread_t * thread:创建的线程id
const pthread_attr_t:线程属性,一般设置为NULL
void *(*start_routine) (void *):线程的执行函数
void *arg:给线程的执行函数传的参数
pthread_create接口创建了一个用户线程,并且通过第一个参数返回了一个用户的id,这个id数字非常大,其实它就是一个地址,是指向自己的线程地址空间在整个进程虚拟地址空间中的位置。
每一个线程都需要有自己的栈区,否则如果所有线程共用一个栈的话,会引起调用栈混乱,并且因为CPU是以pcb来调度的,因此CPU调度的基本单位,所以每一个线程也都应该有自己的上下文数据来保存CPU调度切换时的数据。(而这些都是在线程地址空间中,每一个线程都有自己的线程地址空间,它们相对来说独立,但是线程地址空间是在虚拟地址空间内的)
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
void* start(void *arg){
int num = (int)arg;
while(1){
printf("The pthread%d\n",num);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,start,(void*)999);
//pthread_t pthread_self(void);
// 获取调用线程的线程id(用户态线程id)tid是pthread_t类型即无符号长整型lu
printf("Main pthread ID:%lu\n",pthread_self());
printf("The pthread!!ID:%lu\n",tid);
while(1){
printf("This is main pthread!!\n");
sleep(1);
}
return 0;
}
Linux下线程操作函数都是库函数,需要链接动态库-pthread
thread: 用于接受一个用户线程ID
attr: 用户设置线程属性,一般置空
start_routine:线程的入口函数,线程运行的就是这个函数,这个函数退出了,线程也就退出了
arg:用于给线程入口函数传递参数
返回值:成功:0 失败:errno
其实在每一个task_struct里边都有pid、tgid,其中pid线程标识,tgid是描述该线程属于哪个线程组(进程)的线程,这个tgid实际上是该线程所处线程组的首线程的pid,也就是说在一个线程组中tgid等于pid的那个线程就是主线程。
- 查看进程中所有线程信息1:ps -efL
ps -efL |head -1&&ps -efL |grep create
LWP:轻量级进程,这一列记录的就是每个线程task_struct中的tid
NLWP:这个进程中有几个线程
PPID:父进程ID
PID:进程ID(线程组tgid),也就是主线程的tid。
2、查看线程:
ps -aL |head -1&&ps -aL |grep create
PID:进程ID(线程组tgid),也就是主线程的tid。
LWP:线程ID,各个task_struct中的tid.
线程没有父子之分,所有线程都是有平级的,如果非要说有区别的话,那么就是主线程和其他线程的区别。
线程终止
线程的退出方式:
- return num;退出:在main函数中调用return,效果是退出进程,在线程中调用return,退出线程。
- exit(num)针对整个进程,即使在非主线程中使用exit(),也会是整个进程退出.
void pthread_exit(void *retval);
Compile and link with -pthread.
DESCRIPTION
The pthread_exit() function terminates the calling thread and returns a value
via retval that (if the thread is joinable)is available to another thread in
the same process that calls pthread_join(3).
retval存储线程退出状态信息,如果不关心,可以置空。
终止一个调用线程,线程调用线程退出,main函数调用,主线程退出,其他线程成了僵尸线程,进程显示僵尸进程,进程显示的是主线程stat。终止一个调用线程,线程调用线程退出,main函数调用,主线程退出,其他线程成了僵尸线程,进程显示僵尸进程,进程显示的是主线程stat。
查看线程的命令
ps aux -L | head -1 && ps aux -L | grep exit | grep -v 'test'
ps -aL
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
主线程用来取消主线程自己的线程id为thread的线程
//这段代码用于演示线程退出的几种方式
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
void *thr_start(void *arg)
{
//在线程中调用exit函数会怎样?
//进程要是退出了,那么进程中的线程也会退出
//exit(0);
//return NULL;
//sleep(5);
//pthread_exit(NULL);
while(1){
printf("child pthread!!!\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = -1;
ret = pthread_create(&tid,NULL,thr_start,NULL);
if(ret != 0){
printf("pthread create error\n");
return -1;
}
//int pthread_cancel(pthread_t thread);
//取消普通线程
pthread_cancel(tid);
while(1){
printf("first pthread!!\n");
sleep(1);
}
return 0;
}
线程等待(线程分离)
主线程退出,其他线程也会形成僵尸线程:占用了一部分资源不释放,最终造成资源泄露。
线程等待就是接受线程的返回值,然后释放线程的所有资源。
线程处于joinable状态才能被等待,如果一个线程在调用pthread_join函数之前已经退出,则pthread_join函数立即返回,否则阻塞等待,直到这个指定的线程退出,才会返回。
pthread_join函数的返回值只需要考虑线程的退出码或者errorno,不需要考虑线程异常的情况,因为一旦线程产生异常,系统会认为整个进程异常,这个进程就挂了。
int pthread_join(pthread_t thread, void **retval); //避免资源泄露,线程属性是joinable状态
pthread_t thread:指定等待线程的线程ID
void **retval:将线程退出信息接收到retval中
The pthread_join() function waits for the thread specified by thread to terminate. If that thread has already terminated,
then pthread_join() returns immediately. The thread specified by thread must be joinable.
//演示线程退出等待,获取线程返回值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
//线程需要被等待的条件是:线程处于joinable状态,一个线程创建出来默认属性就是joinable状态
void* Func()
{
sleep(2);//让这个非主线程睡觉,让主线程等它睡醒后,接收该线程的退出状态值,释放资源
printf("I am not the main pthread\n");
pthread_exit("I am not the main pthread !I will exit!");
return "I am the best!!";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,Func,NULL);
if(ret!=0){
perror("pthread_create error");
exit(-1);
}
printf("I am the Main pthread\n");
char* addr;//定义一级指针用于给pthread_join的第二个参数初始化,否则二级指针为空,不能使用
char** ptr = &addr;
sleep(5);
pthread_join(tid,(void**)ptr);//主线程到这里会阻塞等待它创建的线程id是tid的线程
printf("%s\n",*ptr);
//pthread_cancel(tid);
//sleep(5);
//pthread_join(tid,(void**)ptr);//主线程到这里会阻塞等待它创建的线程id是tid的线程
//如果一个线程是被取消的那么它的返回值只有一个-1,PTHREAD_CANCELD
//printf("%d\n",addr);
return 0;
}
线程分离:功能是设置状态
我们等待一个线程是因为需要获取线程的返回值,并且释放资源。那么假如我不关心返回值,那么这个等待将毫无意义,仅仅是为了释放资源。因此就有一个线程属性叫:线程分离属性 detach属性,这个属性就是需要设置,它是告诉操作系统,这个指定的线程我不关心返回值,所以如果线程退出了,操作系统就自动把所有资源回收。而设置一个线程分离属性我们常称为线程分离
线程的detach属性与joinable属性相对应,也相冲突,两者不会同时存在。如果一个线程属性是detach,那么pthread_join的时候将直接报错,所以我们说,只有一个线程处于joinable状态才可以被等待
线程被设置成detach属性,退出后将自动释放资源,不会形成僵尸线程
detach与joinable属性相冲突,无法同时存在,设置detach就表明了不关心返回值
int pthread_detach(pthread_t thread);
线程也可以分离自己pthread_detach(pthread_self());
- 使用gdb调试的注意事项
1、编译过程一定要加-g选项:因为在Linux系统下,默认生成的是release(不加调试信息)版本的可执行程序,如果不加-g,则不能调试。例如编译hello.c生成hello的debug版本;
gcc -g hello.c -o hello
2、在开启gdb调试不想看到那么一大堆版本信息可以加-q,例如调试hello
gdb -q hello