Linux学习日记19——线程

学习视频链接 

黑马程序员-Linux系统编程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1KE411q7ee?p=151&spm_id_from=333.1007.top_right_bar_window_history.content.click

目录

一、线程概念

1.1 什么是线程

1.2 查看火狐浏览器的线程

1.3 Linux内核线程实现原理

1.4 线程共享资源

1.5 线程非共享资源

1.6 线程优、缺点

二、进程控制原语

2.1 pthread_self 函数

2.2 pthread_create 函数

2.3 测试代码


一、线程概念

1.1 什么是线程

LWP:light weight process 轻量级的进程,本质仍是进程(在 Linux 环境下)

进程:独立地址空间,拥有 PCB

线程:有独立的 PCB,但没有独立的地址空间(共享)

区别:在于是否共享地址空间 独居(进程);合租(线程)

Linux下:

线程:最小的执行单位

进程:最小分配资源单位,可看成是只有一个线程的进程

进程创建线程,每个线程都有 PCB,原来的进程变成线程了

1.2 查看火狐浏览器的线程

 进程 id 是同一个,进程号有多少,共有 22 个

1.3 Linux内核线程实现原理

类 Unix 系统中,早期是没有“线程”概念的,80 年代才引入(应减少信号和线程混用),借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切

1、轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone

2、从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表(参考操作系统分页式存储)是相同的

3、进程可以蜕变成线程

4、线程可看做寄存器和栈的集合

5、在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位

察看 LWP 号:ps -Lf pid 查看指定线程的 lwp 号

1.4 线程共享资源

1、文件描述符表

2、每种信号的处理方式

3、当前工作目录

4、用户 ID 和组 ID

5、内存地址空间(.text/ .data/ .bss/heap/共享库)(不贡献栈)

1.5 线程非共享资源

1、线程 ID

2、处理器现场和栈指针(内核栈)

3、独立的栈空间(用户空间栈)

4、errno 变量

5、信号屏蔽字

6、调度优先级

1.6 线程优、缺点

1、优点

提高程序并发性,开销小,数据通信、共享数据方便

2、缺点

库函数、不稳定,调试、编写困难、GDB不支持,对信号支持不好

优点相对凸出,缺点不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大

二、进程控制原语

2.1 pthread_self 函数

1、作用

获取线程 ID。其作用对应进程中 getpid() 函数

2、pthread_t pthread self(void);

返回值:成功:0;失败:无

3、线程 ID

pthread_t 类型,本质:在Linux下为无符号整数 (%lu),其他系统中可能是结构体实现

线程 ID 是进程内部 识别标志。(两个进程间,线程 ID 允许相同)

4、注意

不应使用全局变量 pthread_t tid;  在子线程中通过 pthread_create 传出参数来获取线程 ID,而使用 pthread_self

2.2 pthread_create 函数

1、作用

创建一个新线程    其作用,对应进程中 fork() 函数

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

返回值:成功:0;   失败:错误号  —— Linux 环境下,所有线程特点,失败均直接返回错误号

参数:

参数1:传出参数,保存系统为我们分配好的线程 ID

参数2:通常传 NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数

参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束

参数4:线程主函数执行期间所使用的参数

2.3 测试代码

1、查看父进程的线程 id

2、创建线程,主线程执行 main 函数,创建的线程执行 tfn

3、循环创建多个子线程

4、错误写法

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}


void *tfn(void *arg)
{
    int i = *((int *)arg);
    sleep(i);
    printf("I'm  %dth thread: pid = %d, tid = %lu\n", i + 1, getpid(), pthread_self());
    return NULL;
}

int main(int argc, char *argv[]) 
{
    int i;
    int ret;
    pthread_t tid;

    for (int i = 0; i < 5; i++) {
        ret = pthread_create(&tid, NULL, tfn, (void*)&i);
        if (ret != 0) {
            perror("pthread_create error");
        }   
    }   

    sleep(5);
    printf("I'm main thread: pid = %d, tid = %lu\n", getpid(), pthread_self());

    return 0;
}

原因:地址传递和值传递的效果不一样,线程传参要用值传递而不是地址传递。其中要借助两次强制类型转换 int(4字节) —> char(8字节) —> int(4字节)

5、线程间全局变量共享

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

int var = 100;

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void *tfn(void *arg)
{
    var = 200;
    printf("In thread, change var = %d\n", var);

    return NULL;
}

int main(void) 
{
    printf("At first var = %d\n", var);

    pthread_t tid;
    pthread_create(&tid, NULL, tfn, NULL);
    sleep(1);

    printf("After pthread_create var = %d\n", var);

    return 0;
}

