Linux系统编程-线程

目录

[一. 线程](#一. 线程)

[1.1 什么是线程](#1.1 什么是线程)

[1.2 线程与进程的区别](#1.2 线程与进程的区别)

[1.3 线程的共享与非共享资源](#1.3 线程的共享与非共享资源)

[1.3.1 线程共享资源](#1.3.1 线程共享资源)

[1.3.2 线程非共享资源](#1.3.2 线程非共享资源)

[1.4 线程优缺点](#1.4 线程优缺点)

[1.4.1 线程优点](#1.4.1 线程优点)

[1.4.2 线程缺点](#1.4.2 线程缺点)

[二. 线程控制函数](#二. 线程控制函数)

[2.1 pthread_self()](#2.1 pthread_self())

[2.2 pthread_create()](#2.2 pthread_create())

代码1,创建一个线程:

代码2,循环创建多个线程,并且要求每个线程打印自己是第几个被创建的:

代码3,线程间共享全局变量测试:

[2.3 pthread_exit()](#2.3 pthread_exit())

[2.4 pthread_join()](#2.4 pthread_join())

[2.5 pthread_detach()](#2.5 pthread_detach())

[2.6 pthread_cancel()](#2.6 pthread_cancel())

[2.7 线程终止的三种方式](#2.7 线程终止的三种方式)

[2.8 进程与线程控制原语对比](#2.8 进程与线程控制原语对比)


一. 线程

1.1 什么是线程

线程:light weight process(LWP)轻量级进程,本质还是进程。

1.2 线程与进程的区别

|--------------------------|-------------------|
| 进程 | 线程 |
| 独立的地址空间,拥有PCB | 拥有独立的PCB,但是地址空间共享 |
| 是最小分配资源单位(可认为是只有一个线程的进程) | 最小执行单位 |

另外,若在进程A中创建了多个线程,但在CPU眼中其为进程,则进程A争夺CPU资源具有优势(线程为最小执行单位),但线程并不是越多越好。

这里通过一个小例子来说明:

在Linux系统下打开火狐浏览器:使用ps ajx命令查看其进程pid为6940

再使用命令 ps -Lf 6940 查看其LWP线程情况:LWP指的是线程号(CPU执行的最小单位),可以看到总共有64个线程,故浏览器的执行速度一般很快。

1.3 线程的共享与非共享资源

线程可以看作寄存器和栈的集合

1.3.1 线程共享资源

|------------------------------------------|
| 文件描述符表 |
| 每种信号的处理方式、未决信号集(说明某信号一但处理完成,其他线程无需在进行处理) |
| 当前工作目录 |
| 用户ID和组ID |
| 内存地址空间(.text/.data/.bss/heap/共享库) |

1.3.2 线程非共享资源

|------------------------------|
| 线程ID |
| 处理器现场(寄存器)和栈指针(内核栈) |
| 独立的栈空间(用户栈) |
| errno变量(线程控制函数失败时大部分直接返回错误号) |
| 信号屏蔽字(mask) |
| 调度优先级(可以通过指定调度优先级来指导哪个线程先执行) |

总结:

1、虽然线程共享每种信号的处理方式,但是线程不共享信号屏蔽字,可以通过修改信号屏蔽字来

指定一个线程来处理特定的信号。

2、线程共享.data,说明共享全局变量,线程间通信简单。

1.4 线程优缺点

1.4.1 线程优点

1、提高程序的并发性

2、开销小

3、共享数据,数据通信简单

1.4.2 线程缺点

1、库函数不稳定

2、调试,编写困难,不支持gdb调试

3、对信号支持不好

二. 线程控制函数

|--------------------------|----------------------|
| pthread_t pthread_self() | 获取线程ID |
| int pthread_creat() | 创建线程 |
| void pthread_exit() | 将单个线程退出 |
| int pthread_join() | 阻塞等待线程退出,获取线程退出状态 |
| int pthread_detach() | 设置线程分离,线程结束后自己主动释放资源 |
| int pthread_cancel() | 杀死线程,需要取消点 |

2.1 pthread_self()

作用:获取当前调用此函数线程的线程ID。

返回值:此函数总是成功。

注意:线程ID与LWP的区别:

线程ID:是进程内部识别线程的标志,主要是给本进程使用的(两个进程间,线程ID允许相同)

LWP:标识线程身份,给CPU用,CPU使用其来分配资源

2.2 pthread_create()

作用:`pthread_create()` 函数会在调用进程内启动一个新的线程。新线程通过调`start_routine()` 函数开始执行,而参数 `arg` 则作为 `start_routine()` 函数的唯一参数被传递进去。

返回值:成功返回0 失败返回错误号。

参数一:传出参数,传出线程ID

参数二:线程属性,一般为NULL

参数三:回调函数,线程的执行逻辑,

注意:这个函数传参时应该使用传值调用,借助强转来接收参数(精度不会丢失)。

参数四:回调函数的参数

备注:在一个线程中调用pthread_create() 创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。 start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait()得到子进程的退出状态,稍后详细介绍pthread_join

pthread_create成功返回后,新创建的线程的id被填写到thread参数 所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid()可以获得当前进程的id,是一个正整数值。线程id的类型是pthread_t ,它只在当前进程中保证是唯一的,在不同的系统中pthread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self()可以获得当前线程的id。

代码1,创建一个线程:

cpp 复制代码
void *tfun(void *arg)//子线程的回调函数
{
    printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
    return NULL;
}
int main()
{
    pthread_t tid;
    int ret;
    printf("main  : pid is %d,tid is %lu\n",getpid(),pthread_self());
    ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
    if(ret != 0)
    {
        fprintf(stderr,"pthread_creat error: %s\n",strerror(ret));
        exit(1);
    }
    printf("tid is %lu\n",tid);
    sleep(1);//防止主线程先结束,进程地址空间销毁,子线程无法执行回调函数
    return 0;
}

执行结果:

代码2,循环创建多个线程,并且要求每个线程打印自己是第几个被创建的:

正确的写法:

cpp 复制代码
void *tfun(void *arg)
{
    int i = (int)arg;
    sleep(i);
    printf("%d pthread:pid is %d,tid is %lu\n",i + 1,getpid(),   pthread_self());
    return NULL;
    
}
int main()
{
    pthread_t tid;
    int ret, i;
    printf("main:pid is %d,tid is %lu\n",getpid(),pthread_self());
    for(i = 0; i < 5; i++)
    {
       ret = pthread_create(&tid, NULL, tfun, (void*)i);//传参采用值传递
                                                        //借助强转
       if(ret != 0)
        {
            fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
            exit(1);
        }
    }
    sleep(i);
    return 0;
}

结果:

错误写法:

cpp 复制代码
void *tfun(void *arg)
{
    int i = *((int*)arg);
    sleep(i);
    printf("%d pthread:pid is %d,tid is %lu\n",i + 1,getpid(),   pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    int ret, i;
    printf("main:pid is %d,tid is %lu\n",getpid(),pthread_self());
    for(i = 0; i < 5; i++)
    {
       ret = pthread_create(&tid, NULL, tfun, (void*)&i);
       if(ret != 0)
        {
            fprintf(stderr,"pthread_creat error: %s\n",strerror(ret));
            exit(1);
        }
    }
    sleep(i);
    return 0;
}

结果:

两段代码的区别在于传参时的区别:正确的代码采用传值调用,直接将i的值传给回调函数,错误的代码采用传址调用,将i的地址传给回调函数。

**错误分析:**i变量保存在main函数的栈帧里,当将i的地址传递给回调函数时,回调函数的栈帧,也就是tfun这个函数的栈帧拿到的是i的地址,通过这个地址访问的是main函数栈帧的i值,此时main函数这个线程正在运行,i会发生变化,故会出错。

代码3,线程间共享全局变量测试:

cpp 复制代码
int var = 100;
void *tfun(void *arg)//子线程的回调函数
{
    var = 200;
    printf("pthread:var = %d\n",var);
    return NULL;
}

int main()
{
    pthread_t tid;
    int ret;
    printf("before pthread var = %d\n",var);//线程之间共享全局变量

    ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
        exit(1);
    }
    sleep(1);//防止主线程先结束,进程地址空间销毁,子线程无法执行回调函数
    printf("after pthread var = %d\n",var);
    return 0;
}

结果:

2.3 pthread_exit()

作用:将单个线程退出,不销毁进程地址空间。

参数:retval表线程退出状态

另注意:pthread_exit返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在

线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

返回值:成功返回0 失败返回错误号。

exit、return、pthread_exit各自退出效果:

exit:表示退出当前进程

return:表示返回到调用者

pthread_exit:表示退出单个线程

代码:

cpp 复制代码
void *tfun(void *arg)
{
    int i = (int)arg;
    sleep(i);
    if(i == 2)
        //exit(0);//表示退出当前进程
//        return NULL;//表示返回到调用者
        pthread_exit(NULL);//表示退出单个线程        

    printf("%d pthread:pid is %d,tid is %lu\n",i + 1,getpid(),   pthread_self());
    return NULL;
    
}

int main()
{
    pthread_t tid;
    int ret, i;
    printf("main:pid is %d,tid is %lu\n",getpid(),pthread_self());
    for(i = 0; i < 5; i++)
    {
       ret = pthread_create(&tid, NULL, tfun, (void*)i);//传参采用值传递
                                                        //借助强转
       if(ret != 0)
        {
            fprintf("pthread_create error: %s\n",strerror(ret));
            exit(1);
        }
    }

    sleep(i);
    return 0;
}

exit结果:

return结果:

pthread_exit结果:

代码:

cpp 复制代码
void *tfun(void *arg)//子线程的回调函数
{
    printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    int ret;

    printf("main  : pid is %d,tid is %lu\n",getpid(),pthread_self());

    ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
    if(ret != 0)
    {
        fprintf(stderr,"pthread_creat error: %s\n",strerror(ret));
        exit(1);
    }

    //sleep(1);//防止主线程先结束,进程地址空间销毁
             //子线程无法执行回调函数
    //return 0;
   // pthread_exit(NULL);
    pthread_exit((void*)0);
}

2.4 pthread_join()

作用:阻塞等待线程退出,获取线程退出状态。

参数:thread表示线程ID,retval传出参数,存储线程结束状态

注:若线程非正常死亡,例如被pthread_cancel函数取消,则retval所指向的位置

为-1(PTHREAD_CANCELED )(另外注意是retval所指向的位置为-1,并不是pthread_join

函数返回-1)

返回值:成功返回0 失败返回错误号。

对比记忆:

进程中:main返回值、exit 参数为 int 类型,则等待子进程结束 wait 函数 的参数类型为 int * 类型。

线程中:线程主函数返回值(pthread_create函数的第三个参数,是一个函数指针,返回值类型为void*)、pthread_exit 的参数为void * 类型,则等待线程结束 pthread_join 函数 的参数为 void ** 类型。

调用该函数的线程将挂起等待,直到idthread 的线程终止。thread线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,++retval所指向的单元里存放的是常数PTHREAD_CANCELED(-1)++。
  3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

代码:

cpp 复制代码
typedef struct stu{
    int age;
    char name[20];
}stu;

void *tfun(void *arg)
{
    stu *s1;
    s1 = malloc(sizeof(stu));
    s1->age = 18;
    strcpy(s1->name, "zhangsan");
    return (void*)s1;
}

/*
void *tfun(void *arg)
{
    stu s1;
    s1.age = 18;
    strcpy(s1.name, "zhangsan");
    return (void*)&s1;//这样不行,函数调用结束,栈帧销毁
                      //返回不了局部变量
}
*/
int main()
{
    pthread_t tid;
    stu arg, *s;
    int ret;
    pthread_create(&tid, NULL, tfun, NULL);
    ret = pthread_join(tid, (void**)&s);//获取子线程的退出状态/退出值
    if(ret != 0)
    {
        fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
        exit(1);
    }
    printf("pthread_join finish\n");
    printf("age = %d, name = %s\n",s->age,s->name);
    free(s);
    s=NULL;
    pthread_exit(NULL);
}

结果:

使用valgrind工具查看其有无内存泄漏:

2.5 pthread_detach()

作用:设置线程分离,线程终止不能残留资源在内核中,线程自己主动释放资源(网络,多线程服

务器常用)

参数:thread表示指定线程的ID。

返回值:成功返回0 失败返回错误号。

**1、线程分离状态:**指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

2、**僵尸进程:**进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。

3、也可使用 pthread_create函数参2(线程属性)来设置线程分离。

4、不能对一个已经处于detach状态 的线程调用pthread_join,++这调用的pthread_join将返回**22号errnoEINVAL(参数无效)**错误(++ 这里是pthread_join函数返回22,而不改变retval所指向的位置的值,与pthread_cancel函数对pthread_join函数的效果不一样++)。++也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

代码:

cpp 复制代码
void *tfun(void *arg)//子线程的回调函数
{
    printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    int ret;
    printf("main  : pid is %d,tid is %lu\n",getpid(),pthread_self());

    ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
        exit(1);
    }
    ret = pthread_detach(tid);//线程分离,线程自动回收资源
    if(ret != 0)
    {
        fprintf(stderr,"pthread_detach error:%s\n",strerror(ret));
        exit(1);
    }
    sleep(1);
    ret = pthread_join(tid,NULL);
    printf("pthread_join ret is %d\n",ret);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
        exit(1);
    }
    pthread_exit(NULL);
}

结果:

2.6 pthread_cancel()

作用:杀死(取消)线程

参数:参数:thread表示指定线程的ID。

返回值:成功返回0 失败返回错误号。

注意:1、线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。

2、取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用

creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这

些取消点的系统调用列表。

3、可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过

调用 pthread_testcancel函数自行设置一个取消点。

4、被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值

是 -1。可在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void

*) -1)。 因此当我们对一个已经被取消的线程使用 pthread_join 回收时,得到的返回值

为-1。

代码1:

cpp 复制代码
void *tfun(void *arg)//子线程的回调函数
{
    while(1)
    {
        printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
        sleep(1);
    }
    return (void*)666;
}

int main()
{
    pthread_t tid;
    int ret;
    void *arg;


    ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
        exit(1);
    }

    pthread_cancel(tid);

    ret = pthread_join(tid, &arg);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
        exit(1);
    }
    printf("pthread_join arg is %d\n",(int)arg);
    printf("main  : pid is %d,tid is %lu\n",getpid(),pthread_self());

    pthread_exit(NULL);
}

结果:

代码2,三种终止线程方式的比较:

cpp 复制代码
//三种终止线程方式的比较

void *tfun1(void *arg)//子线程的回调函数
{
    printf("thread 1 return\n");
    return (void*)111;
}
void *tfun2(void *arg)//子线程的回调函数
{
    printf("thread 2 exit\n");
    pthread_exit((void*)222);
}
void *tfun3(void *arg)//子线程的回调函数
{
    while(1)
    {
       // printf("thread 3 cancel\n");
     //   sleep(1);
         pthread_testcancel();//自己添加取消点
    }
    return (void*)666;
}

int main()
{
    pthread_t tid;
    int ret;
    void *arg;

    ret = pthread_create(&tid, NULL, tfun1, NULL);//创建子线程
    ret = pthread_join(tid, &arg);
    printf("1 pthread_join arg is %d\n",(int)arg);

    ret = pthread_create(&tid, NULL, tfun2, NULL);//创建子线程
    ret = pthread_join(tid, &arg);
    printf("2 pthread_join arg is %d\n",(int)arg);

    ret = pthread_create(&tid, NULL, tfun3, NULL);//创建子线程
    sleep(3);
    pthread_cancel(tid);
    ret = pthread_join(tid, &arg);
    printf("3 pthread_join arg is %d\n",(int)arg);//pthread_cancel杀死的
                                            //线程返回-1,可用pthread_join
                                            //接收
    pthread_exit(NULL);
}

结果:

2.7 线程终止的三种方式

总结:终止某个线程而不终止整个进程,有三种方法:

  1. 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit(结束当前进程)。
  2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
  3. 线程可以调用pthread_exit终止自己。

2.8 进程与线程控制原语对比

|--------|----------------|
| 进程 | 线程 |
| getpid | pthread_self |
| fork | pthread_create |
| wait | pthread_join |
| kill | pthread_cancle |
| exit | pthread_exit |

相关推荐
凹凸曼的大表哥9 小时前
基于凌鸥081ZYKFB开发板的编码器测转速算法学习
c语言·单片机·嵌入式硬件
眠りたいです19 小时前
现代C++:C++14中的新语言特性和库特性
c语言·开发语言·c++
ytttr8731 天前
OPC UA 协议栈 C 语言实现
c语言·开发语言·mfc
song5011 天前
Ascend C 算子开发:从入门到上手
c语言·开发语言·图像处理·人工智能·分布式·flutter·交互
小a杰.1 天前
Ascend C编程语言进阶:高性能算子开发技巧
android·c语言·开发语言
小a杰.1 天前
Ascend C算子开发实战 - 从零开始写算子
c语言·开发语言
我还记得那天1 天前
数组的2个应用举例
c语言·开发语言·二分查找·数组
学困昇1 天前
Linux IPC 详解:匿名管道、命名管道、共享内存与信号量
linux·运维·服务器·c语言·c++·人工智能
『昊纸』℃1 天前
作为小白,C语言如何从零开始呢
c语言·ide·学习·编程·教材