1. 线程的创建
1.1 创建线程
启动程序时,创建的进程只是一个单线程的进程,称之为初始线程或主线程,本小节我们讨论如何创建一个新的线程。
创建线程与创建进程的方法是一样的,让我们来看一下创建线程的函数:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
我们来解读一下参数的含义:
参数 | 含义 |
---|---|
thread | pthread_t类型指针,当pthread_create()成功返回时,新创建的线程的线程ID会保存在参数thread |
attr | pthread_attr_t类型指针,指向pthread_attr_t类型的缓冲区,pthread_attr_t数据类型定义了线程的各种属性,如果将参数attr设置为NULL,那么表示将线程的所有属性设置为默认值,以此创建新线程。 |
start_routine | 参数start_routine是一个函数指针,指向一个函数,新创建的线程从start_routine()函数开始运行,该函数返回值类型为void*,并且该函数的参数只有一个void*,其实这个参数就是pthread_create()函数的第四个参数arg |
arg | 传递给start_routine()函数的参数。一般情况下,需要将arg指向一个全局或堆变量,意思就是说在线程的生命周期中,该arg指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数arg设置为NULL,表示不需要传入参数给start_routine()函数。 |
返回值 | 成功返回0;失败时将返回一个错误号,并且参数thread指向的内容是不确定的。 |
注意pthread_create()在调用失败时通常会返回错误码,它并不像其它库函数或系统调用一样设置errno,每个线程都提供了全局变量errno的副本,这只是为了与使用errno到的函数进行兼容,在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局变量,这样可以把错误的范围限制在引起出错的函数中。
线程创建成功,新线程就会加入到系统调度队列中,获取到CPU之后就会立马从start_routine()函数开始运行该线程的任务;调用pthread_create()函数后,通常我们无法确定系统接着会调度哪一个线程来使用CPU资源,先调度主线程还是新创建的线程呢(而在多核CPU或多CPU系统中,多核线程可能会在不同的核心上同时执行)? 如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现。这与前面学习父、子进程时也出现了这个问题,无法确定父进程、子进程谁先被系统调度。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
return (void *)0;
}
int main(void)
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret)
{
fprintf(stderr, "Error: %s\n", strerror(ret));
exit(-1);
}
printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
sleep(1);
exit(0);
}
应该将pthread_t作为一种不透明的数据类型加以对待,但是在示例代码中需要打印线程ID,所以要明确其数据类型,示例代码中使用了printf()函数打印线程ID时,将其作为unsignedlongint数据类型,在Linux 系统下,确实是使用unsignedlongint来表示pthread_t,所以这样做没有问题!
主线程休眠了1秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创建的线程还没有机会运行,整个进程就结束了。
在主线程和新线程中,分别通过getpid()和pthread_self()来获取进程ID和线程ID,将结果打印出来,运行结果如下所示:
编译时出现了错误,提示“对‘pthread_create’未定义的引用”,示例代码确实已经包含了<pthread.h>头文件,但为什么会出现这样的报错,仔细看,这个报错是出现在程序代码链接时、而并非是编译过程,所以可知这是链接库的文件,如何解决呢?
从打印信息可知,正如前面所介绍那样,两个线程的进程ID相同,说明新创建的线程与主线程本来就属于同一个进程,但是它们的线程ID不同。从打印结果可知,Linux系统下线程ID数值非常大,看起来像是一个指针。
2. 线程ID
2.1 不可或缺的线程ID
就像每个进程都有一个进程ID一样,每个线程也有其对应的标识,称为线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
进程ID使用pid_t数据类型来表示,它是一个非负整数。而线程ID使用pthread_t数据类型来表示,一个线程可通过库函数pthread_self()来获取自己的线程ID,其函数原型如下所示:
#include <pthread.h>
pthread_t pthread_self(void);
3. 终止线程
3.1 如何终止线程
在新线程的启动函数(线程 start 函数)new_thread_start()通过 return 返回之后,意味着该线程已经终止了,除了在线程 start 函数中执行 return 语句终止线程外,终止线程的方式还有多种,可以通过如下方式终止线程的运行:
- 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
- 线程调用 pthread_exit()函数;
- 调用 pthread_cancel()取消线程
tips:这里万分注意的是,慎在线程中使用exit()、_exit()或者_Exit(),否则,你使用的整个程序都会退出,后果看个人的使用场景吧!
我们来看一下pthread_exit()函数将终止调用它的线程,其函数原型如下所示:
#include <pthread.h>
void pthread_exit(void *retval);
参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用 pthread_join()来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join()来获取的。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
printf("新线程 start\n");
sleep(1);
printf("新线程 end\n");
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret)
{
fprintf(stderr, "Error: %s\n", strerror(ret));
exit(-1);
}
printf("主线程 end\n");
pthread_exit(NULL);
exit(0);
}
正如上面介绍到,主线程调用 pthread_exit()终止之后,整个进程并没有结束,而新线程还在继续运行。所以进程里的那几个退出函数,大家一定要注意使用呀,可能有时候程序莫名其妙退出,但是检查半天,又能编译成功,但是就是运行不完整程序,会花费大量的时间!
2023年了,祝大家新年快乐!
本文参考正点原子的嵌入式LinuxC应用编程。