2.4 pthread_exit

1、作用

将单个线程退出

2、以前代码存在的问题

(1) 我们想退出某个线程,使用 exit 会退出整个进程

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void *tfn(void *arg)
{
    int i =(int)arg;
    sleep(i);
        
    if (i == 2) {
        exit(0);
    }   
    printf("I'm %dth thread: pid = %d, tid = %lu\n", i + 1, getpid(), pthread_self());

    return NULL;
}

int main(void) 
{
    int i;
    int ret;
    pthread_t tid;

    for (i = 0; i < 5; i++) {
        ret = pthread_create(&tid, NULL, tfn, (void *)i);
        if (ret != 0) {
            sys_err("pthread_create error");
        }   
    }   

    sleep(5);

    return 0;
}

(2) return 能达到退出线程的目的 

但是在函数调用内使用 return 是达不到这样的效果的

 

3、void pthread_exit(void *retval);

参数:retval 表示线程退出状态,通常传 NULL

在不添加 sleep 控制输出顺序的情况下。pthread_create 在循环中,几乎瞬间创建 5 个线程,但只有第 1 个线程有机会输出(或者第 2 个也有,也可能没有,取决于内核调度)如果第 3 个线程执行了 exit,将整个进程退出了,所以全部线程退出了

所以,多线程环境中,应尽量少用,或者不使用 exit 函数,取而代之使用 pthread_exit 函数,将单个线程退出。任何线程里 exit 导致进程退出,其他线程未工作结束,主控线程退出时不能 return 或 exit

另注意,pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

4、代码

现在主线程不用睡眠了,只退出主线程就可以了,子线程不受影响

 

5、总结

在主函数里面执行 return 和 exit 效果是一样的

return:回到调用者那里去

exit:退出当前进程

pthread_exit():退出当前进程

2.5 pthread_join函数

1、作用

阻塞等待线程退出,获取线程退出状态其作用, 对应进程中 waitpid() 函数

2、int pthread_join(pthread_t thread, void **retval);

成功:0;失败:错误号

参数:thread:线程 ID([注意]:不是指针);retval:存储线程结束状态

和 wait 类比,wait 退出值是 int 所以传出参数是 int *,线程返回值是 void * 所以传出参数是 void **

3、代码

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

struct thrd {
    int  var;
    char str[256];
};

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void *tfn(void *arg)
{
    struct thrd *tval;
    tval = malloc(sizeof(tval));
    tval->var = 100;
    strcpy(tval->str, "hello thread");

    return (void*)tval;
}

int main(void) 
{
    pthread_t tid;
    struct thrd *retval;

    int ret = pthread_create(&tid, NULL, tfn, NULL);
    if (ret != 0) {
        sys_err("pthread_create error");
    }

    ret = pthread_join(tid, (void **)&retval);
    if (ret != 0) {
        sys_err("phread_join error");
    }

    printf("child thread exit with var = %d, str = %s\n", retval->var, retval->str);

    pthread_exit(NULL);  // 将当前线程退出
}

2.6 pthread_cancel函数

1、作用

杀死(取消)线程。        其作用,对应进程中 kil() 函数

2、int pthread_cancel(pthread_t thread);

成功:0;失败:错误号

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

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用 creat,open,pause,close,read,write .... 执行命令 man 7 pthreads 可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节

可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调pthread_testcancel 函数自行设置一个取消点

被取消的线程,退出值定义在 Linux 的 pthread 库中。常数 PTHREAD_CANCELED 的值是 -1 可在头文件 pthread.h 中找到它的定义:#define PTHREAD_CANCELED(void *) -1)。因此当我们对一个已经被取消的线程使用 pthread_join 回收时,得到的返回值为 -1

3、代码

(1) 杀死子进程演示

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void *tfn(void *arg)
{
    while (1) {
        printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
        sleep(1);
    }
    
    return NULL;
}

int main(void) 
{
    pthread_t tid;

    int ret = pthread_create(&tid, NULL, tfn, NULL);
    if (ret != 0) {
        sys_err("pthread_create error");
    }

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

    sleep(5);    

    ret = pthread_cancel(tid);
    if (ret != 0) {
        sys_err("phread_join error");
    }

    while(1);

    pthread_exit(NULL);
}

(2) 测试被杀死的子进程的返回值

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

void *tfn1(void *arg)
{
    printf("thread 1 returning\n");

    return (void *)111;
}

void *tfn2(void *arg)
{
    printf("thread 2 exiting\n");

    pthread_exit((void *)222);
}

void *tfn3(void *arg)
{
    while(1) {
        printf("thread 3 : I'm going to die in 3 seconds ...\n");
        sleep(1);
    }   
    return (void *)111;
}


