进程和线程的总结

本文详细介绍了Linux下线程的概念、特性及操作,包括线程与进程的区别、线程创建、线程ID获取、线程退出、线程同步与通信、线程属性设置等,并提供了示例代码,强调了线程资源共享与独立性,以及线程安全问题。
摘要由CSDN通过智能技术生成

线程:Linux下, 线程是轻量级的进程,又称为LWP(light weight process),线程是进程中的一条执行流程。

  1. 同一个进程下的线程拥有的PCB是独立的,然而它们共享相同的地址空间。PCB中指向内存资源的三级页表是相同的。
  2. 线程之间可以并发的执行。
  3. 线程之间共享内存地址空间(未初始化的全局变量和静态变量(bss),data段,text段,heap空间,共享区等),文件描述符表,当前工作目录,用户ID和组ID,当前的工作目录。
  4. 不共享的资源:用户栈空间,线程id,信号屏蔽字,errno变量,处理器现场和栈指针,调度优先级

线程和进程的区别:

  1. 进程是资源分配(分配内存地址空间,文件描述符等)的单位,而线程是CPU调度的基本单位,如果一个进程中包含3个线程,CPU调度的时候是线程。
  2. 线程相对于进程来说开销小,数据共享方便(只有栈和寄存器是独有的,其他资源都是共享的),提高程序的并发性。

查看线程号

可以从LWP那一列看到 ,对应的线程号。这里的线程号和下文提到的线程ID是不一样的,线程号是用来给CPU使用的。

ps -Lf 进程id

线程常用的操作:

1.获取线程id号, pthread_t本质上是无符号长整型数据(%lu),线程ID。线程ID是用来标识同一个进程下面的不同的线程的。不同的进程之间,线程ID号是可以相同。

pthread_t pthread_self(void);

2.创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数介绍:

1.thread:线程的id号,它是传出参数,是在pthread_create函数内部获取到的,传入的应该是它的地址。
2.attr:线程的属性,一般设置为NULL即可,是常量。
3.start_routine:函数指针,指向创建的线程函数的地址。函数的返回值为void*,对应的参数也为void*
4.线程函数的参数:因此它的参数也为void*类型,如果不需要的话,设置为NULL即可。

传入参数和传出参数:传入参数指的在函数调用的时候,该值已经被定义好,作为参数给函数使用。传出参数一般给的都是地址,该值在传入的时候没有确定下来,而是在函数内部被确定下来,从函数中带出值。例子永远是最好的解释:这里的num1是传入参数,num2是传出参数哦。

int func(int num1, int *num2)
{
    *num2 = 5;
    return num1 + *num2; 
}
int num1=4;
int num2
int result = func(num1, &num2);

对应的代码如下所示:需要注意的是这里必须要让主线程进行休眠,如果主线程不进行休眠的话, 直接return之后,进程的地址空间就会被销毁掉,由于线程之间的地址空间是共享的,所以线程也会被销毁掉,就无法看到打印的东西哦。

#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include <errno.h>
#include <pthread.h>
void* tfun(void *arg)
{
  printf("thread pid=%d,tid=%lu\n",getpid(),pthread_self());
  return NULL;
}
int main()
{
  pthread_t tid;
  printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
  int ret = pthread_create(&tid,NULL,tfun,NULL);
  if(ret !=0)
  {
    perror("pthread_create_error");
  }
  sleep(1);
  return 0;
}

循环创建多个线程:
代码如下所示:

void* tfun(void *arg)
{
  long int i=(long int)(arg);
  sleep(i);
  printf("thread pid=%d,i=%d\n,tid=%lu\n",getpid(),i,pthread_self());
  return NULL;
}
int main()
{
  pthread_t tid;
  long int i;
  for( i=0;i<5;++i)
  {
    int ret = pthread_create(&tid,NULL,tfun,(void*)i);
    if(ret !=0)
    {
      perror("pthread_create_error");
    }
  }
  printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
  sleep(i);
  return 0;
}

打印结果如下所示:
在这里插入图片描述
间隔一秒就会打印一次,由于线程是并发执行的,虽然线程创建的顺序是按照0,1,2,3,4的顺序,但是执行对应的线程函数时,是需要抢占CPU的。先被创建出来的,有可能先拿到CPU的使用权。宏观上来看,几个线程同时都会执行对应的回调函数,后面创建的线程睡眠的时间更长,比如进程1:sleep(1),经过1秒之后才打印。进程2,sleep(2),经过2秒之后打印。

