Linux多线程编程(10分钟入门)

如今,几乎所有的电脑(操作系统)都支持同时执行多个任务,比如一边用迅雷下载资源,一边听歌,一边用 QQ 和好友聊天,这样的执行方式简称“并发”或者“并行”。

并发和并行都指的是计算机可以同时执行多个任务,但严格来讲,它们是有区别的,只是本节不对它们做更细致的区分。

程序并行的常用实现方式有两种,分别叫做“多进程编程”和“多线程编程”。本节,我们教大家如何在 Linux 下进行多线程编程。

程序、进程和线程

学习多线程编程的实现方法之前,首先要搞清楚什么是线程,这就要从程序、进程和线程三者的关系和区别讲起。

大家常常编写程序,程序其实就是一系列指令(代码)的集合,我们通常将它编写在一个或者多个文件中。例如,C 语言程序通常编写在后缀名为 .c 的文件中,Python 程序编写在后缀名为 .py 的文件中,我们通常将存有程序的文件称为“源文件”。

程序以源文件的方式存储在外存(比如硬盘、U盘等)中,只有运行的时候才会被载入内存。对于支持并行的操作系统来说,必须为每一个运行的程序分配所需的资源(内存空间、输入输出设备等),并确保同时运行的程序之间不会相互干扰,为此,操作系统将每一个运行着的程序视为一个进程:

  • 操作系统以进程为单位,为每个进程分配执行所需要的资源;
  • 原则上,各个进程之间不允许访问对方的资源;
  • 操作系统实时监控着每个进程的执行状态,必要时可以强制其终止执行。

也就是说在操作系统看来,每个载入内存执行的程序都是一个进程。操作系统以进程为单位分配资源,各个进程相互独立,执行过程互不干扰。

同一时间,操作系统可以运行多个应用程序(进程),每个应用程序(进程)还可以同时执行多个任务,例如迅雷支持同时下载多个文件,QQ 也支持同时和多个好友聊天。同一进程中,执行的每个任务都被视为一个线程。

线程和进程之间的关系,与工厂和工人之间的关系非常相似。一个进程好比是一座工厂,一个线程就如同这个工厂中的一个工人。工厂可以容纳多个工人,每个工人负责完成一项具体的任务。工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源。

也就是说,一个进程中可以包含多个线程,所有线程共享进程拥有的资源。当然,每个线程也可以拥有自己的私有资源。下图给您展示进程和线程之间的关系:

图 1 进程和线程的关系

如图 1 所示,所有线程共享的进程资源有:

  • 代码:即应用程序的代码;
  • 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
  • 进程空间:操作系统分配给进程的内存空间;
  • 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。

各个线程也可以拥有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等。

多线程编程的实现方法

了解了程序、进程和线程之间的关系后,多线程的含义就很容易理解了,它指的是一个进程中拥有多个(≥2)线程。通常,我们将编写多线程程序的过程称为“多线程编程”。

本文的目标立足于教会大家编写入门级别的多线程程序,有关线程同步、线程死锁、线程属性等内容,建议您转至《多线程编程(C语言+Linux)》专题做系统的学习。

Linux 上编写多线程程序,可以借助 <pthread.h> 头文件提供的一些函数,常用的函数有如下几个:

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() 函数的功能主要有两个,分别是:

  1. 接收目标线程执行结束时的返回值;
  2. 释放目标线程占用的进程资源。

pthead_join() 函数的语法格式如下:

int pthread_join(pthread_t thread, void ** retval);

thread 参数用于指定目标线程;retval 参数用于存储接收到的返回值。实际场景中,调用 pthread_join() 函数可能仅是为了及时释放目标线程占用的资源,并不想接收它的返回值,这种情况下可以将 retval 置为 NULL。

pthread_join() 函数会一直阻塞当前线程,直至目标线程执行结束,阻塞状态才会消除。如果成功等到了目标线程执行结束(成功获取到目标线程的返回值),pthread_join() 函数返回数字 0,否则返回非零数。

想全方位搞清楚 pthread_join() 函数的功能和用法,可阅读《获取线程函数的返回值》一文。

第一个多线程程序

接下来,我们利用上文学到的知识,编写第一个多线程程序:

  1. #include <stdio.h>
  2. #include <pthread.h>
  3. //定义线程要执行的函数,arg 为接收线程传递过来的数据
  4. void* Thread1(void* arg)
  5. {
  6. printf("http://www.weixueyuan.net\n");
  7. return "Thread1成功执行";
  8. }
  9. //定义线程要执行的函数,arg 为接收线程传递过来的数据
  10. void* Thread2(void* arg)
  11. {
  12. printf("魏雪原\n");
  13. return "Thread2成功执行";
  14. }
  15. int main()
  16. {
  17. int res;
  18. //创建两个线程变量
  19. pthread_t mythread1, mythread2;
  20. void* thread_result;
  21. //创建 mythread1 线程,执行 Thread1() 函数
  22. res = pthread_create(&mythread1, NULL, Thread1, NULL);
  23. if (res != 0) {
  24. printf("线程创建失败");
  25. return 0;
  26. }
  27. //创建 mythread2 线程,执行 Thread2() 函数
  28. res = pthread_create(&mythread2, NULL, Thread2, NULL);
  29. if (res != 0) {
  30. printf("线程创建失败");
  31. return 0;
  32. }
  33. //阻塞主线程,直至 mythread1 线程执行结束,用 thread_result 指向接收到的返回值,阻塞状态才消除。
  34. res = pthread_join(mythread1, &thread_result);
  35. //输出线程执行完毕后返回的数据
  36. printf("%s\n", (char*)thread_result);
  37. //阻塞主线程,直至 mythread2 线程执行结束,用 thread_result 指向接收到的返回值,阻塞状态才消除。
  38. res = pthread_join(mythread2, &thread_result);
  39. printf("%s\n", (char*)thread_result);
  40. printf("主线程执行完毕");
  41. return 0;
  42. }

程序中共有 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)》专题中给大家做详细的讲解。

(0)

相关推荐