1.Linux线程概念
1.1 什么是线程
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的控制序列。一个进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行。透过进程的虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
在Linux系统中,线程(thread)是执行程序的基本单元。他是进程内的一个独立执行序列,与同一进程内的其他线程共享进程的资源,包括内存空间、文件描述符、打开的文件等。每个线程有自己的线程ID和线程上下文,但是它们属于同一个进程。
线程的优点:
创建一个新线程的代价要比创建一个新的进程小的多。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点:
性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多。
线程异常:
单个线程如果出现除0、野指针问题导致线程崩溃,进程也会随之崩溃。
线程是进程的执行分支,线程出现异常,就类似进程出现异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
线程用途:
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边聊QQ,一边下载群内的文件就是多线程运行的一种表现)
2.Linux进程与线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据,比如:
线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级。因为每个线程都是独立的单位,所以需要有线程id来分辨;又因为线程是调度的基本单位,即每个线程要去执行不同的任务,从而需要由独立的任务上下文,由此需要有独立的寄存器、栈、errno、信号屏蔽器、调度优先级。
进程的多个线程共享同一地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在个线程中都可以访问到;除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式
当前工作目录
用户id和组id
3.Linux线程控制
3.1 POSIX线程库
posix线程库提供一组函数和数据类型,用于创建、同步和管理线程,以及对线程进行互斥和条件变量操作等。
与线程有关的函数是一个完整的系列,绝大多数的函数的名字都是以“pthread_”打头的。
要使用这些与线程相关的函数,需要引入头文件 <pthread.h>
链接这些函数库时要使用编译器命令中的“-lpthread"选项。
3.2 线程创建与终止
3.2.1创建线程
pthread_create
该函数用于创建一个线程,其函数原型如下;
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
pthread_create函数接受四个参数:
thread:指向pthread_t类型的指针,用于存储新线程的标识符。在成功创建线程后,该指针将包含新线程的标识符,可以用于其他pthread库函数操作线程。
attr:指向pthread_attr_t类型的指针,用于指定线程的属性。如果为NULL,表示使用默认属性创建线程。
start_routine:指向线程函数的指针,它是线程的入口点。线程函数的返回类型是void *,并接受一个void *类型的参数。
arg:传递给线程函数的参数,可以是任意类型的指针。在线程函数中,可以通过类型转换将其还原为实际的参数。
返回值:如果创建线程成功就返回0,否则返回错误码
下面是个简单的代码示例:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg)
{
int i;
for (;;)
{
printf("I'am thread 1\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
int ret;
if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0)
{
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for (;;)
{
printf("I'am main thread\n");
sleep(1);
}
}
需要注意的是编译这段代码的时候需要链接pthread库才行,在Linux系统上可以使用 -pthread选项来连接pthread库。比如:
gcc your_code.c -pthread -o your_program
如果成功编译之后,运行该可执行文件可以得到如下效果;
两个线程交替输出,一个简单并发程序就这样诞生了。
3.2.2 进程ID和线程ID
在Linux中,目前的线程实现的是Native POSIX Thread Libaray,简称NPTL。是Linux系统上的一个线程库,他是对POSIX线程库的实现。在这种实现下,线程又被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)即PCB。
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准有要求进程内的所有线程调用getpid函数时返回相同的ID。
那么现在就会有问题,既然每个线程都有自己独立的线程ID,但是又要在使用getpid函数时返回相同的进程ID。那么该如何处理呢?
为了解决上述问题,Linux内核引入了线程组的概念。
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之后都存在一个进程描述符(task_struct)与之对应,进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。
现在介绍的线程ID,不同于之前在pthread_create函数中介绍的线程ID;现在介绍的线程ID,和进程ID一样,线程是pid_t类型的变量,而且是用来唯一标识线程的一个整形变量。那么如何查看一个线程的ID呢?
可以通过如下命令查看线程ID
ps -eLf
该命令会列出系统中的所有的线程,包括它们的进程ID和线程ID。所以我们需要通过grep命令来筛选我们想要的线程。
这次我们依然采用的上述演示的并行代码,然后再另一个终端中查看线程。可以看到上述有两个线程,但是他们的PID和PPID是相同的,证明它们是同一个进程下面的线程,但是它们的线程ID是不同的。
Linux提供了gettid系统调用来返回线程ID,但是glibc并没有将该系统调用封装起来,再开放接口来供程序员使用。如果确实需要获得线程ID,可以采用如下方法以获取当前线程的线程ID。
#include <sys/syscall.h> pid_t tid;
tid = syscall(SYS_gettid);
以下是一个简单示例:
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t tid;
tid = syscall(SYS_gettid);
printf("Thread ID: %d\n", tid);
return 0;
}
从上面的截图来看,test.exe进程的ID为19764,然后这个进程中的一个线程的线程ID也是19764。这不是巧合,线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中称为group leader,内核在创建第一个线程时,会将线程组的ID设置成第一个线程的线程ID,group_leader指针则指向自身,即主线程的进程描述符。所以线程组存在一个线程ID等于进程ID,而该线程即为线程组的主线程。
至于线程组其他线程的ID则有内核负责分配,其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。
需要注意的是,线程和进程不一样,进程有父进程的概念,但在线程里面,所有的线程都是对等关系。
3.2.3 线程ID及进程地址空间布局
pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_self函数,可以获得线程自身的ID。
#include <pthread.h>
pthread_t pthread_self(void);
pthread_self函数不需要参数,它会返回调用线程的pthread_t类型的值,表示当前线程的ID。以下是简单的示例代码。
#include <stdio.h>
#include <pthread.h>
void *threadFunction(void *arg) {
pthread_t tid = pthread_self();
printf("Thread ID: %lu\n", (unsigned long)tid);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, threadFunction, NULL);
pthread_join(thread, NULL);
return 0;
}
对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
3.3 线程终止
如果需要值终止某个线程而不终止整个进程,可以有如下三种方法:
a.从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit,那么整个进程都终止了。
b.线程可以调用pthread_exit终止自己。
c.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
pthread_exit函数
该函数时POSIX线程库提供的一个函数,用于终止当前线程的执行,并返回一个退出状态给线程的创建者。
#include <pthread.h>
void pthread_exit(void *value_ptr);
value_ptr是一个指针,用于指定线程的退出状态。它可以是任意类型的指针,通过类型转换可以传递不同类型的值。线程的创建者可以通过调用pthread_join函数来获取线程的退出状态
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了,这时候线程开辟的栈已经被操作系统回收了。
pthread_cancel函数
pthread_cancel函数同样是POSIX线程库提供的一个函数,用于请求取消指定线程的执行。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
当调用pthread_cancel函数时,他会向目标线程发送取消请求。目标线程会在适当的时机检查取消请求,并根据其设置的取消状态决定是否取消自己的执行,也就是说该函数并不一定会结束一个线程,被取消的线程可以选择接受或忽略取消请求。
pthread_cancel函数的返回值表示请求是否成功发送,如果返回0,则表示请求成功发送;如果返回非零值,则表示请求发送失败。即该函数的返回值表示请求是否发送到线程,而不是表示线程是否接受取消请求而结束这个线程。
3.4 线程等待
为什么需要线程等待?
线程等待是一种同步机制,用于确保主线程等待其他线程完成其执行,以避免并发执行的竞争条件和数据访问冲突。
pthread_join函数
该函数由POSIX线程库提供,用于等待指定线程的结束,并获取线程的退出状态。
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
该函数接受两个参数:thread表示目标线程的标识符,value_ptr是一个二级指针,用于接收目标线程的退出状态。
返回值:成功返回0,失败返回错误码。
当调用pthread_join函数时候,他会阻塞调用线程,知道目标线程结束。一旦目标线程结束,pthread_join会返回,并将目标线程的退出状态存储在value_ptr指向的地址中。
目标线程的退出状态可以通过对value_ptr解引用来获取,通过pthread_join得到的终止状态是不同的,共有如下四种:
1.如果thread线程通过return返回,value_ptr所指向的地址里存放的是thread线程函数的返回值。
2.如果thread线程被别的线程调用pthread_cancel异常终止,value_ptr所指向的地址里存放的是常数PTHREAD_CANCELED。
3.如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4.如果不关心退出状态,可以将value_ptr设置为NULL
4.分离线程
当创建一个线程的时候,新线程的初始状态可以是joinable和detached。这是由线程属性决定的,默认情况下,新创建的线程是joinable的。joinable状态的线程意味着其他线程可以通过调用pthread_join函数来等待该线程的结束,并获取其退出状态。通过这种方式,可以实现线程之间的同步和协作。
具体的,joinable线程状态有以下特点:
1.其他线程可以调用pthread_join函数来等待joinable线程的结束。调用线程将被阻塞,知道目标线程结束为止。
2.在调用pthread_join函数后,被等待的线程的资源(包括栈和线程控制块)将被保留,知道调用线程成功地获取了该线程地退出状态。
3.一旦线程结束,并且它地退出状态已经被获取,可以通过调用pthread_join函数来释放该线程地资源,清理其占用的系统资源。
如果并不关心线程的返回值,join是一种负担,因为这时候该线程并不会退出,会一直占用系统资源。这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,不必等待将返回值传递出去后再释放。
这个时候我们可以使用pthread_detach函数。该函数是POSIX线程库提供的一个函数,用于将指定线程设置为detached状态,使其再结束时自动释放资源。
#include <pthread.h>
int pthread_detach(pthread_t thread);
pthread_detach函数接收一个pthread_t类型的参数thread,表示要设置为detached状态的目标线程的标识符。可以时线程组内其他线程对目标线程进程分离,也可以是线程自己分离。
需要注意的是,一个线程不能既是joinable的,同时又是detached的。也就是说一个线程已经被pthread_detach函数调用过,不能再被pthread_join函数调用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{
pthread_detach(pthread_self());
printf("%s\n", (char *)arg);
return NULL;
}
int main(void)
{
pthread_t tid;
const char *thread1_arg = "thread1 run...";
if (pthread_create(&tid, NULL, thread_run, (void *)thread1_arg) != 0)
{
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1); // 很重要,要让线程先分离,再等待
if (pthread_join(tid, NULL) == 0)
{
printf("pthread wait success\n");
ret = 0;
}
else
{
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
可以看到上述代码无法成功调用pthread_join函数。
5.线程互斥
5.1 进程线程间的互斥相关背景概念
进程互斥和线程互斥是一种同步机制,用于确保多个线程再访问共享资源时的互斥性,以避免竞争和数据访问冲突。
竞争指的是多个线程或进程同时访问共享资源,并且其最终结果取决于执行的相对时间, 顺序或具体的调度机制。当多个线程对共享资源进行读写操作时,如果操作的结果受到线程执行的顺序或时间的影响,就可能发生竞争条件。
数据访问冲突发生在多个线程或进程同时访问共享的数据时,导致对数据的访问产生冲突和不一致的情况,当多个线程对共享数据进行读写操作时,如果没有适当的同步机制来保证对数据的访问互斥和有序,就可能导致数据的不一致性。
临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区域,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断地操作,该操作只有两种状态,要么完成,要么未完成。没有失败。
5.2 互斥量mute
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量叫做共享变量,可以通过数据的共享,完成线程之间的交互。
但是多个线程并发的操作共享变量,就会出现一些问题,比如上文中的竞争和数据冲突问题。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
}
int main(void)
{
pthread_t t1, t2, t3, t4;
const char *thread1_arg = "thread 1";
const char *thread2_arg = "thread 2";
const char *thread3_arg = "thread 3";
const char *thread4_arg = "thread 4";
pthread_create(&t1, NULL, route, (void *)thread1_arg);
pthread_create(&t2, NULL, route, (void *)thread2_arg);
pthread_create(&t3, NULL, route, (void *)thread3_arg);
pthread_create(&t4, NULL, route, (void *)thread4_arg);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
上述代码编译成的可执行文件,每次运行状况都不一样。
第一次运行
第二次运行:
可以看到两次运行的结果都不一样,这是因为上述代码存在竞争问题,多个线程同时访问和修改共享的ticket变量。又没有设置互斥条件,从而导致四个线程之间不断抢夺该变量。这才导致每次运行的结果都不一样,可能这次运行是线程1卖出了第100票,下次就变成了线程3。
要想解决上述问题,需要做到三点:
代码必须要有互斥行为:当代码进入临界区运行时,不允许其他线程进入该临界区。
如果多个线程同时要求运行临界区的代码,并且临界区没有线程在运行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,只需要一把锁即可,一把Linux系统提供的锁,即互斥量mutex。
5.3 互斥量的接口
5.3.1 初始化互斥量
1.静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
上述代码中首先声明一个mutex的互斥量,然后调用PTHREAD_MUTEX_INITIALIZER宏进行静态初始化。这回将互斥量初始化为默认属性。
使用静态初始化的好处是可以在声明时就对互斥量进行初始化,避免了显示调用pthread_mutex_init函数。这种方法适用于静态全局互斥量或静态局部互斥量。
2.动态分配
pthread_mutex_init函数用于动态初始化互斥量(Mutex)。其函数原型如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
mutex是一个指向互斥量对象的指针,用于指定要初始化的互斥量。
attr是一个指向互斥量属性对象(pthread_mutexattr_t类型)的指针,用于指定互斥量的属性。如果不需要特定属性,可以将该参数设置为NULL。
pthread_mutex_init函数返回一个整数值,用于指示操作的结果。如果返回值为0,表示互斥量初始化成功;如果返回值为非零值,表示初始化失败。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int main() {
pthread_mutex_t mutex; // 声明互斥量变量
pthread_mutexattr_t mutexAttr; // 声明互斥量属性变量
int result;
// 初始化互斥量属性
pthread_mutexattr_init(&mutexAttr);
// 设置互斥量属性(可选)
// ...
// 初始化互斥量
result = pthread_mutex_init(&mutex, &mutexAttr);
if (result != 0) {
printf("互斥量初始化失败\n");
exit(EXIT_FAILURE);
}
// 互斥量初始化成功,可以在此之后使用互斥量
// 销毁互斥量属性
pthread_mutexattr_destroy(&mutexAttr);
// 销毁互斥量
pthread_mutex_destroy(&mutex);
return 0;
}
5.3.2 销毁互斥量
销毁互斥量需要注意以下几点:
1.使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
不要销毁一个已经加锁的互斥量。
已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
可以使用pthread_mutex_destroy来销毁互斥量。
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t* mutex);
mutex是一个指向互斥量对象的指针,用于指定要销毁的互斥量。
pthread_mutex_destroy函数返回一个整数值,用于指示操作的结果。如果返回值为0,表示互斥量销毁成功;如果返回值为非零值,表示销毁失败。
5.3.3 互斥量加锁和解锁
有了互斥量之后并不能实现互斥,还需要对互斥量进行加锁和解锁操作才能对线程进行互斥操作。
互斥量加锁:
互斥量加锁的目的是确保只有一个线程可以访问临界区,从而避免多个线程同时访问和修改共享资源。在POSIX线程库中,可以使用pthread_mutex_lock函数来加锁互斥量,将其设置为被当前线程独占。其函数原型如下:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex);
mutex是一个指向互斥量对象的指针,用于指定要加锁的互斥量。
调用pthread_mutex_lock函数时,如果互斥量当前已经被其他线程锁定,则调用线程会被阻塞,直到互斥量解锁为止。当互斥量成功锁定时,函数返回0;如果出现错误,函数返回非零值。
互斥量解锁:
互斥量解锁的目的是释放对临界区的独占访问权,允许其他线程进入临界区并访问共享资源。
在POSIX线程库中,可以使用pthread_mutex_unlock函数来解锁互斥量,将其标记为可被其他线程锁定。其函数原型如下:
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t* mutex);
mutex是一个指向互斥量对象的指针,用于指定要解锁的互斥量。
调用pthread_mutex_unlock函数会释放互斥量,使其再次可用于其他线程进行加锁操作。当互斥量成功解锁时,函数返回0;如果出现错误,函数返回非零值。
当调用pthread_mutex_unlock函数时,如果对应的互斥量并没有被当前线程加锁,将会导致未定义的行为,可能导致程序崩溃、死锁或其他不可预测的结果。
所在,在使用互斥量之前,需要确保在解锁之前先加锁。
由此,我们简单改进以下之前的代码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_mutex_lock(&mutex);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
// sched_yield(); 放弃CPU
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main(void)
{
pthread_t t1, t2, t3, t4;
const char *thread1_arg = "thread 1";
const char *thread2_arg = "thread 2";
const char *thread3_arg = "thread 3";
const char *thread4_arg = "thread 4";
pthread_create(&t1, NULL, route, (void *)thread1_arg);
pthread_create(&t2, NULL, route, (void *)thread2_arg);
pthread_create(&t3, NULL, route, (void *)thread3_arg);
pthread_create(&t4, NULL, route, (void *)thread4_arg);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}
可以看到现在只有线程1访问共享变量,线程2、3、4都没有访问共享变量。因为线程1首先创建,然后运行route线程函数,对mutex互斥量进行加锁,之后ticket只要大于0,就不解锁。所以线程1一直占据着共享变量ticket的访问权。
5.4 可重入和线程安全
5.4.1 概念
线程安全:是指在多线程环境下可以正确执行,而不会出现数据竞争或破坏共享数据的一致性。数据安全代码可以被多个线程同时调用,并保证每个线程的执行结果是正确的。
可重入性:是指代码段或函数在被中断执行后,可以安全地重新进入执行,而不会导致不正确地结果。可重入代码可以被多个线程同时调用,而不会出现数据竞争或意外地副作用。
常见的线程不安全的情况有如下:
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
类或者接口对于线程来说都是原子操作。
多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见不可重入的情况:
调用了malloc/free的函数,因为malloc函数使用全局链表来管理堆的。
调用了标准I/O库函数,标准I/O库的很多实现都以不可冲入的方式使用全局数结构。
可重入函数体内使用了静态的数据结构。
常见可重入的情况:
不使用全局变量或者静态变量
不使用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用这提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中由全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数中锁还未释放则会产生死锁问题,因此是不可重入的。
6.常见锁概念
6.1 死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
6.2 形成死锁的四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
6.3 避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
7. Linux线程同步
7.1 条件变量
当一个线程互斥地访问某个变量时,他可能发现在其他线程改变状态之前,他什么也做不了。
例如一个线程访问队列时,发现队列为空,他只能等待,直到其他线程将一个节点添加到队列中。这种情况就需要用到条件变量。
条件变量是一种同步机制,用于线程间地等待和通知,它允许一个或多个线程等待某个条件地发生,并在条件满足时进行通知,从而实现线程地协调和同步。
7.2同步概念与竞态条件
同步是指多个线程或进程之间协调和管理彼此地执行顺序,以避免竞态条件和不确定地行为。同步机制的目的是确保多个进程在访问共享资源时能够正确的协作,避免数据竞争和不一致性。
竞争条件是指多个线程或进程同时访问和修改共享资源时,最终地结果依赖于它们地执行顺序,导致无法预测地结果。竟态条件通常发生在多线程或多进程环境中,由于缺乏适当地同步机制,导致对共享资源的并发访问出现问题。
7.3 条件变量函数
1.初始化
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
cond:指向要初始化的条件变量对象。
attr:条件变量的属性,通常使用 NULL
该函数用于初始化条件变量对象cond。在初始化之前,应该分配内存给cond,通常使用静态分配或动态分配。
2.销毁
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
cond:指向要销毁的条件变量对象。
该函数用于销毁条件变量对象cond,释放与之关联的资源。在调用该函数之前,确保条件变量已经初始化且不再需要使用。
3.条件等待
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
cond:指向条件变量对象。
mutex:指向与条件变量关联的互斥量。在调用函数前,该互斥量必须被锁定。
该函数用于线程等待条件变量的发生。他是条件变量的核心函数之一,常用于实现线程间的同步和通信。
该函数的执行过程如下:
1.当线程调用pthread_cond_wait 函数时,他会首先释放传入的互斥量mutex,使得其他线程可以获得该互斥量并执行相应的临界区代码。
2.然后,该线程进入等待状态,被操作系统放入等待队列中,等待其他线程发出信号以满足特定的条件。
3.在等待期间,该线程处于阻塞状态,不会消耗CPU资源,直到以下两个条件之一发生:
a.其他线程调用了与该条件变量关联的pthread_cond_signal 函数,唤醒了等待的线程之一。
b.其他线程调用了与该条件变量关联的pthread_cond_broadcast 函数,唤醒了所有等待的线程。
4.当线程被唤醒后,他会重新获得之前释放的互斥量mutex,并从pthread_cond_wait函数返回。线程可以继续执行接下来的代码。
使用这个函数需要注意以下几点:
在调用pthread_cond_wait之前,确保互斥量mutex已经被锁定,以确保线程等待时的互斥性。
在pthread_cond_wait函数返回之前,互斥量mutex会被重新锁定,因此等待线程重新获得互斥量后可以安全地访问共享资源。
pthread_cond_wait函数必须在循环中使用,以避免虚假唤醒的问题。在被唤醒后,线程应该检查等待的条件是否满足,如果不满足,则继续等待。
还需要知道的是,调用pthread_cond_wait的线程会被放入到一个等待队列中,但是当满足条件的时候,操作系统会根据其调度机制决定唤醒队列中哪一个,这完全是由操作系统决定,并不会明确要唤醒哪个线程。
4.选择一个线程唤醒
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
cond:指向条件变量对象。
该函数用于通知等待在条件变量上的一个线程。他会选择等待队列中的一个线程,并唤醒他以继续执行。被唤醒的线程将重新获得与条件变量相关联的互斥量,并继续执行它们在pthread_cond_wait中阻塞的代码。
5.唤醒所有线程
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
cond:指向条件变量对象。
该函数用于通知等待在条件变量上的所有线程,他会唤醒等待队列中的所有线程,使它们继续执行。被唤醒的线程将重新获得与条件变量相关联的互斥量,并且继续执行它们在pthread_cond_wait函数中阻塞的代码。
以下是简单的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1(void *arg)
{
while (1)
{
pthread_cond_wait(&cond, &mutex);
printf("活动\n");
}
}
void *r2(void *arg)
{
while (1)
{
pthread_cond_signal(&cond);
sleep(1);
}
}
int main(void)
{
pthread_t t1, t2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, r1, NULL);
pthread_create(&t2, NULL, r2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
7.4 为什么pthread_cond_wait需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足。所以必须要有一个线程通过某些操作,改变共享变量,是原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥量来保护,没有互斥量就无法安全的获取和修改共享数据。
按照上面的说法,我们如果先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了?
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经由其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。必须要借助互斥量来实现互斥。
7.5 条件变量使用规范
等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
8.生产者消费者模型
8.1 概念
生产者-消费者模型是一种常见的并发编程模型,用于解决生产者和消费者之间的协作问题。在该模型中,生产者负责生成数据或任务,并将其放入共享缓冲区,而消费者则从共享缓冲区中获取数据或任务进行处理。
该模型的目标是实现生产者和消费者之间的有效同步和通信,以避免生产者生产过快而消费者无法及时处理,或者消费者处理过快而生产者无法及时生成的问题。
以下是生产者-消费者模型的基本思想:
1.共享缓冲区:生产者和消费者之间共享一个缓冲区,用于存放生产者生成的数据或任务。
2.同步机制:使用互斥量和条件变量实现生产者和消费者之间的同步和通信。
3.生产者流程:生产者通过获取互斥量来保护共享缓冲区,然后检查缓冲区是否已满。如果已满,生产者进入等待状态,等待消费者消费部分数据以腾出空间。如果缓冲区未满,生产者将生成的数据放入缓冲区,然后通过条件变量通知消费者由新的数据可用。
4.消费者流程:消费者通过获取互斥量来保护共享缓冲区,然后检查缓冲区是否为空。如果为空,消费者进入等待状态,等待生产者生成数据,如果缓冲区非空,消费者从缓冲区中获取数据进行处理,然后通过条件变量通知生产者有更多的空间可用。
8.2 简单代码示例
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#define NUM 8
class BlockQueue
{
private:
std::queue<int> q;
int cap;
pthread_mutex_t lock;
pthread_cond_t full;
pthread_cond_t empty;
private:
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnLockQueue()
{
pthread_mutex_unlock(&lock);
}
void ProductWait()
{
pthread_cond_wait(&full, &lock);
}
void ConsumeWait()
{
pthread_cond_wait(&empty, &lock);
}
void NotifyProduct()
{
pthread_cond_signal(&full);
}
void NotifyConsume()
{
pthread_cond_signal(&empty);
}
bool IsEmpty()
{
return (q.size() == 0 ? true : false);
}
bool IsFull()
{
return (q.size() == cap ? true : false);
}
public:
BlockQueue(int _cap = NUM) : cap(_cap)
{
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&full, NULL);
pthread_cond_init(&empty, NULL);
}
void PushData(const int &data)
{
LockQueue();
while (IsFull())
{
NotifyConsume();
std::cout << "queue full, notify consume data, product stop." << std::endl;
ProductWait();
}
q.push(data);
// NotifyConsume();
UnLockQueue();
}
void PopData(int &data)
{
LockQueue();
while (IsEmpty())
{
NotifyProduct();
std::cout << "queue empty, notify product data, consume stop." << std::endl;
ConsumeWait();
}
data = q.front();
q.pop();
// NotifyProduct();
UnLockQueue();
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&full);
pthread_cond_destroy(&empty);
}
};
void *consumer(void *arg)
{
BlockQueue *bqp = (BlockQueue *)arg;
int data;
for (;;)
{
bqp->PopData(data);
std::cout << "Consume data done : " << data << std::endl;
}
}
// more faster
void *producter(void *arg)
{
BlockQueue *bqp = (BlockQueue *)arg;
srand((unsigned long)time(NULL));
for (;;)
{
int data = rand() % 1024;
bqp->PushData(data);
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1);
}
}
int main()
{
BlockQueue bq;
pthread_t c, p;
pthread_create(&c, NULL, consumer, (void *)&bq);
pthread_create(&p, NULL, producter, (void *)&bq);
pthread_join(c, NULL);
pthread_join(p, NULL);
return 0;
}
8.3POSIX信号量
POSIX信号量是一种用于进程间或线程间同步和互斥的机制,可以通过增加或减少信号量的计数来控制对共享资源的访问。POSIX信号量提供了更灵活的同步和互斥方式,相比于互斥锁和条件变量,他能更好地适应多线程和多进程地并发环境。
8.3.1 POSIX信号量函数
1.初始化信号量,sem_init函数
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
该函数用与初始化一个信号量。
sem:指向要初始化的信号量对象。
pshared:指定信号量的共享性,为0表示信号量局部于当前进程,非0表示信号量可以在多个进程间共享。
value:指定信号量的初始计数值。
2.销毁信号量,sem_destroy函数
#include <semaphore.h>
int sem_destroy(sem_t *sem);
sem:指向要销毁的信号量对象。
3.增加信号量,sem_post函数
#include <semaphore.h>
int sem_post(sem_t *sem);
sem:指向要增加的信号量对象。
4.减少信号量,sem_wait函数
#include <semaphore.h>
int sem_wait(sem_t *sem);
该函数用于减少信号量的计数,如果计数为0,则阻塞等待。
sem:指向要减少的信号量对象。
8.4 基于环形队列的生产消费模型
环形队列采用数组模拟,用模运算来模拟环状特性
环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。
但是我们现在有信号量这个计数器,就可以很简单的进行多线程间的同步过程。
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#define NUM 16
class RingQueue
{
private:
std::vector<int> q;
int cap;
sem_t data_sem;
sem_t space_sem;
int consume_step;
int product_step;
public:
RingQueue(int _cap = NUM) : q(_cap), cap(_cap)
{
sem_init(&data_sem, 0, 0);
sem_init(&space_sem, 0, cap);
consume_step = 0;
product_step = 0;
}
void PutData(const int &data)
{
sem_wait(&space_sem); // P
q[consume_step] = data;
consume_step++;
consume_step %= cap;
sem_post(&data_sem); // V
}
void GetData(int &data)
{
sem_wait(&data_sem);
data = q[product_step];
product_step++;
product_step %= cap;
sem_post(&space_sem);
}
~RingQueue()
{
sem_destroy(&data_sem);
sem_destroy(&space_sem);
}
};
void *consumer(void *arg)
{
RingQueue *rqp = (RingQueue *)arg;
int data;
for (;;)
{
rqp->GetData(data);
std::cout << "Consume data done : " << data << std::endl;
sleep(1);
}
}
// more faster
void *producter(void *arg)
{
RingQueue *rqp = (RingQueue *)arg;
srand((unsigned long)time(NULL));
for (;;)
{
int data = rand() % 1024;
rqp->PutData(data);
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1);
}
}
int main()
{
RingQueue rq;
pthread_t c, p;
pthread_create(&c, NULL, consumer, (void *)&rq);
pthread_create(&p, NULL, producter, (void *)&rq);
pthread_join(c, NULL);
pthread_join(p, NULL);
}
上述这段代码编译而成的可执行文件运行之后就是下面的结果。可以看到消费者线程和生产者线程在交替运行。
上述代码首先定义了用一个RingQueue类,其中包含了一个用于存储数据的std::vector对象 q ,一个表示队列容量的变量cap,两个信号量data_sem和space_sem,以及两个指示当前消费位置和生产位置的变量consume_step和product_step。
在RingQueue的构造函数中,通过sem_init分别对data_sem和space_sem进行初始化。data_sem的初始值为0,表示初始时没有可消费的数据,space_sem的初始值为队列的容量,表示初始时队列中所有的空间全部空闲可供生产者生产,因为一开始生产者还没有开始生产。也就没有数据供消费和占据队列的空间。
PutData函数用于生产者线程向队列中放入数据。它先通过sem_wait操作对space_sem进行P操作,消耗一个空闲的空间,供给生产。然后将数据放入环形队列的消费位置,并更新消费位置consume_step。最后通过sem_post操作对data_sem进行V操作,即增加一个data_sem信号量,表示有新的数据可供消费,因为这时候生产者已经生产了一份数据。
GetData函数用于消费者线程从队列中获取数据。他先通过sem_wait操作对data_sem进行P操作,即消耗一个data_sem信号量,然后从环形队列的生产位置取出数据,并更新生产位置product_step。最后通过sem_post操作对space_sem进行V操作,即释放一个space_sem信号量,表示有新的空闲空间可供生产。
consumer函数时消费者线程的入口函数。在一个无限循环中,它调用GetData函数从队列中获取数据,并进行处理。
producter函数是生产者线程的入口函数。在一个无线循环中,它生成随机数据,并调用PutData函数将数据放入队列。
在main函数中,首先创建了一个RingQueue对象rq,然后创建了消费者线程c和生产者线程p,将rq作为参数传递给它们。最后,使用pthread_join函数等待两个线程的结束,以释放两个线程的资源。
通过使用信号量,上述代码实现了生产者消费者模型的同步和互斥,当队列的空闲空间用完时,生产者线程会等待,当队列中没有可消费的数据时,消费者线程会等待。这样可以避免数据竞争和资源冲突,确保生产者和消费者之间的正确协调和数据处理。
9. 读者写者问题
9.1 读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高得多,通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢?读写锁就是用来专门处理这种情况的。
9.2 读写锁接口
1.初始化读写锁,pthread_rwlock_init函数
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
rwlock:指向要初始化的读写锁对象。
attr:指定读写锁的属性,通常传入 NULL 使用默认属性。
2.销毁读写锁,pthread_rwlock_destroy函数
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
rwlock:指向要销毁的读写锁对象。
3.获取读取的锁,pthread_rwlock_rdlock函数
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
rwlock:指向要获取读取锁的读写锁对象。
4.获取写入的锁,pthread_rwlock_wrlock函数
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
rwlock:指向要获取写入锁的读写锁对象。
5.释放读写锁,pthread_rwlock_unlock函数
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
rwlock:指向要释放的读写锁对象
6.设置读写优先,pthread_rwlockattr_setkind_np函数
#include <pthread.h>
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
attr:指向读写锁属性对象的指针。
pref:设置读写优先级的值。可选的取值为:
PTHREAD_RWLOCK_PREFER_READER_NP:优先选择读取操作。
PTHREAD_RWLOCK_PREFER_WRITER_NP:优先选择写入操作。
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:优先选择写入操作,并且不允许递归写入。
该函数用于设置读写锁属性对象的读写优先级,以指定读取操作或写入操作的优先级。
需要注意的是,该函数是POSIX扩展函数,并不是标准POSIX线程库中定义的函数。因此,它的可移植性有限。
9.3 读者写者模型
#include <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <cstddef>
volatile int ticket = 1000;
pthread_rwlock_t rwlock;
void *reader(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_rwlock_rdlock(&rwlock);
if (ticket <= 0)
{
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
void *writer(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_rwlock_wrlock(&rwlock);
if (ticket <= 0)
{
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, --ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
struct ThreadAttr
{
pthread_t tid;
std::string id;
};
std::string create_reader_id(std::size_t i)
{
std::ostringstream oss("thread reader ", std::ios_base::ate);
oss << i;
return oss.str();
}
std::string create_writer_id(std::size_t i)
{
std::ostringstream oss("thread writer ", std::ios_base::ate);
oss << i;
return oss.str();
}
void init_readers(std::vector<ThreadAttr> &vec)
{
for (std::size_t i = 0; i < vec.size(); ++i)
{
vec[i].id = create_reader_id(i);
pthread_create(&vec[i].tid, nullptr, reader, (void *)vec[i].id.c_str());
}
}
void init_writers(std::vector<ThreadAttr> &vec)
{
for (std::size_t i = 0; i < vec.size(); ++i)
{
vec[i].id = create_writer_id(i);
pthread_create(&vec[i].tid, nullptr, writer, (void *)vec[i].id.c_str());
}
}
void join_threads(std::vector<ThreadAttr> const &vec)
{
for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); it !=
vec.rend();
++it)
{
pthread_t const &tid = it->tid;
pthread_join(tid, nullptr);
}
}
void init_rwlock()
{
#if 0 // 写优先
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿
pthread_rwlock_init(&rwlock, nullptr);
#endif
}
int main()
{
const std::size_t reader_nr = 1000;
const std::size_t writer_nr = 2;
std::vector<ThreadAttr> readers(reader_nr);
std::vector<ThreadAttr> writers(writer_nr);
init_rwlock();
init_readers(readers);
init_writers(writers);
join_threads(writers);
join_threads(readers);
pthread_rwlock_destroy(&rwlock);
return 0;
}
reader() 函数是读者线程的入口函数,它通过调用 pthread_rwlock_rdlock() 获取读锁,读取 ticket 的值并打印出来,然后释放读锁。如果 ticket 的值小于等于 0,表示没有剩余的票可供读取,读者线程退出。
writer() 函数是写者线程的入口函数,它通过调用 pthread_rwlock_wrlock() 获取写锁,递减 ticket 的值并打印出来,然后释放写锁。如果 ticket 的值小于等于 0,表示没有剩余的票可供写入,写者线程退出。
create_reader_id() 和 create_writer_id() 函数用于生成读者线程和写者线程的标识。
init_readers() 函数初始化读者线程,根据指定的数量创建读者线程,并为每个读者线程分配一个唯一的标识。
init_writers() 函数初始化写者线程,根据指定的数量创建写者线程,并为每个写者线程分配一个唯一的标识。
join_threads() 函数等待所有读者线程和写者线程结束。
init_rwlock() 函数初始化读写锁。根据预定义的宏选择读优先还是写优先。
10. 线程池
10.1概念
线程池是一种用于管理和复用线程的技术。他是一组预先创建的线程,用于执行任务队列中的工作,而不是为每个任务都创建一个新的线程。线程池维护了一定数量的线程,这些线程可以重复使用,从而避免了频繁创建和销毁线程的开销。
线程池的主要目的是通过有效的管理线程来提高系统的性能和资源利用率。线程池具有以下优点:
资源管理:线程池可以限制系统中线程的数量,防止线程过多导致系统资源耗尽的问题。它可以根据系统负载情况自动调整线程数目。
提高响应性:由于线程池中的线程已经预先创建并准备好,可以立即响应任务的到达。这消除了线程创建和销毁的开销,减少了任务等待的时间。
提高系统性能:通过复用线程,线程池避免了频繁创建和销毁线程的开销,从而提高了系统的性能。此外,线程池还可以根据任务的优先级和类型进行调度,使得高优先级的任务能够得到更快的处理。
控制并发:线程池可以限制并发执行的任务数量,以防止系统过载。通过控制线程的数量和任务队列的大小,可以有效地管理系统资源,避免因并发量过大而导致系统崩溃。
10.2 线程池的应用场景
1.需要大量的线程来完成任务,且完成任务的时间比较短。WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为的那个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2.对相应要求苛刻的任务,比如服务器迅速响应客户请求。
3.接手突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误