错误的写法:
假设我们传递的是i的地址,那么对应的arg指向的其实是主线程中i的地址。需要注意的是,for循环中的i的变化,运行在用户态,当子线程去获取主线程中i元素的时候,对应的i值已经发生了改变。即回调函数中的对arg进行解引用的时候。
主要的原因是因为:使用pthread_create创建线程的时候,内部调用的是clone函数,需要不停的进行用户态和内核态的切换,因此消耗的时间是比较大的,而i值的改变速度又很快。

void* tfun(void *arg)
{
  long int i=*((int*)arg);
  sleep(i);
  printf("thread pid=%d,i=%d\n,tid=%lu\n",getpid(),i,pthread_self());
  return NULL;
}
int main()
{
  pthread_t tid;
  long int i;
  for( i=0;i<5;++i)
  {
    int ret = pthread_create(&tid,NULL,tfun,&i);
    if(ret !=0)
    {
      perror("pthread_create_error");
    }
  }
  printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
  sleep(i);
  return 0;
}

如何验证线程之间共享全局变量?
设置一个全局变量,在主线程中首先打印该值,在子线程中修改该全局变量的值,之后在主线程中查看该值,发现已经被修改啦!

3.进程的退出

void pthread_exit(void *retval)
  1. return:返回到调用者到那里去。main函数的调用者就是当前的out文件,对于线程来说:返回到pthread_create那里,pthread_create由主线程调用的。
  2. exit(0):退出当前的进程。
  3. pthread_exit(NULL),退出当前线程。
    总体来说,调用线程的时候,采用第三种方法,可以用来退出单个的线程。

4. 阻塞等待当前线程的退出,获取线程退出的状态
注意:任意线程得到其他线程的pid都可以回收,没有父线程回收子线程的说法。等价于进程中的waitpid函数,该函数是父进程回收子线程。
参考以下文章:pthread_join函数

int pthread_join(pthread_t thread, void **retval);
args:
    pthread_t thread: 被连接线程的线程号
    void **retval : 指向一个指向被连接线程的返回值的指针的指针:二级指针,因为线程的返回值为void*,因此存储它的返回状态使用void**。类似于wait函数,存储进程的退出状态使用的是int型指针,退出时返回的是pid。
return:
    线程连接的状态,0是成功,非0是失败

代码如下:需要注意的是创建的线程函数,它的返回值不是局部变量,如下所示:ret是在堆空间上分配的内存,因此当线程函数调用结束,通过join函数中的retval可以获取到对应的值,而如果不是malloc,而是在tfn中创建一个局部变量exit_t ret,ret.a=300,ret.b=400,得到的结果将是错误的,因为局部变量一旦脱离作用域将被销毁。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
typedef struct {
	int a;
	int b;
} exit_t;
void *tfn(void *arg)
{
	exit_t *ret;
	ret = malloc(sizeof(exit_t)); 
	ret->a = 100;
	ret->b = 300;
	//return (void*)ret.
	pthread_exit((void *)ret);
}
int main(void)
{
	pthread_t tid;
	exit_t *retval;
	pthread_create(&tid, NULL, tfn, NULL);
	/*调用pthread_join可以获取线程的退出状态*/
	pthread_join(tid, (void **)&retval);      //wait(&status);
	printf("a = %d, b = %d \n", retval->a, retval->b);
	return 0;
}

循环回收多个线程:需要注意的是回收子线程的时候,不能创建完立刻就回收,这样打印出来的线程id号都是一样的,因此操作系统回收第一个之后,会给第二个分配和第一个的一样的线程id号。

void* tfun(void *arg)
{
  long int i=(long int)(arg);
  sleep(i);
  printf("thread pid=%d,i=%d\n,tid=%lu\n",getpid(),i,pthread_self());
  return NULL;
}
int main()
{
  pthread_t tid[5];
  long int i;
  int ret;
  for( i=0;i<5;++i)
  {
    ret = pthread_create(&tid[i],NULL,tfun,(void*)i);
    if(ret !=0)
    {
      perror("pthread_create_error");
    }
  }
  
  for( i=0;i<5;++i)
  {
    ret = pthread_join(tid[i],NULL);
    if(ret !=0)
    {
      fprintf(stderr,"pthread_join_error:%s\n",strerror(ret));
      exit(1);
    }
  }
  printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
  sleep(i);
  return 0;
}

