linux线程基本概念及线程控制

1.初识线程

1)线程的概念

在一个进程里的一个执行路线被称为线程,更专业的说是:线程是一个进程内部的控制序列,并且一个进程中至少有一个线程。

2)进程与线程的关系

进程:Linux系统中的进程被称为轻量级进程,CPU眼中的看到的PCB要比传统进程更加轻量级。透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流。进程是资源竞争的基本单位。

线程:linux的线程是用进程模拟的,没有真正意义的线程。对于一个线程的创建,其实是创建了一个进程,然后该进程(线程)的PCB与进程的PCB指向同一个虚拟地址空间,就可以共享资源,不同的PCB再分别工作。 线程是调度的基本单元,是程序执行的最小单位。线程共享进程数据比如可执行的程序文本,程序的全局内存与栈,堆内存以及文件描述符,但也有自己私有的数据比如:线程ID,寄存器(上下文信息),运行时堆栈(一个线程一个),erron值,信号屏蔽字,调度优先级。

3)进程的多个线程共享

同一个地址空间,因此Text Segment,Data Segment都是共享的,定义一个函数,各线程都可以调用;定义一个全局变量,各线程都可以访问到;并且各线程还共享以下的进程资源和环境:

  • 文件描述符表
  • 各种信号的处理方式(SIG_IGN(忽略),SIG_DFL(默认)或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id与组id

4)线程的优点

  • 创建一个线程的代价要比创建一个新进程代价小很多。
    因为创建一个进程开辟虚拟地址空间,页表,PCB以及对应关系等操作,而对于一个线程的创建,其实是创建一个新的进程,但该进程只需要开辟PCB,然后该进程的PCB与以前进程的PCB指向同一个虚拟地址空间,这样便可以共享数据,然后各自的线程PCB执行自己的操作。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作少很多。只需要切换上下文。
  • 线程占有的资源要比进程少很多。线程的大部分资源都是共享进程的。
  • 更充分的利用了多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上进行将计算分解到多个线程中实现。
  • I/O密集型应用(一个进/线程以I/O为主),为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。

5)线程的缺点

  • 性能损失
    一个很少被外部事件阻塞的计算密集型线程(以计算为主的线程)往往无法与其他线程共享一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失是指增加了额外的同步与调度开销,而可用的资源不变。

  • 健状性低
    编写多线程需要更全面更深入的考虑,在一个多线程程序中,因为时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,简单的说就是指线程与线程之间是缺乏保护的。

  • 缺乏访问控制
    进程是访问控制的基本粒度,在一个线程中调用某些操作系统函数会对整个进程造成影响。

  • 编程难度提高
    编写与调试一个多线程程序比单线程程序困难的多。

6)线程异常情况

  • 当单个线程中存在不可逆错误时(野指针,除0操作等)。不仅会导致该线程崩溃,而且进程也随之崩溃。
  • 线程是进程的执行分支,线层异常,会触发信号机制,从而终止进程,该进程的其他线程也会被终止。

2.线程控制

1)POSIX线程库

  • 与线程有关的函数构成一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文件<pthread.h>。
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”。

2)创建线程

功能:创建一个新的线程
原型

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

参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表达使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数。
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码。

错误检查

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量error(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
  • pthread同样也提供了线程内的errno变量,以支持其他使用errno的代码,对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。
    测试
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>


void *rout(void *arg)
{
  int i ;
  for( i = 0;i<5;i++)
  {
    printf("i am thread 1\n");
    sleep(1);
  }
}



int main()
{
  pthread_t tid;
  pthread_create(&tid,NULL,rout,NULL);
  int j;
  for( j = 0 ;j< 5;j++)
  {
    printf("man is running...\n");
    sleep(1);
  }
}

运行结果:
图中两个线程的运行顺序由调度优先级决定。
在这里插入图片描述

3)进程ID与线程ID

  • 在linux中,目前的线程实现是Native POSIX Thread Libaray,简称为NPTL。在这种实现下,线程被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符。

  • 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符号,进程和内核的描述符一下子就变成了1:N的关系。为了解决POSIX标准又要求进程内的所有线程调用gitpid函数时返回相同的进程ID的问题Linux引入了线程组:

    struct task_struct{
                ...
                pid_t pid;
                pid_t tgid;
                ...
                struct task_struct *group_leader;
                ...
                struct list_head  thread_group;
                ...
         };
    
  • 多线程的进程,又称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符号中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID

    线程ID-》系统调用pid_t gettid(void);—》内核进程描述符号中对应的结构是pid_t pid.
    进程ID-》系统调用pid_t getpid(void);—》内核进程描述符号中对应的结构是pid_t tgid.
    上面讲的线程ID,不同于pthread_t类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整型变量。

    使用ps -eLF | head -1 && ps -eLF | grep 线程名 | grep -v grep.
    ps -L 显示的LWP表示线程ID,是gettid()系统调用的返回值。NLWP表示线程组内线程的个数

  • Linux提供了 gettid系统调用来返回其线程的ID,但是glibc并没有将该系统调用封装起来,在开放接口供程序成员使用。如果确实需要获得线程ID,可以使用:
    #include<sys/syscall.h>
    pid_t tid;
    tid = syscall(SYS_gettid);

  • 线程组内的第一个线程,在用户态被称为主线程,在内核中被称为group leader,在内核创建第一个线程时,会将线程组的ID设置为第一个线程的ID,group leader指针指向自身,即主线程的进程描述符号。线程组内存在一个线程ID等于线程ID,而该线程即为线程组的主线程。

