16. 线程处理
一、什么是线程
线程(Thread)是进程内的一个执行单元,它共享相同的地址空间和其它资源(包括文件描述符、信号处理等),但每个线程都有自己的栈空间。相比于进程而言,线程的创建、销毁、切换等操作要操作的资源消耗小很多。由于线程共享地址空间和数据段,因此同一进程的多进程之间数据交互比进程间通信要方便很多,但也由此带来了线程同步的问题。
二、线程的基本操作
2.1、线程的创建
我们可以使用 pthread_create() 函数 创建一个线程。
/**
* @brief 创建一个线程
*
* @param __newthread 指向线程标识符的指针
* @param __attr 线程属性的结构体
* @param __start_routine 新线程的开始执行的入口
* @param __arg 新线程的入口参数
* @return int 0表示成功,否则表示失败
*/
int pthread_create(pthread_t *__restrict __newthread, const pthread_attr_t *__restrict __attr, void *(*__start_routine) (void *), void *__restrict __arg);
参数 __newthread 是 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符。
参数 __attr 是 线程属性的结构体。如果不需要指定线程属性,可以传入 NULL,此时线程将会采用默认属性。
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};
typedef union pthread_attr_t pthread_attr_t;
参数 __start_routine 是一个 void *() (void *) 类型的函数指针,它定义了新线程开始时执行时的入口点。这个函数必须接收一个 void * 类型的参数,并返回 void * 类型的结构。
参数 __arg 是新线程的入口参数,可以是一个指向任何类型数据的指针。
2.2、等待指定线程的结束
我们可以通过 pthread_join() 函数 等待指定线程的结束。
/**
* @brief 等待指定线程的结束
*
* @param __th 指定线程ID
* @param __thread_return 用于接收线程结束后传递的返回值
* @return int 0: 成功 其它: 失败的错误码
*/
int pthread_join(pthread_t __th, void **__thread_return);
参数 __thread_return 是一个可选参数,它可以用于 接收线程结束后传递的返回值。如果非空,pthread_join() 会在成功是将线程的 exit status 复制到 *retval 所指向的内存位置。如果线程没有显示通过 pthread_exit() 提供返回值,则该参数可以设置为 NULL 或忽略。
2.3、线程分离
我们可以通过 pthread_detach() 函数 使主线程与子线程分离。
/**
* @brief 线程分离
*
* @param __th 指定线程的ID
* @return int 0:成功,其他:失败的错误码
*/
int pthread_detach(pthread_t __th);
POSIX 线程终止后,如果没有调用 pthread_detach() 函数或 pthread_join() 函数,其它资源会继续占用内存,类似于僵尸进程的未回收状态。默认情况下创建线程后,它处于可 join 状态,此时可以调用 pthread_join() 等待线程终止并回收资源。但是如果主线程不需要等待线程终止,可以将其标记为 detached 状态,这意味着线程终止后,其资源会自动被系统回收。
2.4、线程的终止
我们可以使用 pthread_exit() 方法 终止线程。
/**
* @brief 线程的终止
*
* @param __retval 要返回给其它线程的数据
*/
void pthread_exit(void *__retval);
当某个线程调用 pthread_exit() 方法后,该线程会被关闭(相当于 return 语句)。线程可以通过 retval 向其它线程传递消息,retval 指向的区域不可以放在线程函数的栈内。其它线程如果需要获得这个返回值,需要调用 pthread_join() 方法。
2.5、取消线程
我们还可以在其它线程中调用 pthread_cancel() 函数 取消指定线程的执行。
/**
* @brief 取消指定线程的执行
*
* @param __th 指定线程的ID
* @return int 0:成功,-1:失败
*/
int pthread_cancel(pthread_t __th);
当我们向目标线程发送取消请求后,目标线程是否响应和何时响应取决于它的 取消状态 和 类型。
取消状态(Cancelabiliity State)可以是 enable(默认)或 disabled。如果取消状态为禁用,则取消请求会被挂起,直至线程启动取消功能。如果取消状态为启动,则线程的取消类型决定它何时取消。
取消类型(Cancelablity Type)可以是 asynchronous(异步)或 deferred(被推迟,默认值)。asynchronous 意味着线程可以在任何时候被取消(通常立即被取消,但系统并不保证这一点)。deferred 意味着取消请求会被挂起,直至被取消的线程执行取消点函数时才会真正执行线程的取消操作。
取消点函数 是在 POSIX 线程库中专门设计用于检查和处理请求的函数。当被取消的线程执行这些操作时,如果线程的取消状态是 enabled 且类型是 deferred,则它会立即响应取消请求并终止执行。
取消操作和 pthread_cancel() 函数的调用是异步的,这个函数的返回值只能高数调用者取消请求是否发送成功。当线程被陈成功取消后,通过 pthread_join() 和线程关联将会获得 PTHREAD_CANCELED 作为返回信息,这是判断取消是否完成的唯一方式。
三、线程的同步
当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步措施,就会遇到线程同步问题。这种情况下,程序最终结果依赖于线程执行的具体时序,这就导致了竞态条件。
竞态条件(rece condition)是一种特定的线程同步问题,指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖与进程或者线程执行的精确时序。它会导致程序的行为和输出超出预期,因为共享资源的最终状态取决于执行的顺序和时机。为了确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件。
为了避免竞态条件,我们可以给共享资源加上锁,使同一时间操作共享资源的线程只有一个,常见的锁机制如下:
- 互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
- 读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问是互斥的。
- 自旋锁(Spinlocks):在获取锁之前,线程在循环中等待,适用于锁持有时间非常短的场景。
3.1、互斥锁
首先,我们可以使用 pthread_mutex_init() 函数来 初始化互斥锁。
/**
* @brief 初始化互斥锁
*
* @param __mutex 互斥锁
* @param __mutexattr 互斥锁的属性,当属性为NULL时,可通过PTHREAD_MUTEX_INITIALIZER宏初始化
* @return int 0:成功 -1:失败
*/
int pthread_mutex_init(pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr);
在操作共享资源之前,我们可以使用 pthread_mutex_lock() 函数或 pthread_mutex_trylock() 函数来 获取互斥锁。
/**
* @brief 获取互斥锁,如果互斥锁已经被锁定,则阻塞
*
* @param __mutex 互斥锁
* @return int 0:成功 其它:失败
*/
int pthread_mutex_lock(pthread_mutex_t *__mutex);
/**
* @brief 获取互斥锁,如果互斥锁已经被锁定,则返回 EBUSY
*
* @param __mutex 互斥锁
* @return int 0:成功 EBUSY: 锁被占用
*/
int pthread_mutex_trylock(pthread_mutex_t *__mutex);
在操作完共享资源之后,我们需要使用 pthread_mutex_unlock() 函数来 释放互斥锁。
/**
* @brief 释放互斥锁
*
* @param __mutex 互斥锁
* @return int 0:成功 其它:失败
*/
int pthread_mutex_unlock(pthread_mutex_t *__mutex);
最后,我们需要调用 pthread_mutex_destroy() 函数来 销毁互斥锁。
/**
* @brief 销毁互斥锁
*
* @param __mutex 互斥锁
* @return int 0:成功 其它:失败
*/
int pthread_mutex_destroy(pthread_mutex_t *__mutex);
我们新建一个 main.c 文件。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
// 初始化锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *sell(void *argv);
int main(void)
{
pthread_t pid[3] = {0};
char name[3][30] = {0};
// 创建线程
for (int i = 0; i < sizeof(pid) / sizeof(pid[0]); i++)
{
sprintf(name[i], "window %d", i + 1);
pthread_create(&pid[i], NULL, sell, name[i]);
}
// 主线程等待线程结束
for (int i = 0; i < sizeof(pid) / sizeof(pid[0]); i++)
{
pthread_join(pid[i], NULL);
}
// 销毁锁
pthread_mutex_destroy(&lock);
return 0;
}
void *sell(void *argv)
{
char *name = (char *)argv;
while(ticket > 0)
{
// 获取锁
pthread_mutex_lock(&lock);
if (ticket > 0)
{
printf("%s sell the number is %d ticket!\n", name, ticket);
ticket--;
}
// 释放锁
pthread_mutex_unlock(&lock);
}
}
新建一个 Makefile 文件,它的内容如下:
CC := gcc
# $@ 表示目标文件名称 $^ 表示所有的依赖文件
main: main.c
- $(CC) $^ -o $@
- ./$@
在终端中输入 make 运行。
make
3.2、读写锁
在读写锁的控制下,多个线程可以同时获取读锁。这些线程可以并发地读取共享资源,但它们的存在阻止了写锁的授予。如果至少有一个读操作持有读锁,写操作就无法获得写锁。写操作将会阻塞,直到所有的读操作都被释放。
首先,我们可以使用 pthread_rwlock_init() 函数来 初始化读写锁。
/**
* @brief 初始化读写锁
*
* @param __rwlock 读写锁
* @param __attr 读写锁的属性,当属性为NULL时,可通过PTHREAD_RWLOCK_INITIALIZER宏初始化
* @return int 0:成功 -1:失败
*/
int pthread_rwlock_init(pthread_rwlock_t *__restrict __rwlock, const pthread_rwlockattr_t *__restrict __attr);
在读取共享资源之前,我们可以使用 pthread_rwlock_rdlock() 函数或 pthread_rwlock_tryrdlock() 函数来 获取读锁。
/**
* @brief 获取读锁
*
* @param __rwlock 读写锁
* @return int 0:成功 其它:失败
*/
int pthread_rwlock_rdlock(pthread_rwlock_t *__rwlock);
/**
* @brief 获取读锁
*
* @param __rwlock 读写锁
* @return int 0:成功 其它:失败
*/
int pthread_rwlock_tryrdlock(pthread_rwlock_t *__rwlock);
在操作共享资源之前,我们可以使用 pthread_rwlock_wrlock() 函数或 pthread_rwlock_trywrlock() 函数来 获取写锁。
/**
* @brief 获取写锁,如果任意读锁或者写锁已经被锁定,则阻塞
*
* @param __rwlock 读写锁
* @return int 0:成功 其它:失败
*/
int pthread_rwlock_wrlock(pthread_rwlock_t *__rwlock);
/**
* @brief 获取写锁,任意读锁或者写锁已经被锁定,则返回 EBUSY
*
* @param __rwlock 读写锁
* @return int 0:成功 EBUSY: 锁被占用
*/
int pthread_rwlock_trywrlock(pthread_rwlock_t *__rwlock);
在操作完共享资源之后,我们需要使用 pthread_rwlock_unlock() 函数来 释放读写锁。
/**
* @brief 释放读写锁
*
* @param __mutex 读写锁
* @return int 0:成功 其它:失败
*/
int pthread_rwlock_unlock(pthread_rwlock_t *__rwlock);
最后,我们需要调用 pthread_rwlock_destroy() 函数来 销毁读写锁。
/**
* @brief 销毁读写锁
*
* @param __rwlock 读写锁
* @return int 0:成功 其它:失败
*/
int pthread_rwlock_destroy(pthread_rwlock_t *__rwlock);
但读写锁可能会造成写饥饿问题。写饥饿(Writer Starvation)是指在使用读写锁时,写线程可能无限期地等待获取写锁,因为读线程持续地获取读锁而不断推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_rwlock_t rwlock;
int shared_data = 0;
void *lock_read(void *argv);
void *lock_write(void *argv);
int main(void)
{
pthread_t writer[2] = {0};
pthread_t reader[6] = {0};
// 初始化锁
pthread_rwlock_init(&rwlock, NULL);
// 创建线程
pthread_create(&writer[0], NULL, lock_write, "writer 1");
pthread_create(&reader[0], NULL, lock_read, "reader 1");
pthread_create(&reader[1], NULL, lock_read, "reader 2");
pthread_create(&reader[2], NULL, lock_read, "reader 3");
pthread_create(&writer[1], NULL, lock_write, "writer 2");
pthread_create(&reader[3], NULL, lock_read, "reader 4");
pthread_create(&reader[4], NULL, lock_read, "reader 5");
pthread_create(&reader[5], NULL, lock_read, "reader 6");
// 主线程等待线程结束
for (int i = 0; i < sizeof(writer) / sizeof(writer[0]); i++)
{
pthread_join(writer[i], NULL);
}
for (int i = 0; i < sizeof(reader) / sizeof(reader[0]); i++)
{
pthread_join(reader[i], NULL);
}
// 销毁锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
void *lock_read(void *argv)
{
printf("thread %s want to acquire a read lock.\n", (char *)argv);
// 获取读锁
pthread_rwlock_rdlock(&rwlock);
printf("thread %s acquire read lock, the shared data is %d.\n", (char *)argv, shared_data);
sleep(1);
// 释放读锁
pthread_rwlock_unlock(&rwlock);
printf("thread %s release read lock.\n", (char *)argv);
}
void *lock_write(void *argv)
{
printf("thread %s want to acquire a write lock.\n", (char *)argv);
// 获取写锁
pthread_rwlock_wrlock(&rwlock);
sleep(1);
shared_data++;
printf("thread %s acquire write lock, the shared data is %d.\n", (char *)argv, shared_data);
// 释放写锁
pthread_rwlock_unlock(&rwlock);
printf("thread %s release write lock.\n", (char *)argv);
}
在终端中输入 make 运行程序,我们可以发现,writer[1] 永远都在最后才能获得写锁。
在 Linux 中,提供了可以修改的属性 pthread_rwloadattr_t。默认情况下,属性中指定的策略为 “读优先”,当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为 “写优先”,当写操作阻塞时,读线程无法获取锁,避免了写线程持有锁的时间持续延长,使得写线程获取锁的等待时间显著降低,从而避免写饥饿问题。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_rwlock_t rwlock;
int shared_data = 0;
void *lock_read(void *argv);
void *lock_write(void *argv);
int main(void)
{
pthread_t writer[2] = {0};
pthread_t reader[6] = {0};
// 初始化读写锁对象
pthread_rwlockattr_t attr = {0};
pthread_rwlockattr_init(&attr);
// 修改参数,设置写优先
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
// 初始化锁
pthread_rwlock_init(&rwlock, &attr);
// 创建线程
pthread_create(&writer[0], NULL, lock_write, "writer 1");
pthread_create(&reader[0], NULL, lock_read, "reader 1");
pthread_create(&reader[1], NULL, lock_read, "reader 2");
pthread_create(&reader[2], NULL, lock_read, "reader 3");
pthread_create(&writer[1], NULL, lock_write, "writer 2");
pthread_create(&reader[3], NULL, lock_read, "reader 4");
pthread_create(&reader[4], NULL, lock_read, "reader 5");
pthread_create(&reader[5], NULL, lock_read, "reader 6");
// 主线程等待线程结束
for (int i = 0; i < sizeof(writer) / sizeof(writer[0]); i++)
{
pthread_join(writer[i], NULL);
}
for (int i = 0; i < sizeof(reader) / sizeof(reader[0]); i++)
{
pthread_join(reader[i], NULL);
}
// 销毁锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
void *lock_read(void *argv)
{
printf("thread %s want to acquire a read lock.\n", (char *)argv);
// 获取读锁
pthread_rwlock_rdlock(&rwlock);
printf("thread %s acquire read lock, the shared data is %d.\n", (char *)argv, shared_data);
sleep(1);
// 释放读锁
pthread_rwlock_unlock(&rwlock);
printf("thread %s release read lock.\n", (char *)argv);
}
void *lock_write(void *argv)
{
printf("thread %s want to acquire a write lock.\n", (char *)argv);
// 获取写锁
pthread_rwlock_wrlock(&rwlock);
sleep(1);
shared_data++;
printf("thread %s acquire write lock, the shared data is %d.\n", (char *)argv, shared_data);
// 释放写锁
pthread_rwlock_unlock(&rwlock);
printf("thread %s release write lock.\n", (char *)argv);
}
在终端中输入 make 运行。
3.3、自旋锁
在 Linux 内核中,自旋锁是一种用于多处理器系统的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。自旋锁相对于其它锁的优点是它们在锁被占用时会持续检查锁的状态(即 “自选”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免上下文切换的开销。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销,不能在用户空间使用。
四、死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的 死锁;出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
// 初始化锁
pthread_mutex_t lock1;
pthread_mutex_t lock2;
void task1(void *argv);
void task2(void *argv);
int main(void)
{
pthread_t pid1 = 0;
pthread_t pid2 = 0;
// 创建线程
pthread_mutex_init(&pid1, NULL);
pthread_create(&pid1, NULL, task1, NULL);
pthread_mutex_init(&pid2, NULL);
pthread_create(&pid2, NULL, task2, NULL);
// 主线程等待线程结束
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
// 销毁锁
pthread_mutex_destroy(&lock1);
pthread_mutex_destroy(&lock2);
return 0;
}
void task1(void *argv)
{
pthread_mutex_lock(&lock1);
printf("task 1 acqure lock A.\n");
sleep(1);
pthread_mutex_lock(&lock2);
printf("task 1 acquire lock B.\n");
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
}
void task2(void *argv)
{
pthread_mutex_lock(&lock2);
printf("task 2 acqure lock B.\n");
sleep(1);
pthread_mutex_lock(&lock1);
printf("task 2 acquire lock 1.\n");
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
}
在终端中输入 make 运行。
五、条件变量
在多线程编程中,多个线程可能会因为竞争资源而导致死锁,一旦产生死锁,程序将无法继续运行。为了解决死锁问题,我们可以使用条件变量在满足某种条件下暂时释放锁,用于实现线程间通信,避免产生死锁。
在 C99 标准中引入了一个新的关键字 restrict,该关键字用于修饰指针,它的作用是告诉编译器,被修饰的指针是编译器所知的唯一一个可以在其内部作用域内用来访问指针所指向的对象的方法。这样一来,编译器可以放心地执行代码优化,因为不存在其它的别名(即其它指向同一内存区域的指针)会影响这块内存的状态。
首先,我们需要使用 pthread_cond_init() 函数来 初始化条件。
/**
* @brief 初始化条件
*
* @param __cond 指向条件变量的指针
* @param __cond_attr 条件的属性,当属性为NULL时,可通过PTHREAD_COND_INITIALIZER宏初始化
* @return int 成功返回0,失败返回错误码
*/
int pthread_cond_init(pthread_cond_t *__restrict __cond, const pthread_condattr_t *__restrict __cond_attr);
然后,我们需要在持有互斥锁的线程中调用 pthread_cond_wait() 函数或 pthread_cond_timedwait() 函数 临时释放互斥锁。
/**
* @brief 临时释放锁
*
* @param __cond 指向条件条件变量的指针
* @param __mutex 互斥锁
* @return int 0 成功,其它 失败时错误码
*/
int pthread_cond_wait(pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex);
/**
* @brief 临时释放锁
*
* @param __cond 指向条件条件变量的指针
* @param __mutex 互斥锁
* @param __abstime 超时时间
* @return int 成功返回0,超时返回ETIMEOUT,其它错误时返回对应的错误码
*/
int pthread_cond_timedwait(pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex, const struct timespec *__restrict __abstime);
调用 pthread_cond_wait() 方法的线程必须持有互斥锁。调用该方法的线程会阻塞并临时释放互斥锁,并等待其它线程调用 pthread_cond_signal()或 pthread_cond_boradcast() 唤醒。被唤醒后,该线程会尝试重新获取互斥锁。
pthread_cond_timedwait() 函数与 pthread_cond_wait() 函数类似,只是它添加了超时机制。如果在指定的时间内没有被触发,该函数将会返回一个超时错误(ETIMEOUT)。
然后我们可以使用 pthread_cond_signal() 函数或 pthread_cond_broadcast() 函数 唤醒线程。
/**
* @brief 随机唤醒任意一个满足条件正在等待的线程
*
* @param __cond 指向条件变量的指针
* @return int 成功返回0,失败返回错误码
*/
int pthread_cond_signal(pthread_cond_t *__cond);
/**
* @brief 唤醒所有满足条件正在等待的线程
*
* @param __cond 指向条件变量的指针
* @return int 成功返回0,失败返回错误码
*/
int pthread_cond_broadcast(pthread_cond_t *__cond);
最后,我们使用 pthread_cond_destroy() 函数 销毁条件。
/**
* @brief 销毁条件
*
* @param __cond 指向条件变量的指针
* @return int 成功返回0,失败返回错误码
*/
int pthread_cond_destroy(pthread_cond_t *__cond);
修改 main.c 文件中的内容。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define FACTORY_MAX_CAPACITY 10
#define PRODUCT_MAX_COUNT 100
// 初始化锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 初始化条件变量
pthread_cond_t condition = PTHREAD_COND_INITIALIZER;
int product_number = 0;
int factory_current_product_count = 0;
int product[FACTORY_MAX_CAPACITY];
void *producer(void *argv);
void *customer(void *argv);
int main(void)
{
pthread_t pid_producer = 0;
pthread_t pid_customer = 0;
// 创建线程
pthread_create(&pid_producer, NULL, producer, NULL);
pthread_create(&pid_customer, NULL, customer, NULL);
// 主线程等待线程结束
pthread_join(pid_producer, NULL);
pthread_join(pid_customer, NULL);
// 销毁条件
pthread_cond_destroy(&condition);
// 销毁锁
pthread_mutex_destroy(&lock);
return 0;
}
void *producer(void *argv)
{
for (product_number = 0; product_number < PRODUCT_MAX_COUNT; product_number++)
{
// 获取互斥锁
pthread_mutex_lock(&lock);
// 如果达到工厂最大生产容量
if (factory_current_product_count == FACTORY_MAX_CAPACITY)
{
// 暂停线程
pthread_cond_wait(&condition, &lock);
}
// 如果没有达到工厂最大生产容量,则生产
product[factory_current_product_count++] = product_number + 1;
printf("factory produce product (%d).\n", product_number + 1);
// 唤醒消费者继续消费
pthread_cond_signal(&condition);
// 释放锁
pthread_mutex_unlock(&lock);
}
}
void *customer(void *argv)
{
while (1)
{
// 如果工厂中的产品数为0并且已经生产到了最后一个产品,则退出循环
if (factory_current_product_count < 0 && product_number == PRODUCT_MAX_COUNT)
{
break;
}
// 获取互斥锁
pthread_mutex_lock(&lock);
// 如果达到工厂中没有产品
if (factory_current_product_count < 0)
{
// 暂停线程
pthread_cond_wait(&condition, &lock);
}
// 如果工厂中有产品,则消费
printf("consumer consume product (%d).\n", product[--factory_current_product_count]);
// 通知生产者继续生产
pthread_cond_signal(&condition);
// 释放锁
pthread_mutex_unlock(&lock);
}
}
在终端中输入 make 运行。

浙公网安备 33010602011771号