操作系统-POSIX线程基本操作
基本概念
可移植操作系统接口(Portable Operating System Interface of UNIX),简称POSIX,OSIX线程(POSIX threads),简称Pthreads,是POSIX标准中对线程定义的一部分标准规范。Pthreads定义了创建和操作线程的一整套API(包括类型、函数、常量),在编写程序时,只要遵循Pthreads规范,那么这段程序就是可以在任何支持Pthreads的操作系统上运行,实现优良的跨平台特性。
总的来说,POSIX就是一个标准化规范接口,更详细的POSIX介绍,可以参考文章末尾官方文档。
POSIX线程优缺点: 进程与线程区别 `进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位`。不同进程拥有独立的代码空间和内存资源,同一进程下的线程是共享当前进程的所有系统资源。进程和线程存在关联,但又有一定的区别,归纳起来有几个本质的不同点。
- 进程拥有自己独立的资源空间,线程是“轻量进程”,共享进程的空间(内存、堆栈、文件描述符)
- 进程是线程的“容器”,每个进程至少存在一个线程
- 进程的创建、调度和销毁过程的资源开销要比线程大
进程的直接访问对象是操作系统,线程的直接访问对象是进程
线程基本目录
前言
:
线程之前应该先了解进程。进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础
。进程是线程的容器。
进程可以简单的理解为一个可以独立运行的程序单位。它是线程的集合,进程就是有一个或多个线程构成的,每一个线程都是进程中的一条执行路径。
提示:以下是本篇文章正文内容,下面案例可供参考
一、线程是什么?
线程(LWP,light weight process)是轻量级的进程,本质仍是进程(在类unix环境下)。进程有独立地址空间,拥有PCB;线程也有PCB,PCB 是独立的,但没有独立的地址空间(共享)
。在Linux下,线程是最小的执行单位
;进程是最小分配资源单位,可看成是只有一个线程的进程。同一个进程的所有线程的PCB都保存有相同的页表,因此线程共享虚拟地址空间。
多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换(因为系统切换的速度非常的快,所以给我们在同时运行的错觉)。
1.线程优缺点
从进程与线程的特性差异对比,线程最大的优势是资源开销比进程要小。线程相比进程,还存在几个优点。
- 与fork相比, 创建线程无需申请文件描述符、系统资源等,效率高
- 线程共享进程的资源空间,避免进程复杂IPC机制
- 线程间的上下文切换要比进程间的上下文切换快得多
缺点:
线程也存在不足,由于线程是共享进程资源空间,如果一个线程因为异常崩溃导致共享资源(如内存空间)异常,其他线程很可能也会崩溃
。进程拥有独立资源空间,以及操作系统的保护机制,一个进程崩溃不会影响其他进程。
二、POSIX线程常用API
Pthreads定义了一套 基于C 语言的类型、函数和常量,API接口声明位于 pthread.h 头文件中,Pthreads所有 API 以 “pthread_”
为 前缀,命名根据具体功能翻译,比较直观通俗,可读性优良。Pthreads API主要包括线程管理和一系列线程间通信(同步)机制。
线程管理(Thread management)
提供了线程创建(creating)、分离(detaching)、调度(joining)以及线程属性操作的一系列函数。
线程间通信(同步)机制
提供了通信机制的创建、销毁、访问及它们属性操作的一系列函数。
【1】互斥锁(Mutex)
【2】读写锁(read/write lock)
【4】自旋锁(spin lock)
【5】屏障(barrier)
【6】条件变量(Condition variable)
注:
信号量(semaphore)和消息队列(Message queue)不属于Pthreads标准的范畴内,但属于POSIX的标准。因此,API命名风格上也有差异,信号量API以“sem_”为前缀,消息队列以“mq_”为前缀。
文主要描述线程管理相关的基本操作,函数命名以 “pthread_”为前缀。
函数组 | 前缀 | 例子 |
---|---|---|
线程属性 | pthread_attr_ | pthread_attr_t |
线程操作 | pthread_ | pthread_create |
三、常用接口
1.线程创建函数
pthread_create是操作系统的创建线程的函数。它的功能是创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的回调函数。
函数原型:
#include <pthread.h>
int pthread_create(
pthread_t *restrict tidp, //新创建的线程ID指向的内存单元。
const pthread_attr_t *restrict attr, //线程属性,默认为NULL
void *(*start_rtn)(void *), //新创建的线程从start_rtn函数的地址开始运行,函数运行结束,表示子线程结束
void *restrict arg //默认为NULL。若上述函数需要参数,将参数放入结构中并将地址作为arg传入。
);
返回值:成功:0;失败:错误编号
执行流程:在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_rtn决定(线程创建成功后就立即去执行start_rtn函数 回调函数)。start_rtn函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_rtn的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_rtn返回时,这个线程就退出了,在主线程可以调用pthread_join得到start_rtn的返回值,类似于父进程调用wait( )得到子进程的退出状态。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid( )可以获得当前进程的id,是一个正整数值。线程id的类型是pthread_t,它只在当前进程中保证是唯一的,在不同的系统中pthread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址
,所以不能简单地当成整数(%lu)用printf打印,调用pthread_self( )可以获得当前线程的id。
2.线程分离函数
pthread_detach函数:
int pthread_detach(
pthread_t thread // 线程id, 在主控线程当中可以直接获取, 在子线程当中可以使用pthread_self() 获取
);
成功:0;失败:错误号
作用:从状态上实现线程分离,注意不是指该线程独自占用地址空间。
线程状态:线程分离状态:指定该状态设置为detach状态,线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接由自己自动释放(自己清理掉PCB的残留资源)。网络、多线程服务器常用。由于不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误(22号错误)。
3.线程退出状态
pthread_join函数:
int pthread_join(pthread_t thread, // 在主控线程中/主控进程中
void **retval // 存储线程的退出状态 用户定义的指针,用来存储被等待线程的返回值地址
);
成功:0;失败:错误号
作用:阻塞等待线程退出,获取线程退出状态,一般在主空线程或者主控进程中。其作用对应进程中 waitpid() 函数。
线程退出状态总结:调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值(即void *指针);
如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数(即void *指针),其实1与3是等效的。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
线程回收总结:一个进程中的所有线程都是可以互相回收的,即不是只能由主控线程回收子线程,兄弟线程之间也可以回收。一次pthread_join只能回收一个线程。回收线程时也可以不关心线程结束时的状态,pthread_join函数的参数传NULL即可,此时不关心线程结束时的状态,只是将其回收并释放线程所占用的资源。
pthread_join与pthread_detach的区别
两者功能类似,都是在线程结束后用来回收线程占用的资源。
一个线程默认属性是可结合状态(joinable), 如果一个线程结束运行但主线程没有等待其结束,则它的状态会变成类似于进程中的僵尸进程状态(Zombie Process),部分资源没有被回收(如退出状态码),所以创建线程者(主线程)应该调用pthread_join来等待线程结束,获得线程的退出代码,回收资源(类似进程的wait、waitpid)。
pthread_join是阻塞函数,直至线程结束,但一些场景下,主线程不能或者不希望被阻塞,此时可以在主线程或者子线程调用pthread_detach函数,使子线程成为脱离状态(detached),这样该线程结束后会自动释放所有资源,而且pthread_detach函数是非阻塞函数,主线程也不会被阻塞。
3.获取线程id函数
pthread_self函数:
pthread_t pthread_self(void);
返回值:成功:调用该函数的线程ID;失败:永远不会失败。
pthread_t:typedef unsigned long int pthread_t; 输出格式格式为:%lu
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
作用: 在子线程中通过使用pthread_self线程ID。然后在进行相应操作,比如分离线程等
4.线程退出函数
pthread_exit函数:
void pthread_exit(void *retval); 参数:retval表示线程退出状态,通常传NULL。用户定义的指针,用来存储线程退出状态的返回值地址
作用:将单个线程退出。
线程退出总结:
-
return的作用是返回到函数的调用点,如果是main函数中的return,则代表该进程结束,并释放进程地址空间,所有线程都终止。对于其它函数的return,则直接返回到函数的调用点。
-
exit和_exit函数会直接终止整个进程,导致所有线程结束。pthread_exit函数则会导致调用该函数的线程结束。所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程也结束,主控线程退出时不能return或exit。
-
另注意,pthread_exit或者return返回的指针(线程执行的函数用return或者pthread_exit结束线程时)
所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配
,因为当其它线程得到这个返回指针时线程函数已经退出了,此时退出的这个线程函数所占据的栈空间可能又会被重新分配出去,因此其他线程再次使用这个返回的地址没有意义。
线程的最外层函数的结束可以不用调用pthread_exit函数来退出线程,因为线程return时,线程会自动退出调度状态,系统会回收线程资源。一般情况下,我们在线程内部,在满足特定条件时,需要退出线程,才调用pthread_exit使线程终止并退出。
注:
如果多个线程共享一段资源,一个线程退出后,并不会释放共享资源,直至所有线程退出调度,系统才会回收共享资源。
5.线程杀死函数
pthread_cancel函数:
int pthread_cancel(pthread_t thread);
成功:0;失败:错误号
作用:杀死(取消)线程,其作用对应进程中 kill() 函数。
取消点:
注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。杀死线程不是立刻就能完成,必须要到达取消点。取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write等等。执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthread_testcancel( )函数自行设置一个取消点,即在子线程的执行的函数中调用该函数即可:pthread_testcancel( ); 该函数是库函数,但执行该库函数需要进一步使用系统调用,从而到达检查点。只要线程到达检查点,如果有其它线程对其调用了pthread_cancel函数,则该线程就会自动终止。
被取消的线程,退出值为常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)。因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1。
线程比较函数
pthread_equal函数
int pthread_equal(pthread_t t1, pthread_t t2);
返回,线程号相等返回1,否则返回0
作用:比较两个线程ID是否相等。
线程错误函数
char * strerror(int errnum);
编译
gcc编译时,要加参数:-lpthread(libpthread.so)
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。
例子
回收子线程的返回值的
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
typedef struct {
int a;
char b;
char str[10];
}exit_t; //返回值为一个结构体
void *ftn( void *arg )
{
int s;
exit_t *retval; //retval的地址在栈空间,但是retval本身却位与堆空间(heap),malloc所分配的。
retval=(exit_t *)malloc( sizeof(exit_t) ); //必须用malloc分配,不能用线程栈空间
s=(int)arg; //注意只能传值,不能传地址(用于判别是哪个线程)
if( s<=1 )
{
retval->a=10;
retval->b='a';
strcpy(retval->str,"zsx");
}
if( s>1 && s<=3 )
{
retval->a=20;
retval->b='b';
strcpy(retval->str,"rgf");
}
if( s>3 )
{
retval->a=30;
retval->b='c';
strcpy(retval->str,"zy");
}
printf("I am %dth thread, and my ID is %lu.\n",s+1,pthread_self( ));
pthread_exit((void *)retval); //或者 return (void *)retval; 两者等价!
}
int main(int argc, char *argv[ ])
{
int n=5, i, ret;
if( argc==2 )
n=atoi(argv[1]);
pthread_t tid[n];
for(i=0;i<n;i++)
{
ret=pthread_create(&tid[i],NULL,ftn,(void *)i); //注意只能传值,不能传地址,因为地址对应的i值会变化
if( ret!=0)
{
fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
exit(1);
}
}
for(i=0;i<n;i++) //回收每个子线程
{
exit_t *re; //re位于主控线程的栈空间,但是re本身的值为子线程传给它的值。
ret=pthread_join( tid[i],(void **)&re);
if( ret!=0)
{
fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
exit(1);
}
printf("the %dth thread: a=%d, b=%c, str=%s.\n",i+1,re->a,re->b,re->str);
free(re); //注意必须释放malloc分配的空间,防止内存泄漏
re = NULL; //置空,防止使用幽灵指针
}
printf("In main: the PID is %d, and the TID is %lu.\n",getpid( ),pthread_self( ));
pthread_exit((void *)1);
}
总结: 回收子线程的返回值的方式有很多种:
- 第一种方式:上述程序中,exit_t *retval;
然后直接返回retval这个地址给主控线程,但是地址不能是用户栈空间,因此用malloc函数分配空间后,再赋值返回; - 第二种方式:也可以exit_t retval,即直接返回retval本身这个值,即return (void *)retval;
主控线程中:exit_t *re; (exit_t)re即可,但是不建议这样做,兼容性不高。这种方法类似于上面的i传参方式; - 第三种方式:还是按值传递,即利用主控线程中的pthread_create函数中的最后一个参数,将值传给子线程,然后让子线程返回回来,即:主控线程:
exit_t *re=(exit_t *)malloc( sizeof(exit_t) ), 然后(void
*)re作为其最后一个参数传递给子线程的arg参数,则子线程中:(exit_t *)arg,然后给arg赋值,最后(void *)arg返回即可!
注意在线程函数中:pthread_exit((void *)retval);不能写为:pthread_exit((void *)&retval); 同样的错误,即这样相当于传递的是子线程用户栈空间的地址。