活动地址:毕业季·进击的技术er
嵌入式Linux系统编程-》线程基本API+面试题+源码【第6天】
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