5. 多线程编程
5.1 多线程编程的基本概念
线程是计算机中实现多任务的基本单位之一,它是进程中的一个执行流程。一个进程可以包含多个线程,每个线程可以独立运行,并且共享该进程的内存空间和系统资源。
线程的特点是轻量级,创建和销毁的开销比进程小,同时多个线程之间的切换也比进程的切换快速,因此在需要同时执行多个任务的情况下,使用线程可以提高程序的性能和响应速度。
线程通常由操作系统调度执行,并可以通过同步机制来控制多个线程之间的协作和互斥访问共享资源。常见的同步机制包括互斥锁、条件变量、信号量等。线程的实现方式包括用户级线程和内核级线程,其中用户级线程是由应用程序自己实现的,而内核级线程则由操作系统实现。
5.2 使用多线程的好处
提高程序的并发性和响应速度:多线程可以使程序中的多个任务并行执行,从而提高程序的并发性和响应速度。例如,在一个网络服务器中,可以使用多线程来处理客户端请求,从而提高服务器的处理能力和响应速度。
更小的系统开销:线程在同一进程内部运行,因此相对于多进程来说,线程间的切换和通信的开销更小。多进程需要使用进程间通信(IPC)机制进行进程间数据的传递和同步,而线程可以直接访问共享内存,避免了IPC的开销。
充分利用多核CPU的性能:多线程可以充分利用多核CPU的性能,使多个线程在不同的CPU核心上并行执行,从而提高程序的性能。例如,在图像处理、视频编码等密集计算场景中,可以使用多线程来加速计算。
提高资源利用率:多线程可以共享同一进程的代码段、数据段和堆空间,从而减少资源的浪费。例如,在一个文件压缩程序中,可以使用多线程来并行处理多个文件,从而提高磁盘和CPU的利用率。
简化程序设计:多线程可以将复杂的任务分解成多个简单的子任务,每个子任务可以由一个单独的线程来处理,从而简化程序的设计和实现。例如,在一个图形界面程序中,可以使用多线程来分离界面和后台任务的处理,使程序更加清晰和易于维护。
需要注意的是,多线程也存在一些问题,例如线程安全、资源竞争、死锁等,需要仔细考虑和解决。因此,在使用多线程时需要谨慎设计和同步机制,以确保线程之间的正确协作。
虽然多线程给应用开发带来了好处,但是并不是所有情况都适合多线程,使用多线程的情况包括但不限于以下几种:
- 并发执行多个任务。
- 处理耗时任务。
- 各个任务有不同的优先级。
- 实现异步操作。
5.3 线程的状态
- 一个线程从创建到结束是一个生命周期,总是处于以下几种状态:
- 就绪态:当线程可以开始运行,但还没有被调度执行时,线程处于就绪状态。此时,线程已经分配了所有需要的系统资源,等待操作系统的调度器分配CPU资源。
- 运行状态:当线程被调度执行并开始运行时,线程处于运行状态。
- 阻塞状态:当线程需要等待某些事件发生时,如等待I/O操作完成、等待信号量、等待锁等,线程就会进入阻塞状态。此时,线程不会占用CPU资源。
- 终止状态:当线程执行完成或发生了未处理的异常时,线程就会进入终止状态。此时,线程所占用的系统资源会被释放,线程对象被销毁。
5.4 线程的标识
- 在多线程编程中,线程的标识ID号是用来唯一标识一个线程的。
- 线程标识通常是一个整数值,由操作系统内核自动生成,并由编程语言提供的库函数返回。
- 线程标识的ID号从线程创建开始存在,线程结束后,该ID号就自动消失。
5.5 创建线程
在Linux系统下的多线程遵循POSIX标准,那么在开发之前先来熟悉一下POSIX多线程API函数:
| API函数 | 含义 |
|---|---|
| pthread_create | 创建一个新线程 |
| pthread_join | 等待一个线程结束并获取其返回值 |
| pthread_self | 获取线程ID |
| pthread_cancel | 取消另一个线程 |
| pthread_exit | 在线程函数中调用来退出线程函数 |
| pthread_kill | 向线程发送一个信号 |
5.5.1 pthread_create函数
在POSIX API中使用pthread_create()函数创建进程,函数声明:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);参数说明:
- thread:指向线程标识符的指针。
- attr:设置线程属性,具体内容在下一小节讲解。
- start_routine:start_routine是一个函数指针,指向要运行的线程入口,即线程运行时要执行的函数代码。
- arg:运行线程时传入的参数。
- 返回值:若线程创建成功,则返回0。若线程创建失败,则返回对应的错误代码。
- 注意:在链接时需要使用库libpthread.a。因为pthread的库不是Linux系统的库, 所以在编译时要加上-lpthread 选项。
5.5.2 pthread_join函数
在POSIX API中使用pthread_join()函数等待一个指定的线程结束并获取它的退出状态,函数声明:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);- thread 参数是被等待的线程的标识符;
- retval 参数是一个指向指针的指针,用于存储被等待的线程的退出状态。
- 如果不需要获取被等待线程的退出状态,可以将 retval 参数设置为 NULL。
5.5.3 pthread_exit函数
在POSIX API中使用pthread_exit()函数显式地终止当前线程的执行并退出线程,函数声明:
#include <pthread.h>
void pthread_exit(void *retval);- retval 参数是一个指向任意类型数据的指针;
- 表示线程的退出状态。
- 如果不需要获取线程的退出状态,可以将 retval 参数设置为 NULL。
在线程执行过程中遇到了 return,也会终止执行;既然 return 关键字也适用于线程函数,<pthread.h> 头文件为什么还提供 pthread_exit() 函数,它们的区别如下:
return (void)0; 和 pthread_exit((void)0) 的主要区别在于:return 语句只是从线程函数中返回并退出当前线程,而 pthread_exit() 函数可以在任何时候使用并显式地退出线程。
pthread_exit() 函数还提供了线程清理处理的功能,可以在退出线程时调用所有已注册的线程清理处理函数,从而更好地管理线程资源。
5.5.4 线程应用实例
创建一个线程,并传入整型参数:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg)
{
int *count = (int*)arg;
for (int i = 1; i <= *count; ++i)
{
printf("Thread: %d\n", i);
sleep(1);
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
int count = 5;
ret = pthread_create(&tid, NULL, thread_func, &count);
if (ret != 0)
{
printf("pthread_create failed:%d\n", ret);
return -1;
}
printf("create treads success\n");
ret = pthread_join(tid, NULL);
if ( ret != 0)
{
printf("pthread_join");
return -1;
}
printf("Thread finished.\n");
return 0;
}编译和运行程序:
linaro@linaro-alip:~/Linux/thread$ gcc -o thread thread.c -lpthread
linaro@linaro-alip:~/Linux/thread$ ./thread
create treads success
Thread: 1
Thread: 2
Thread: 3
Thread: 4
Thread: 5
Thread finished.
linaro@linaro-alip:~/Linux/thread$
5.6 线程属性
- POSIX标准规定线程有多个属性,其中包括:分离状态、调度策略和参数、作用域、栈尺寸、栈地址、优先级等。
5.6.1 分离状态
在 POSIX 线程库中,一个线程可以是分离的,也可以是非分离的。线程的分离状态定义了线程的结束方式以及线程占用的系统资源。
如果一个线程是分离的,那么它结束时不会留下任何资源,包括线程 ID 和线程占用的内存资源。这意味着它不能被其他线程 join,也不能被获取状态信息。
可以通过 pthread_attr_setdetachstate() 函数来设置线程的分离状态,其参数可以是 PTHREAD_CREATE_JOINABLE 或者 PTHREAD_CREATE_DETACHED,分别对应着非分离和分离状态。
一般来说,如果一个线程只是执行一些独立的任务,不需要其他线程 join 获取其状态信息,那么可以将其设置为分离状态,以减少资源占用。 但是需要注意的是,对于分离状态的线程,不能通过 join 等方式来确保线程已经结束,需要自己在代码中处理好线程的结束。
通过线程属性设置线程的分离状态:
初始化线程属性:
int pthread_attr_init(pthread_attr_t *attr);销毁线程属性所占用的资源:
int pthread_attr_destroy(pthread_attr_t *attr);- 返回值:成功:0;失败:错误号。
设置线程属性,分离 or 非分离:
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);- attr:已初始化的线程属性(传入参数)detachstate: PTHREAD_CREATE_DETACHED(分离线程)、PTHREAD _CREATE_JOINABLE(非分离线程)
创建一个子线程,设置了其分离状态为PTHREAD_CREATE_DETACHED,使得该线程在结束时自动释放资源:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg)
{
printf("Child thread is running.\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_attr_t attr;
int detachstate;
// 初始化线程属性对象
pthread_attr_init(&attr);
// 获取默认的分离状态
pthread_attr_getdetachstate(&attr, &detachstate);
printf("Default detach state: %s\n", detachstate == PTHREAD_CREATE_JOINABLE ? "joinable" : "detached");
// 设置线程的分离状态为detached
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, thread_func, NULL);
// 销毁线程属性对象
pthread_attr_destroy(&attr);
// 主线程等待一会,以保证子线程已经运行
sleep(1);
printf("Main thread is exiting.\n");
pthread_exit(NULL);
}
5.6.2 栈尺寸
栈(stack)是一种数据结构,用于存储函数调用、局部变量以及临时数据等信息。每个线程都有自己的栈,用于存储其执行期间所需要的信息。
栈的尺寸是指在创建线程时分配给它的栈空间大小。栈的大小直接影响线程能够处理的数据量和执行时间。如果栈太小,则可能会导致栈溢出,程序崩溃。如果栈太大,则会浪费系统资源。
在创建线程时,可以通过指定线程属性来设置线程的栈尺寸。例如,在 POSIX 线程库中,可以使用 pthread_attr_setstacksize() 函数设置线程栈的大小,单位是字节。
栈尺寸的大小应该根据具体应用程序的需要进行调整。如果应用程序需要处理大量数据或者需要递归调用函数,那么可能需要分配更大的栈空间。如果应用程序只需要处理少量数据,那么可以使用较小的栈空间,以节省系统资源。
在 POSIX 线程库中,可以使用函数设置和获取线程栈的大小,单位是字节,函数声明:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);参数说明:
- attr:指向一个线程属性的指针。
- stacksize:线程栈的大小。
5.6.3 调度策略
线程的调度策略指的是操作系统如何安排线程在 CPU 上的执行顺序。不同的调度策略会影响线程的优先级和执行方式。
POSIX 线程库定义了三种调度策略:
- SCHED_FIFO(先进先出):按照线程加入队列的顺序执行,直到该线程运行结束或者它被抢占,才会执行下一个线程。优先级高的线程会先执行,优先级相同的按照先来先服务的顺序执行。
- SCHED_RR(轮流调度):每个线程执行的时间片固定,当时间片用完后,将被放回队列尾部,等待下一次调度。同样,优先级高的线程会先执行,优先级相同的按照先来先服务的顺序执行。
- SCHED_OTHER(其他):由系统决定线程的调度,该策略常常被用作多线程编程的默认策略,通常不需要显式地指定。线程的调度策略可以使用 pthread_attr_setschedpolicy() 函数设置,也可以使用 pthread_setschedparam() 函数动态地修改线程的优先级和调度策略。
在 POSIX 线程库中,可以使用函数获取和设置线程的调度策略与参数,函数声明:
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);参数说明:
- attr:指向一个线程属性的指针。
- inheritsched:线程是否继承调度属性,可选值分别为:
- PTHREAD_INHERIT_SCHED:调度属性将继承于创建的线程,attr中设置的调度属性将被忽略。
- PTHREAD_EXPLICIT_SCHED:调度属性将被设置为attr中指定的属性值。
- policy:可选值为线程的三种调度策略,SCHED_OTHER、SCHED_FIFO、SCHED_RR。
返回值:若函数调用成功返回0,否则返回对应的错误代码。
5.6.4 优先级
在Linux系统中,线程的优先级分为静态优先级(static priority)和动态优先级(dynamic priority)两种。
- 静态优先级是在创建线程时分配的,通常使用pthread_attr_setschedparam()函数来设置。静态优先级值越小,表示线程优先级越高,反之越低。当线程的调度策略为SCHED_OTHER时,其静态优先级必须设置为0。这是Linux系统调度的默认策略,处于0优先级别的这些线程按照动态优先级被调度。在实时调度策略中,静态优先级决定了实时线程的基本调度次序。
- 动态优先级则是在运行时根据线程的行为和资源需求而动态调整的。在Linux系统中,动态优先级是使用nice值来计算的。nice值是一个整数,取值范围为-20到+19,其中-20表示最高优先级,+19表示最低优先级。当一个线程处于就绪状态但是无法被调度时,其动态优先级会增加一个单位,这样能够保证这些线程之间的竞争公平性。而当线程被调度后,其动态优先级会降低一定的值,以防止某个线程占用CPU时间过长,导致其他线程无法得到执行。
设置线程的最小和最大优先级,函数声明:
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);获取线程3种调度策略下可设置的最小和最大优先级:
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
int main()
{
printf("Valid priority range for SCHED_OTHER: %d - %d\n",
sched_get_priority_min(SCHED_OTHER),
sched_get_priority_max(SCHED_OTHER));
printf("Valid priority range for SCHED_FIFO: %d - %d\n",
sched_get_priority_min(SCHED_FIFO),
sched_get_priority_max(SCHED_FIFO));
printf("Valid priority range for SCHED_RR: %d - %d\n",
sched_get_priority_min(SCHED_RR),
sched_get_priority_max(SCHED_RR));
return 0;
}SCHED_OTHER是不支持优先级使用的,而SCHED_FIFO和SCHED_RR支持优先级的使用,他们分别为1和99,数值越大优先级越高。设置和获取优先级通过以下两个函数:
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);设置和获取线程的调度参数,包括调度策略和优先级:
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
int main() {
pthread_attr_t attr;
struct sched_param param;
int policy;
// 初始化线程属性对象
pthread_attr_init(&attr);
// 获取默认调度策略
pthread_attr_getschedpolicy(&attr, &policy);
// 打印默认调度策略
if (policy == SCHED_OTHER) {
printf("Default scheduling policy is SCHED_OTHER\n");
} else if (policy == SCHED_RR) {
printf("Default scheduling policy is SCHED_RR\n");
} else if (policy == SCHED_FIFO) {
printf("Default scheduling policy is SCHED_FIFO\n");
}
// 获取默认的调度参数
pthread_attr_getschedparam(&attr, ¶m);
// 打印默认的优先级
printf("Default priority is %d\n", param.sched_priority);
// 设置线程的调度策略和优先级
param.sched_priority = 50;
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, ¶m);
// 获取新的调度策略和优先级
pthread_attr_getschedpolicy(&attr, &policy);
pthread_attr_getschedparam(&attr, ¶m);
// 打印新的调度策略和优先级
if (policy == SCHED_OTHER) {
printf("New scheduling policy is SCHED_OTHER\n");
} else if (policy == SCHED_RR) {
printf("New scheduling policy is SCHED_RR\n");
} else if (policy == SCHED_FIFO) {
printf("New scheduling policy is SCHED_FIFO\n");
}
printf("New priority is %d\n", param.sched_priority);
// 销毁线程属性对象
pthread_attr_destroy(&attr);
return 0;
}
5.7 线程结束
- 线程的结束指的是线程执行完它的任务后退出,释放它占用的资源,以便其他线程能够使用这些资源。线程的结束可以有两种方式:
- 正常结束:线程执行完它的任务后,通过调用pthread_exit()函数或者从线程函数中返回来退出线程。在正常结束的情况下,线程会自动释放它所占用的资源,包括栈空间、线程描述符和其他资源等。
- 异常结束:线程在执行的过程中出现了错误,导致线程无法继续执行,此时线程会自动退出。在这种情况下,需要确保线程释放它所占用的所有资源,以避免资源泄漏和内存泄漏等问题。
- 总之,线程的结束是非常重要的,它可以避免资源泄漏和内存泄漏等问题,同时也可以提高系统的性能和稳定性。因此,在编写多线程程序时,要注意线程的结束,保证线程能够正常退出,释放所有占用的资源。
- 在线程的创建线程部分,我们介绍过线程执行过程中遇到了 pthread_exit() 或者 return,会终止执行;
5.7.1 pthread_cancel函数
在多线程程序中,一个线程还可以向另一个线程发送“终止执行”的信号(后续称“Cancel”信号),这时就需要调用 pthread_cancel() 函数,函数声明:
#include <pthread.h>
int pthread_cancel(pthread_t thread);在使用pthread_cancel() 函数之前,要了解目标线程对cancle信号的处理机制。
- 对于默认属性的线程,当向目标线程发送取消请求( Cancel 信号)时,它并不会立即结束执行,而是遇到取消点(Cancellation points)时,会响应 Cancel 信号并终止执行。
- POSIX标准中将允许取消操作的函数称为“cancellation points”,也称为“cancellable functions”。
- 取消点是指在程序执行期间,允许取消一个线程并执行清理工作的特定代码位置。在POSIX标准中,许多标准的系统调用和库函数都是取消点,例如I/O函数、内存分配函数等等。比如常见的 pthread_join()、pthread_testcancel()、sleep()、system() 等。
5.7.2 pthread_setcancelstate
如果想要手动修改目标线程处理 Cancel 信号的方式,我们可以使用 pthread_setcancelstate() 和 pthread_setcanceltype() 这两个函数。
pthread_setcancelstate()函数声明:
#include <pthread.h>
int pthread_setcancelstate( int state , int * oldstate );参数含义:
- state 参数有两个可选值:
- PTHREAD_CANCEL_ENABLE(默认值):当前线程会处理其它线程发送的 Cancel 信号;
- PTHREAD_CANCEL_DISABLE:当前线程不理会其它线程发送的 Cancel 信号,直到线程状态重新调整为 PTHREAD_CANCEL_ENABLE 后,才处理接收到的 Cancel 信号。
- oldtate 参数用于接收线程先前所遵循的 state 值,通常用于对线程进行重置。如果不需要接收此参数的值,置为 NULL 即可。
- state 参数有两个可选值:
返回值:pthread_setcancelstate() 函数执行成功时,返回数字 0,反之返回非零数。
5.7.3 pthread_setcanceltype函数
pthread_setcanceltype()函数声明:
#include <pthread.h>
int pthread_setcanceltype( int type , int * oldtype );参数含义:
- type 参数有两个可选值,分别是:
- PTHREAD_CANCEL_DEFERRED(默认值):当线程执行到某个可作为取消点的函数时终止执行;
- PTHREAD_CANCEL_ASYNCHRONOUS:线程接收到 Cancel 信号后立即结束执行。
- oldtype 参数用于接收线程先前所遵循的 type 值,如果不需要接收该值,置为 NULL 即可。
- type 参数有两个可选值,分别是:
返回值:pthread_setcanceltype() 函数执行成功时,返回数字 0,反之返回非零数。
创建一个线程tid,分别将线程的取消状态和取消类型设置为PTHREAD_CANCEL_DISABLE和PTHREAD_CANCEL_ASYNCHRONOUS,主线程sleep(2)后向线程发送了一个取消请求,线程即取消。
#include <pthread.h>
#include <stdio.h>
void* thread_func(void* arg)
{
int oldstate, oldtype;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);
printf("Thread started.\n");
while (1)
{
printf("Thread running...\n");
}
printf("Thread ended.\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(2);
pthread_cancel(tid);
printf("Thread cancelled.\n");
pthread_join(tid, NULL);
printf("Main thread ended.\n");
return 0;
}