5.杀死线程

pthread_cancel函数,相当于进程中的kill函数。

int pthread_cancel(pthread_t thread);成功返回0,失败返回错误号

有以下几点需要注意:

1.线程的取消(杀死)需要等待一个取消点(一般来说进入系统调用时(sleep,open,write,read等函数时,会有机会调用该函数)。
2.如果没有这些系统调用函数,可以手动添加取消点,例如pthread_testcancel函数。
3.被取消的线程,退出时的返回值为-1

总结一下,终止进程的三种方式:
1.从线程主函数处return,它将返回到函数调用的位置处,类似于main函数中调用f(),返回到f函数的位置处。但是这种方法不适合于主线程,因为一旦return或者exit,就会退出整个进程。
2.使用pthread_exit函数,退出当前的线程(推荐使用)
3.一个线程调用pthread_cancel函数来终止同一进程中的其他线程。

6.分离线程
pthread_detach函数,用来分离线程,使得当前的线程和主控线程断开关系,当线程结束之后,其退出的状态将由自己自动释放,而不需要使用pthread_join回收资源。

函数原型如下:

int pthread_detach(pthread_t thread); 成功:0;失败:错误号

需要注意的是,一旦线程被设置为detach之后,再使用pthread_join会返回错误值,代码如下:

#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include <errno.h>
#include <pthread.h>
void* tfun(void *arg)
{
  long int i=(long int)(arg);
  sleep(i);
  printf("thread pid=%d,i=%ld\n,tid=%lu\n",getpid(),i,pthread_self());
  return NULL;
}
int main()
{
  pthread_t tid;
  long int i=0;
  int ret;
  ret = pthread_create(&tid,NULL,tfun,(void*)i);
 sleep(1);//加上sleep函数防止子线程还没有运行,就已经分离了。
  pthread_detach(tid);
  if(ret !=0)
  {
    perror("pthread_create_error");
  }
  ret = pthread_join(tid,NULL);
  if(ret !=0)
  {
    fprintf(stderr,"pthread_join_error:%s\n",strerror(ret));
    exit(1);
  }
  printf("main pid=%d,tid=%lu\n",getpid(),pthread_self());
  sleep(i);
  return 0;
}

在这里插入图片描述
参见本篇博文错误信息打印注意点
这里关于错误信息的打印需要注意:在进程中获取错误信息使用的是perror函数如下所示:

void perror(const char *msg);

因为一般正确返回0,错误返回-1,出错时errno被系统自动设置为对应的错误号,perro根据错误号将其对应的错误信息字符串打印。

int ret = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));;
    if (ret != 0)
    {
        perror("bind error: ");
        exit(1);
    }

而在线程中,正确返回的是0,出错返回的是错误码,因此不能使用perror去捕捉,直接用strerror就能把返回的错误号转换为错误信息字符串。

char * strerror(int errnum);
int ret = pthread_join(tid,NULL);
    if (ret != 0)
    {
        fprintf(stderr, "pthread_join_error: %s\n", strerror(ret));
        exit(1);
    }

7.线程和进程的对比(其实是非常相似哒),其中线程多了一个分离属性,而进程是没有的。

进程线程
进程的创建:fork线程的创建:pthread_create
进程的id:getpid线程的id:pthread_self
进程的退出:exit线程的退出pthread_exit
进程回收资源:waitpid线程回收资源:pthread_join
杀死进程:kill杀死线程:pthread_cancel

8.设置线程的属性

(1)int pthread_attr_init(pthread_attr_t *attr);
(2)int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
其中:detachstate取值有以下两种取值: 
1. PTHREAD_CREATE_DETACHED:线程被设置为分离状态。
2.  PTHREAD_CREATE_DETACHED:线程被设置为连接状态。
(3)int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
//(4)int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
(5)int pthread_attr_destroy(pthread_attr_t *attr);

在这里插入图片描述
9.线程属性注意点

  1. 主线程退出其他线程不退出,主线程应调用 pthread_exit
  2. 避免僵尸线程的方法
    pthread_join:被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值。
    pthread_detach
    pthread_create 指定分离属性:
  3. malloc 和 mmap 申请的内存可以被其他线程释放
    应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit
  4. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

10.vim:一键排版:gg=G,不需要在command模式下输入:
11.一键替换:%s/old/new/g
12.复制多行:nyy,p

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值