嵌入式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);

参数说明:

`thread:新线程的TID
attr:线程属性,若创建标准线程则该参数可设置为NULL
start_routine:线程函数
arg:线程函数的参数
start_routine是一个函数指针,指向线程的执行函数,其参数和返回值都是 void *,使用示例代码如下:`
// simpleThread.c
#include <pthread.h>

void *doSomething(void *arg)
{
    
    
    // ...
}

int main()
{
    
    
    // 创建一条线程,并让其执行函数 doSomething()
    pthread_t tid;
    pthread_create(&tid, NULL, doSomething, NULL);

    // ...
}

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

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

并发性

线程最重要的特性是并发,线程函数 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;
}

运行结果如下:

gec@ubuntu:~$ ./concurrency
4383是偶数
2777是偶数
492是偶数
492是偶数
2362是偶数
3690是偶数
59是偶数
3926是偶数
540是偶数
3426是偶数
4172是偶数
211是偶数
368是偶数
2567是偶数
1530是偶数
1530是偶数
2862是偶数
4067是偶数
...
gec@ubuntu:~$ 

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

2.2 线程的退出

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

#include <pthread.h>

void pthread_exit(void *retval);

其中,参数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);

}

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

2.3 线程的接合

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

#include <pthread.h>

int pthread_join(pthread_t tid, void **val);

接口说明:

若指定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);
}

2.4 其他

2.4.1 获取线程TID

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

#include <pthread.h>

pthread_t pthread_self(void);

以上接口类似进程管理中的 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("接合线程失败");

所有以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));

接口说明:

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);
}

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:~$ 

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

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

3.2 属性变量的使用

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

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

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

定义且初始化属性变量 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);
}

4. 分离属性

4.1 僵尸线程

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

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

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

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

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);

    // ...
}

注意:

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


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

#include <pthread.h>

void *routine(void *arg)
{
    
    
    // 强制将自身设置为分离状态
    pthread_detach(pthread_self());

    // ...
}

int main()
{
    
    
    // 启动标准线程
    pthread_t tid;
    pthread_create(&tid, NULL, routine, NULL);

    // ...
}

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);
}

答:线程是并发的,并且是无序的。上述代码中接合线程的成功与否取决于 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");
}

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

猜你喜欢

转载自blog.csdn.net/m0_45463480/article/details/125466813