4)线程ID及进程地址空间布局

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址空间

  • pthread_create函数产生的线程ID与之前讲的线程ID并不相同。前面所说的线程ID属于进程调度的范畴。因为线程时轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。pthread_create函数产生并标记在第一个参数指向的地址中的线程ID中,属于NPTL线程库的范围。线程库的操作是根据此线程ID来操作线程的。pthread_t的线程ID本质上是进程地址空间的一个地址

  • 线程库NPTL提供了pthread_self函数可以获得线程自身的ID

    pthreat_t pthread_self(void);

5)线程终止

《1》三种方法可以终止某个线程但不是终止整个进程。

  • 从线程函数return ,这种方法对主线程不适用,因为从main函数进行return相当于调用exit,相当于终止进程.
  • 一个线程可以调用pthread_exit终止自己。
  • 一个线程可以调用pthread_cancel终止同一个进程的另一个进程。

注意:不可以调用exit,_exit和Exit因为整个进程就会终止。

《2》 pthread_exit函数

功能:线程终止
原型:void pthread_exit(void *value_ptr)
参数:value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回它的调用者。
对于value_ptr来说,这是一个输出型参数,该线程终止所返回的信息放在value_ptr所指向的内存空间中。
pthread_exit或者return函数返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能是在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

《3》pthread_cancel函数

功能:取消一个执行中的线程
原型: int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值
成功返回0;失败返回错误码

3.线程等待与分离

1)线程等待

《1》为什么需要等待线程?

  • 创建的新线程不会去复用刚才退出的地址空间
  • 已经退出的线程,其空间没有释放,仍然在进程的地址空间里,只有等到获取该线程的退出信息后,通知该线程,线程才会释放资源,如果不进行等待,会造成资源泄漏。

《2》使用pthread_join函数等待

功能:等待线程结束
​原型:
int pthread_join(pthread_t thread, void** value_ptr);
​
参数:
    thread:线程的ID
    value_ptr :它指向一个指针,后者指向线程的返回值
    
返回值:
    成功返回0,失败返回错误码

调用该函数的线程将挂起等待(阻塞式等待),直到ID为thread的线程终止。thread线程以不同的方式终止,通过pthraed_join得到的终止状态是不一样的。

value_ptr的值
如果thread线程通过return返回,value_ptr所指向的单元里面存放的是thread线程函数的返回值

如果thread线程是被别的线程调用pthread_cancel异常终止掉的value_ptr所指向的单元存放的是常数PTHREA_CANCELED

如果thread线程是自己调用pthread_exit终止的value_ptr所指向的单元里面存放的是pthread_exit的参数

如果对thread线程的终止状态不关心可以传NULL给value_ptr参数。

《3》代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>

void *thread1(void *arg)
{
  printf("thread 1 returning ...\n");
  int *p = (int*)malloc(sizeof(int));
  *p =1;
  return (void*)p;
}
void *thread2(void *arg){
  printf("thread 2 exiting....\n");
  int *p = (int*)malloc(sizeof(int));
  *p = 2;
  pthread_exit((void*)p);
}
void *thread3(void *arg)
{
  while(1){
    printf("thread 3 is running ...\n");
    sleep(1);
  }
  return NULL;
}
int main()
{
  pthread_t tid;
  void *ret;

  //thread 1 return
  pthread_create(&tid,NULL,thread1,NULL);
  pthread_join(tid,&ret);
  printf("thread return , thread id %X,return code:%d\n",tid,*(int*)ret);
  free(ret);
  //thread 2 exit
  pthread_create(&tid,NULL,thread2,NULL);
  pthread_join(tid,&ret);
  printf("thread return , thread id %X,return code:%d\n",tid,*(int*)ret);
  free(ret);
  //thread 3 cancel by other
  
  pthread_create(&tid,NULL,thread3,NULL);
  sleep(3);
  pthread_cancel(tid);
  pthread_join(tid,&ret);
  if(ret == PTHREAD_CANCELED){
    printf("thread return,thread id %X,return code:PTHREAD_CANCELED\n",tid);
  }
  else{
    printf("thread return,thread id %X,return code:NULL\n",tid);
  }
}

在这里插入图片描述

2)分离线程

默认情况下创建的线程都是joinable(结合的),线程退出后,需要对其进行pthread_join操作,否则就会无法释放资源,从而造成内存泄露。

如果不关心线程的返回值的话,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

使用 pthread_detach函数分离线程,OS自动释放资源,不需要等待。

int pthread_detach(pthread_t thread);
​可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
int pthread_detach(pthread_self());

​ joinable和分离是冲突的,一个线程既不能是joinable也是分离的。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

void *thread_run(void *arg){
  pthread_detach(pthread_self());
  printf("%s\n",(char*)arg);
  return NULL;
}

int main()
{
  pthread_t tid;
   if (pthread_create(&tid, NULL, thread_run, (void*)("thread 1")) > 0)
   {
     printf( "pthread_create error!");
   }
   sleep(1);
   int ret = 0;
   if (pthread_join(tid, NULL) == 0)
   {
      printf("wait success");
   }
   else{
    printf( "wait error!");
         ret = 1;
   }
   
     return ret;
}

结果为:

thread 1
wait error

猜你喜欢

转载自blog.csdn.net/weixin_41892460/article/details/84558054