Linux多线程编程(10分钟入门)
程序并行的常用实现方式有两种,分别叫做“多进程编程”和“多线程编程”。本节,我们教大家如何在 Linux 下进行多线程编程。并发和并行都指的是计算机可以同时执行多个任务,但严格来讲,它们是有区别的,只是本节不对它们做更细致的区分。
程序、进程和线程
学习多线程编程的实现方法之前,首先要搞清楚什么是线程,这就要从程序、进程和线程三者的关系和区别讲起。大家常常编写程序,程序其实就是一系列指令(代码)的集合,我们通常将它编写在一个或者多个文件中。例如,C 语言程序通常编写在后缀名为 .c 的文件中,Python 程序编写在后缀名为 .py 的文件中,我们通常将存有程序的文件称为“源文件”。
程序以源文件的方式存储在外存(比如硬盘、U盘等)中,只有运行的时候才会被载入内存。对于支持并行的操作系统来说,必须为每一个运行的程序分配所需的资源(内存空间、输入输出设备等),并确保同时运行的程序之间不会相互干扰,为此,操作系统将每一个运行着的程序视为一个进程:
- 操作系统以进程为单位,为每个进程分配执行所需要的资源;
- 原则上,各个进程之间不允许访问对方的资源;
- 操作系统实时监控着每个进程的执行状态,必要时可以强制其终止执行。
也就是说在操作系统看来,每个载入内存执行的程序都是一个进程。操作系统以进程为单位分配资源,各个进程相互独立,执行过程互不干扰。
同一时间,操作系统可以运行多个应用程序(进程),每个应用程序(进程)还可以同时执行多个任务,例如迅雷支持同时下载多个文件,QQ 也支持同时和多个好友聊天。同一进程中,执行的每个任务都被视为一个线程。
线程和进程之间的关系,与工厂和工人之间的关系非常相似。一个进程好比是一座工厂,一个线程就如同这个工厂中的一个工人。工厂可以容纳多个工人,每个工人负责完成一项具体的任务。工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源。
也就是说,一个进程中可以包含多个线程,所有线程共享进程拥有的资源。当然,每个线程也可以拥有自己的私有资源。下图给您展示进程和线程之间的关系:
图 1 进程和线程的关系
如图 1 所示,所有线程共享的进程资源有:
- 代码:即应用程序的代码;
- 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
- 进程空间:操作系统分配给进程的内存空间;
- 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。
各个线程也可以拥有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等。
多线程编程的实现方法
了解了程序、进程和线程之间的关系后,多线程的含义就很容易理解了,它指的是一个进程中拥有多个(≥2)线程。通常,我们将编写多线程程序的过程称为“多线程编程”。Linux 上编写多线程程序,可以借助 <pthread.h> 头文件提供的一些函数,常用的函数有如下几个:本文的目标立足于教会大家编写入门级别的多线程程序,有关线程同步、线程死锁、线程属性等内容,建议您转至《多线程编程(C语言+Linux)》专题做系统的学习。
1) pthread_create()
pthread_create() 函数专门用来创建线程,语法格式如下:
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
- thread:接收一个 pthread_t 类型变量的地址,每个 pthread_t 类型的变量都可以表示一个线程。
- attr:手动指定新线程的属性,我们可以将其置为 NULL,表示新建线程遵循默认属性。
- start_routine:以函数指针的方式指明新建线程需要执行哪个函数。
- arg:向 start_routinue() 函数的形参传递数据。将 arg 置为 NULL,表示不传递任何数据。
如果成功创建线程,pthread_create() 函数返回数字 0,否则返回一个非零值。各个非零值都对应着不同的宏,指明创建失败的原因,常见的宏有以下几种:
- EAGAIN:系统资源不足,无法提供创建线程所需的资源。
- EINVAL:传递给 pthread_create() 函数的 attr 参数无效。
- EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,程序没有相关的设置权限。
以上这些宏都定义在 <errno.h> 头文件中,如果想使用这些宏,需提前引入此头文件。
有关 pthread_create() 函数更详细的讲解,请阅读《创建线程》一文。
2) pthread_exit()
pthread_exit() 函数用于终止线程执行,语法格式如下:void pthread_exit(void *retval);
retval 参数指向的数据将作为线程执行结束时的返回值,如果不需要返回任何数据,将其置为 NULL 即可。注意,retval 不能指向函数内部的局部变量,否则会导致程序运行出错甚至崩溃。return 也可以终止线程执行,它和 pthread_exit() 之间有什么区别呢?我们已经在《终止线程(3种方法)》一文给出了答案。
3) pthread_cancel()
在多线程程序中,一个线程可以借助 pthread_cancel() 函数向另一个线程发送“终止执行”的信号。pthread_cancel() 函数的语法格式如下:
int pthread_cancel(pthread_t thread);
thread 参数用于指定接收信号的目标线程。当成功发送“终止执行”的信号时,函数返回值为 0,否则返回非零数。再次强调,pthread_cancel() 函数只是向目标线程发送“终止执行”的信息,至于目标线程是否接收此信号,以及何时终止执行,由目标线程说了算,我们会在《终止线程执行,千万别踩这个坑!》一文做详细了解。
4) pthread_join()
pthread_join() 函数的功能主要有两个,分别是:- 接收目标线程执行结束时的返回值;
- 释放目标线程占用的进程资源。
pthead_join() 函数的语法格式如下:
int pthread_join(pthread_t thread, void ** retval);
thread 参数用于指定目标线程;retval 参数用于存储接收到的返回值。实际场景中,调用 pthread_join() 函数可能仅是为了及时释放目标线程占用的资源,并不想接收它的返回值,这种情况下可以将 retval 置为 NULL。pthread_join() 函数会一直阻塞当前线程,直至目标线程执行结束,阻塞状态才会消除。如果成功等到了目标线程执行结束(成功获取到目标线程的返回值),pthread_join() 函数返回数字 0,否则返回非零数。
想全方位搞清楚 pthread_join() 函数的功能和用法,可阅读《获取线程函数的返回值》一文。
第一个多线程程序
接下来,我们利用上文学到的知识,编写第一个多线程程序:#include <stdio.h> #include <pthread.h> //定义线程要执行的函数,arg 为接收线程传递过来的数据 void* Thread1(void* arg) { printf("http://www.weixueyuan.net\n"); return "Thread1成功执行"; } //定义线程要执行的函数,arg 为接收线程传递过来的数据 void* Thread2(void* arg) { printf("魏雪原\n"); return "Thread2成功执行"; } int main() { int res; //创建两个线程变量 pthread_t mythread1, mythread2; void* thread_result; //创建 mythread1 线程,执行 Thread1() 函数 res = pthread_create(&mythread1, NULL, Thread1, NULL); if (res != 0) { printf("线程创建失败"); return 0; } //创建 mythread2 线程,执行 Thread2() 函数 res = pthread_create(&mythread2, NULL, Thread2, NULL); if (res != 0) { printf("线程创建失败"); return 0; } //阻塞主线程,直至 mythread1 线程执行结束,用 thread_result 指向接收到的返回值,阻塞状态才消除。 res = pthread_join(mythread1, &thread_result); //输出线程执行完毕后返回的数据 printf("%s\n", (char*)thread_result); //阻塞主线程,直至 mythread2 线程执行结束,用 thread_result 指向接收到的返回值,阻塞状态才消除。 res = pthread_join(mythread2, &thread_result); printf("%s\n", (char*)thread_result); printf("主线程执行完毕"); return 0; }程序中共有 3 个线程,分别是主线程,mythread1 线程和 mythread2 线程。mythread1 线程负责执行 Thread1() 函数,mythread2 线程负责执行 Thread2() 函数。
主线程先后调用了两次 pthread_join() 函数,都会阻塞主线程,直至 mythread1 和 mythread2 线程执行完毕,阻塞状态才会消除。
假设程序存储在 thread.c 文件中,调用 GCC 编译此程序:
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
最终会生成一个名为 thread.exe 的可执行文件,执行如下命令即可看到执行结果:
[root@localhost ~]# ./thead.exe
http:www.weixueyuan.net
魏雪原
Thread1成功执行
Thread2成功执行
主线程执行完毕
总结
本节,我们了解了程序、进程和线程三者之间的关系,学会了如何编写一个简单的多线程程序。但是,与多线程编程相关的知识还有很多,比如实现线程同步,解决线程死锁问题、自定义线程的属性等,这些知识我们会在《多线程编程(C语言+Linux)》专题中给大家做详细的讲解。