POSIX线程基本操作

操作系统-POSIX线程基本操作

基本概念
可移植操作系统接口(Portable Operating System Interface of UNIX),简称POSIX,OSIX线程(POSIX threads),简称Pthreads,是POSIX标准中对线程定义的一部分标准规范。Pthreads定义了创建和操作线程的一整套API(包括类型、函数、常量),在编写程序时,只要遵循Pthreads规范,那么这段程序就是可以在任何支持Pthreads的操作系统上运行,实现优良的跨平台特性。
  总的来说,POSIX就是一个标准化规范接口,更详细的POSIX介绍,可以参考文章末尾官方文档。


POSIX线程优缺点: 进程与线程区别   `进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位`。不同进程拥有独立的代码空间和内存资源,同一进程下的线程是共享当前进程的所有系统资源。进程和线程存在关联,但又有一定的区别,归纳起来有几个本质的不同点。
  1. 进程拥有自己独立的资源空间,线程是“轻量进程”,共享进程的空间(内存、堆栈、文件描述符)
  2. 进程是线程的“容器”,每个进程至少存在一个线程
  3. 进程的创建、调度和销毁过程的资源开销要比线程大
  4. 进程的直接访问对象是操作系统,线程的直接访问对象是进程


前言


线程之前应该先了解进程。进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是线程的容器。
进程可以简单的理解为一个可以独立运行的程序单位。它是线程的集合,进程就是有一个或多个线程构成的,每一个线程都是进程中的一条执行路径。


提示:以下是本篇文章正文内容,下面案例可供参考

一、线程是什么?

线程(LWP,light weight process)是轻量级的进程,本质仍是进程(在类unix环境下)。进程有独立地址空间,拥有PCB;线程也有PCB,PCB 是独立的,但没有独立的地址空间(共享)。在Linux下,线程是最小的执行单位;进程是最小分配资源单位,可看成是只有一个线程的进程。同一个进程的所有线程的PCB都保存有相同的页表,因此线程共享虚拟地址空间。
多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换(因为系统切换的速度非常的快,所以给我们在同时运行的错觉)。

1.线程优缺点

从进程与线程的特性差异对比,线程最大的优势是资源开销比进程要小。线程相比进程,还存在几个优点。

  1. 与fork相比, 创建线程无需申请文件描述符、系统资源等,效率高
  2. 线程共享进程的资源空间,避免进程复杂IPC机制
  3. 线程间的上下文切换要比进程间的上下文切换快得多
    缺点:
    线程也存在不足,由于线程是共享进程资源空间,如果一个线程因为异常崩溃导致共享资源(如内存空间)异常,其他线程很可能也会崩溃。进程拥有独立资源空间,以及操作系统的保护机制,一个进程崩溃不会影响其他进程。

二、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_ttypedef  unsigned long int  pthread_t;  输出格式格式为:%lu

线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
作用: 在子线程中通过使用pthread_self线程ID。然后在进行相应操作,比如分离线程等

4.线程退出函数

pthread_exit函数:

void pthread_exit(void *retval); 参数:retval表示线程退出状态,通常传NULL。用户定义的指针,用来存储线程退出状态的返回值地址
作用:将单个线程退出。

线程退出总结:

  1. return的作用是返回到函数的调用点,如果是main函数中的return,则代表该进程结束,并释放进程地址空间,所有线程都终止。对于其它函数的return,则直接返回到函数的调用点。

  2. exit和_exit函数会直接终止整个进程,导致所有线程结束。pthread_exit函数则会导致调用该函数的线程结束。所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程也结束,主控线程退出时不能return或exit。

  3. 另注意,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);
}

总结: 回收子线程的返回值的方式有很多种:

  1. 第一种方式:上述程序中,exit_t *retval;
    然后直接返回retval这个地址给主控线程,但是地址不能是用户栈空间,因此用malloc函数分配空间后,再赋值返回;
  2. 第二种方式:也可以exit_t retval,即直接返回retval本身这个值,即return (void *)retval;
    主控线程中:exit_t *re; (exit_t)re即可,但是不建议这样做,兼容性不高。这种方法类似于上面的i传参方式;
  3. 第三种方式:还是按值传递,即利用主控线程中的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); 同样的错误,即这样相当于传递的是子线程用户栈空间的地址。

参考

线程的概念
线程属性的修改
POSIX官方文档

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值