int main(void)
{
    pthread_t tid;
    void *tret = NULL;

    pthread_create(&tid, NULL, tfn1, NULL);
    pthread_join(tid, &tret);
    printf("thread 1 exit code = %d\n\n", (int)tret);

    pthread_create(&tid, NULL, tfn2, NULL);
    pthread_join(tid, &tret);
    printf("thread 2 exit code = %d\n\n", (int)tret);

    pthread_create(&tid, NULL, tfn3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &tret);
    printf("thread 3 exit code = %d\n", (int)tret);

    return 0;
}

(3) 杀死一个进程需要一个契机(进入内核)

现在处于死循环进入不了内核

 需要有系统调用或者手动添加一个取消点

 

2.7 pthread_detach 函数

1、作用

实现线程分离

2、int pthread_detach(pthread_t thread);

成功:0;失败:错误号

3、线程分离状态

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

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

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

5、代码

(1) 以前查看错误的方法

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

void *tfn(void *arg)
{
    printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());

    return NULL;
}

int main(void) 
{
    pthread_t tid;

    int ret = pthread_create(&tid, NULL, tfn, NULL);
    if (ret != 0) {
        perror("pthread_create error");
    }   

    ret = pthread_detach(tid);
    if (ret != 0) {
        perror("phread_join error");
    }   

    sleep(1);

    ret = pthread_join(tid, NULL);
    printf("join ret = %d\n", ret);
    if (ret != 0) {
        perror("pthread_join error");
    }   

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

    pthread_exit(NULL);
}

很明显没看出有啥错误,但是报错了 

(2) 现在查看错误的方式

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

void *tfn(void *arg)
{
    printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());

    return NULL;
}

int main(void) 
{
    pthread_t tid;

    int ret = pthread_create(&tid, NULL, tfn, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(1);
    }   

    ret = pthread_detach(tid);  // 设置线程分离 子线程终止,会自动清理PCB,无需回收
    if (ret != 0) {
        fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
        exit(1);
    }   

    sleep(1);

    ret = pthread_join(tid, NULL);
    printf("join ret = %d\n", ret);
    if (ret != 0) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(1);
    }

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

    pthread_exit((void*)0);
}

错误原因是无效的参数,

错误原因:子线程分离了,线程自己把所有的东西都回收了,再用main线程回收就错误了。 

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

三、线程属性

3.1 简介

本节作为指引性介绍,linux 下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数

typedef struct
{
        int etachstate;     // 线程的分离状态
        int schedpolicy;    // 线程调度策略
        struct sched_param schedparam; // 线程的调度参数。
        int inheritsched;   // 线程的继承性
        int scope;          // 线程的作用域
        size_t guardsize;   // 线程栈末尾的警戒缓冲区大小
        int stackaddr_set;  // 线程的栈设置
        void* stackaddr;    // 线程栈的位置。
        size_t stacksize;   // 线程栈的大小。
} pthread_attr_t;

3.2 流程

设置分离属性

pthread_attr_t attr

创建一个线程属性结构体变量

pthread_attr_init(&attr);  初始化线程属性

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);  设置线程属性为分离态

pthread_create(&tid, attr, tfn, NULL);  借助修改后的设置线程属性创建为分离态的新线程

pthread_attr_init(&attr);  销毁线程属性

3.3 代码

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

void *tfn(void *arg)
{
    printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());

    return NULL;
}

int main(void) 
{
    pthread_t tid;
    pthread_attr_t attr;

    int ret = pthread_attr_init(&attr);
    if (ret != 0) {
        fprintf(stderr, "attr_init error: %s\n", strerror(ret));
        exit(1);
    }   

    ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (ret != 0) {
        fprintf(stderr, "pthread_setdetachstate error: %s\n", strerror(ret));
        exit(1);
    }   

    ret = pthread_create(&tid, &attr, tfn, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(0);
    }   

    sleep(1);

    ret = pthread_attr_destroy(&attr);
    if (ret != 0) {
        fprintf(stderr, "attr_destroy error: %s\n", strerror(ret));
        exit(1);
    }

    ret = pthread_join(tid, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_jerror error: %s\n", strerror(ret));
        exit(1);
    }

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

    pthread_exit((void*)0);
}

四、注意事项

1、主线程退出其他线程不退出,主线程应调用 pthread_exit

2、避免僵尸线程

pthread_join

pthread_detachs

pthread_create 指定分离属性

被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

3、malloc 和 mmap 申请的内存可以被其他线程释放

4、应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit

 

5、信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

herb.dr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值