• 嵌入式Linux系统编程-》线程基本API+面试题+源码【第6天】


    活动地址:毕业季·进击的技术er

    1. 线程概念

    线程实际上是应用层的概念,在Linux内核中,所有的调度实体都被称为任务(task),他们之间的区别是:有些任务自己拥有一套完整的资源,而有些任务彼此之间共享一套资源,如下图所示。
    在这里插入图片描述


    上图中:

    左边是一个含有单个线程的进程,它拥有自己的一套完整的资源。
    右边是一个含有两条线程的进程,线程彼此间共享进程内的资源。
    由此可见,线程是一种轻量级进程,提供一种高效的任务处理方式。

    2. 基本接口

    2.1 线程的创建

    创建一条POSIX线程非常简单,只需指定线程的执行函数即可,但函数接口看起来比较复杂,细节如下:

    #include <pthread.h>
    
    int pthread_create(pthread_t *thread,
                       const pthread_attr_t *attr,
                       void *(*start_routine) (void *),
                       void *arg);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参数说明:

    `thread:新线程的TID
    attr:线程属性,若创建标准线程则该参数可设置为NULL
    start_routine:线程函数
    arg:线程函数的参数
    start_routine是一个函数指针,指向线程的执行函数,其参数和返回值都是 void *,使用示例代码如下:`
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // simpleThread.c
    #include <pthread.h>
    
    void *doSomething(void *arg)
    {
        // ...
    }
    
    int main()
    {
        // 创建一条线程,并让其执行函数 doSomething()
        pthread_t tid;
        pthread_create(&tid, NULL, doSomething, NULL);
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    线程的各种接口单独放在线程库中,因此在编译带线程的代码时,必须要指定链接线程库phread,如下:

    gec@ubuntu:~$ gcc simpleThread.c -o simpleThread -lpthread 
    
    • 1

    并发性

    线程最重要的特性是并发,线程函数 doSomething() 会与主线程 main() 同时运行,这是它与普通函数调用的根本区别。需要特别提醒的是,由于线程函数的并发性,在线程中访问共享资源需要特别小心,因为这些共享资源会被多个线程争抢,形成“竞态”。最典型的共享资源是全局变量,比如以下代码:

    // concurrency.c
    #include <pthread.h>
    
    int global = 100;
    
    void *isPrime(void *arg)
    {
        while(1)
        {
            // 一段朴素的代码
            if(global%2 == 0)
                printf("%d是偶数\n", global);
        }
    }
    
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, NULL, isPrime, NULL);
    
        // 一条人畜无害的赋值语句
        while(1)
            global = rand() % 5000;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行结果如下:

    gec@ubuntu:~$ ./concurrency
    
    • 1
    4383是偶数
    2777是偶数
    492是偶数
    492是偶数
    2362是偶数
    3690是偶数
    59是偶数
    3926是偶数
    540是偶数
    3426是偶数
    4172是偶数
    211是偶数
    368是偶数
    2567是偶数
    1530是偶数
    1530是偶数
    2862是偶数
    4067是偶数
    ...
    gec@ubuntu:~$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    可以看到结果错漏百出,原因就是因为线程之间的并发的,global随时都会被争抢,像这种多线程或多进程同时访问共享资源的情形,必须使用互斥锁、读写锁、条件量等同步互斥机制加以约束方可正常运行

    2.2 线程的退出

    与进程类似,当一条线程执行完毕其任务时,可以使用如下接口来退出:

    #include <pthread.h>
    
    void pthread_exit(void *retval);
    
    • 1
    • 2
    • 3

    其中,参数retval是线程的返回值,对应线程执行函数的返回值。若线程没有数据可返回则可写成NULL。

    注意此函数与exit的区别:

    pthread_exit(): 退出当前线程
    exit(): 退出当前进程(即退出进程中的所有线程)
    一个进程中各个线程是平行并发运行的,运行主函数main()的线程被称为主线程,主线程是可以比其他线程先退出的,比如:

    #include <pthread.h>
    
    void *count(void *arg)
    {
        // 循环数数
        for(int i=0; ;i++)
        {
            printf("%d\n", i);
            usleep(200*1000);
        }
    }
    
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, NULL, count, NULL);
    
        // 主线程先退出
        pthread_exit(NULL);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    }

    主线程退出后,其余线程可以继续运行,但请注意,上述代码中如果主线程不调用 pthread_exit() 的话,那么相当于退出了整个进程,则子线程也会被迫退出。

    2.3 线程的接合

    与进程类似,线程退出之后不会立即释放其所占有的系统资源,而会成为一个僵尸线程。其他线程可使用 pthread_join()来释放僵尸线程的资源,并可获得其退出时返回的退出值,该接口函数被称为线程的接合函数

    #include <pthread.h>
    
    int pthread_join(pthread_t tid, void **val);
    
    • 1
    • 2
    • 3

    接口说明:

    若指定tid的线程尚未退出,那么该函数将持续阻塞。 若只想阻塞等待指定线程tid退出,而不想要其退出值,那么val可置为NULL。
    若指定tid的线程处于分离状态,或不存在,则该函数会出错返回。
    需要注意的是,包括主线程在内,所有线程的地位是平等的,任何线程都可以先退出,任何线程也可以接合另外一条线程。以下是接合函数的简单应用示例:

    #include <pthread.h>
    
    void *routine(void *arg)
    {
        pthread_exit("abcd");
    }
    
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, NULL, routine, NULL);
    
        // 试图接合子线程,并获取其退出值
        void *val;
        pthread_join(tid, &val);
    
        printf("%d\n", (char *)val);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.4 其他

    2.4.1 获取线程TID

    如下接口可以获取线程的ID号:

    #include <pthread.h>
    
    pthread_t pthread_self(void);
    
    • 1
    • 2
    • 3

    以上接口类似进程管理中的 getpid(),需要注意的是,进程的PID是系统全局资源,而线程的TID仅限于进程内部的线程间有效。当我们要对某条线程执行诸如发送信号、取消、阻塞接合等操作时,需要用到线程的ID。

    2.4.2 线程错误码

    线程函数对系统错误码的处理跟标准C库函数的处理方式有很大不同,标准C库函数会对全局错误码 errno 进行设置,而线程函数发生错误时会直接返回错误码。


    以线程接合为例,若要判定接合是否成功,成功的情况下输出僵尸线程的退出值,失败的情况下输出失败的原因,那么实现代码应这么写:

    void *val;
    errno = pthread_join(tid, &val);
    
    if(errno == 0)
        printf("成功接合线程,其退出值为:%ld", (long)val);
    else
        printf("接合线程失败:%s\n", strerror(errno)); // 注意需包含头文件 string.h
        
    或:
    
    void *val;
    errno = pthread_join(tid, &val);
    
    if(errno == 0)
        printf("成功接合线程,其退出值为:%d", (int)val);
    else
        perror("接合线程失败");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    所有以pthread_xxx开头的线程函数,成功一律返回0,失败一律返回错误码。

    2.4.3 函数单例

    许多时候,我们希望某个函数只被严格执行一次,这种需求在一些初始化功能模块中尤为常见。

    考虑这么一种情形:

    假设某程序内含多条线程,这些线程使用信号量(不管是system-V信号量组还是POSIX信号量)进行协同合作,由于信号量使用前必须进行初始化,为了使程序性能最优,我们希望线程们启动时谁跑得快谁就对信号量执行初始化的工作,且要确保初始化的工作被严格执行一遍。


    在上述情形中,由于线程的并发特性,我们无法预先知晓哪条线程会对信号量进行初始化,于是就希望有一种只执行一遍的函数单例,可以被众多的并发线程放心去调用。这种机制可以用如下函数达成:

    #include <pthread.h>
    
    // 函数单例控制变量
    pthread_once_t once_control = PTHREAD_ONCE_INIT;
    
    // 函数单例启动接口
    int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    接口说明:

    once_control是一种特殊的变量,用来关联某个函数单例,被关联的函数单例只会被执行一遍。
    init_routine函数指针指向的函数就是只执行一遍的函数单例。 以下是示例代码:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    
    #include <pthread.h>
    
    // 函数单例控制变量
    pthread_once_t   once_control = PTHREAD_ONCE_INIT;
    
    void init_routine(void)
    {
        printf("我会被严格执行一遍。\n");
    }
    
    
    void *f(void *arg __attribute__((unused)))
    {
        pthread_once(&once_control, init_routine);
        pthread_exit(NULL);
    }
    
    int main()
    {
        pthread_t tid;
    
        for(int i=0; i<20; i++)
        	pthread_create(&tid, NULL, f, NULL);
    
        pthread_exit(NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    3. 线程的属性

    3.1 查看线程属性

    线程有许多属性,可以在终端中查看跟线程属性相关的函数:

    # 敲入如下命令后连续按两下tab键
    gec@ubuntu:~$ man pthread_attr_
    pthread_attr_destroy          pthread_attr_getschedpolicy   pthread_attr_setaffinity_np   pthread_attr_setscope
    pthread_attr_getaffinity_np   pthread_attr_getscope         pthread_attr_setdetachstate   pthread_attr_setstack
    pthread_attr_getdetachstate   pthread_attr_getstack         pthread_attr_setguardsize     pthread_attr_setstackaddr
    pthread_attr_getguardsize     pthread_attr_getstackaddr     pthread_attr_setinheritsched  pthread_attr_setstacksize
    pthread_attr_getinheritsched  pthread_attr_getstacksize     pthread_attr_setschedparam    
    pthread_attr_getschedparam    pthread_attr_init             pthread_attr_setschedpolicy   
    gec@ubuntu:~$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可见,线程的属性多种多样,可以归总为如下表格:
    在这里插入图片描述

    这些属性可以在创建线程的时候,通过属性变量统一设置,有少部分可以在线程运行之后再进行设置(比如分离属性),下面介绍属性变量如何使用。

    3.2 属性变量的使用

    由于线程属性众多,因此需要的时候不直接设置,而是先将它们置入一个统一的属性变量中,然后再以此创建线程。属性变量是一种内置数据类型,需要用如下函数接口专门进行初始化和销毁

    #include <pthread.h>
    
    int pthread_attr_init(pthread_attr_t *attr);
    int pthread_attr_destroy(pthread_attr_t *attr);
    
    • 1
    • 2
    • 3
    • 4

    线程属性的一般使用步骤:

    定义且初始化属性变量 attr 将所需的属性,加入 attr 中 使用 attr
    启动线程
    销毁 attr

    示例代码:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    
    #include <pthread.h>
    
    void *routine(void *arg __attribute__((unused)))
    {
        sleep(1);
    }
    
    int main()
    {
        // 初始化属性变量,并将分离属性添加进去
        pthread_attr_t  attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
        // 以分离属性启动线程
        pthread_t tid;
        pthread_create(&tid, &attr, routine, NULL);
    
        // 分离的线程无法接合
        if((errno=pthread_join(tid, NULL)) != 0)
            perror("接合线程失败");
    
        pthread_exit(NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    4. 分离属性

    4.1 僵尸线程

    默认情况下,线程启动后处于可接合状态(即未分离),此时的线程可以在退出时让其他线程接合以便释放资源,但若其他线程未及时调用
    pthread_join() 去接合它,它将成为僵尸线程,浪费系统资源。
    在这里插入图片描述

    因此,若线程退出时无需汇报其退出值,则一般要设置为分离状态,处于分离状态下的线程在退出之后,会自动释放其占用的系统资源。

    将线程设置为分离状态有两种方式:

    `在线程启动前,使用分离属性启动线程
    在线程启动后,使用 pthread_detach() 强制分离`
    
    • 1
    • 2

    4.2 分离与接合

    在线程启动前,使用分离属性启动线程做法如下:

    #include <pthread.h>
    
    void *routine(void *arg)
    {
        // ...
    }
    
    int main()
    {
        // 初始化属性变量,并将分离属性添加进去
        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
        // 以分离属性启动线程
        pthread_t tid;
        pthread_create(&tid, &attr, routine, NULL);
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    注意:

    分离状态下的线程是无法被接合的。


    在线程启动后,使用 pthread_detach() 强制分离的做法如下:

    #include <pthread.h>
    
    void *routine(void *arg)
    {
        // 强制将自身设置为分离状态
        pthread_detach(pthread_self());
    
        // ...
    }
    
    int main()
    {
        // 启动标准线程
        pthread_t tid;
        pthread_create(&tid, NULL, routine, NULL);
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5.问题

    问:老师,下面的代码为什么有时成功,有时失败?

    #include <pthread.h>
    
    void *routine(void *arg)
    {
        // 将自身强制分离,然后退出
        pthread_detach(pthread_self());
        pthread_exit("abcd");
    }
    
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, NULL, routine, NULL);
    
        char *s;
        if((errno=pthread_join(tid, (void *)&s)) != 0)
            perror("接合线程失败");
        else
            printf("接合线程成功:%s\n", s);
    
        pthread_exit(NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    答:线程是并发的,并且是无序的。上述代码中接合线程的成功与否取决于 pthread_detach() 和 pthread_join()
    谁先被执行,而这原则上是不确定的,因此程序的结果也是不确定的。


    6.面试题

    编写一个程序,让主线程先退出并返回一个值,子线程接合主线程后输出主线程的退出值。

    解答:

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    void *routine(void *arg)
    {
        // 试图接合主线程,并获取其退出值
        void *val;
        pthread_t mid = *((pthread_t *)arg);
        pthread_join(mid, &val);
    
        printf("%s\n", (char *)val);
    }
    
    int main()
    {
        pthread_t tid;
        pthread_t mid = pthread_self();
        pthread_create(&tid, NULL, routine, (void *)&mid);
    
        // 退出主线程
        pthread_exit("abcd");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    活动地址:毕业季·进击的技术er

  • 相关阅读:
    公司组织架构图怎么制作?
    亚马逊FBA头程物流选择注意事项有哪些?
    Python算法和数据结构面试指南
    2023年9月青少年软件编程(C 语言) 等级考试试卷(五级)
    数字化转型导师鹏:政府数字化转型政务服务类案例研究
    React 第五章 表单
    【图论——第七讲】Pirm算法求最小生成树问题及其堆优化
    01-mysql5.7安装部署-二进制安装
    网络编程套接字,Linux下实现echo服务器和客户端
    如何在自动化测试中使用MitmProxy获取数据返回?
  • 原文地址:https://blog.csdn.net/m0_45463480/article/details/125466813