C语言多线程编程:并发控制与线程安全实战

一、多线程编程:并发控制与线程安全实战

在这里插入图片描述

1.1 本章学习目标与重点

💡 理解C语言多线程的核心概念(线程、进程、并发、并行),掌握多线程编程的优势与适用场景;
💡 熟练运用POSIX线程库(pthread)核心API,实现线程创建、退出、等待、取消等基础操作;
💡 掌握线程同步与互斥机制(互斥锁、条件变量、信号量),解决并发访问共享资源的竞态问题;
💡 理解线程安全的核心原则,规避死锁、活锁、优先级反转等常见并发问题;
💡 结合实战案例,实现多线程数据处理、生产者-消费者模型、线程池等工程常用功能,提升并发编程能力。

在多核处理器普及的今天,多线程编程已成为C语言开发的核心技能之一。通过多线程,程序可以同时执行多个任务,充分利用CPU资源,提升程序响应速度和处理效率。本章从多线程基础概念入手,系统讲解线程操作API、同步互斥机制、线程安全设计等核心知识点,帮助你从“单线程思维”转向“并发思维”,写出高效、安全的多线程C语言程序。

1.2 多线程核心概念解析

在学习具体编程技巧之前,我们需要先理清多线程的核心概念,明确线程与进程的差异、并发与并行的区别,这是理解后续内容的基础。

1.2.1 线程与进程的本质差异

1. 核心定义
  • 进程:操作系统资源分配的基本单位,是程序运行的独立实例。每个进程拥有独立的内存空间(代码段、数据段、堆区、栈区)、文件描述符等资源,进程间切换开销较大。
  • 线程:进程内的执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,所有线程共享进程的内存空间和资源(如全局变量、文件描述符),线程间切换开销远小于进程。
2. 核心差异对比
对比维度进程(Process)线程(Thread)
资源分配独立资源空间(内存、文件描述符等)共享所属进程的资源空间
调度单位操作系统调度的最小单位(资源分配单位)CPU调度的最小单位(执行单元)
切换开销大(需切换地址空间、保存进程上下文)小(仅需保存线程上下文,共享进程资源)
通信方式复杂(管道、消息队列、共享内存等)简单(直接访问共享变量、全局数据)
稳定性高(一个进程崩溃不影响其他进程)低(一个线程崩溃可能导致整个进程崩溃)
适用场景独立程序运行(如浏览器、编辑器)并发执行多个任务(如服务器多客户端处理、数据并行计算)
3. 示例理解:进程与线程的关系

以“浏览器”为例:

  • 打开一个浏览器,操作系统会创建一个浏览器进程,分配独立内存空间;
  • 浏览器的“多个标签页”可视为多个线程,它们共享浏览器进程的资源(如网络连接、缓存数据);
  • 关闭一个标签页(线程退出),不影响其他标签页和浏览器主进程;
  • 若浏览器进程崩溃,所有标签页(线程)都会终止。

1.2.2 并发与并行的区别

  • 并发(Concurrency):多个任务在同一时间段内交替执行,看似“同时进行”。例如,单核CPU上的多线程程序,CPU通过快速切换线程实现并发。
  • 并行(Parallelism):多个任务在同一时刻同时执行,真正的“同时进行”。例如,多核CPU上的多线程程序,每个核心执行一个线程,实现并行。

✅ 结论:并发是“交替执行”,并行是“同时执行”;多线程编程既支持并发(单核CPU),也支持并行(多核CPU),核心价值是提升程序的执行效率和响应速度。

1.2.3 多线程编程的优势与风险

1. 核心优势
  • 充分利用多核CPU资源:多核环境下,多线程可并行执行,大幅提升CPU利用率;
  • 提升程序响应速度:耗时任务(如网络IO、文件读写)可放在后台线程执行,主线程保持响应;
  • 简化复杂任务编程:将复杂任务拆分为多个独立子任务,每个线程负责一个子任务,降低编程复杂度;
  • 减少资源消耗:相比多进程,线程共享资源,创建和切换开销小,可创建更多线程处理任务。
2. 潜在风险
  • 竞态条件(Race Condition):多个线程并发访问共享资源(如全局变量),导致数据不一致;
  • 死锁(Deadlock):多个线程互相等待对方释放资源,导致所有线程阻塞;
  • 线程安全问题:非线程安全的函数或数据结构在多线程环境下使用,导致逻辑错误;
  • 调试难度高:多线程程序的执行顺序不确定,问题难以复现和排查。

💡 开发建议:多线程编程的核心是“扬长避短”——利用多线程提升效率,同时通过同步互斥机制规避并发风险。

1.2.4 C语言多线程编程的实现方式

C语言本身没有内置多线程支持,需依赖操作系统提供的线程库:

  • POSIX线程库(pthread):适用于Linux、Unix、macOS等类Unix系统,是最常用的C语言多线程实现方式;
  • Windows线程库:适用于Windows系统,提供CreateThread等API,与POSIX线程库语法不同;
  • 跨平台线程库:如Boost.Thread、Qt Thread,封装了不同系统的线程API,支持跨平台开发。

本章重点讲解POSIX线程库(pthread),其API标准化程度高、应用广泛,是C语言多线程编程的必备技能。

1.3 POSIX线程库(pthread)核心API详解

POSIX线程库(简称pthread)提供了一套完整的多线程操作API,涵盖线程创建、退出、等待、同步互斥等功能。使用时需包含头文件<pthread.h>,编译时需链接线程库(GCC编译器添加-pthread选项)。

1.3.1 线程创建:pthread_create

1. 函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   void *(*start_routine)(void *), void *arg);
2. 参数说明
  • thread:输出参数,用于存储新创建线程的ID(pthread_t类型,本质是无符号长整型);
  • attr:线程属性(如栈大小、优先级),通常设为NULL,使用默认属性;
  • start_routine:线程入口函数(函数指针),线程创建后会执行该函数,格式为void *func(void *arg)
  • arg:传递给线程入口函数的参数,若需传递多个参数,可封装为结构体指针。
3. 返回值
  • 成功:返回0;
  • 失败:返回非0错误码(不同错误对应不同值,可通过strerror函数获取错误描述)。
4. 用法示例:创建简单线程
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

// 线程入口函数:打印线程ID和传入的参数
void *thread_func(void *arg) {
    // 获取线程ID(pthread_self()返回当前线程ID)
    pthread_t tid = pthread_self();
    char *msg = (char *)arg;

    printf("线程ID:%lu,收到消息:%s\n", (unsigned long)tid, msg);

    // 线程执行5秒后退出
    sleep(5);
    printf("线程ID:%lu,执行完毕,退出!\n", (unsigned long)tid);

    // 线程退出时返回数据(可通过pthread_join获取)
    return (void *)"线程执行成功!";
}

int main() {
    pthread_t tid;  // 存储线程ID
    char *msg = "Hello, Pthread!";
    int ret;

    // 创建线程
    ret = pthread_create(&tid, NULL, thread_func, (void *)msg);
    if (ret != 0) {
        fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
        return 1;
    }

    printf("主线程:成功创建线程,线程ID:%lu\n", (unsigned long)tid);

    // 主线程等待子线程退出(避免主线程先退出导致子线程被终止)
    void *thread_ret;
    ret = pthread_join(tid, &thread_ret);
    if (ret != 0) {
        fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
        return 1;
    }

    printf("主线程:子线程退出,返回值:%s\n", (char *)thread_ret);

    return 0;
}
5. 编译与运行(Linux系统)
# 编译:链接pthread库
gcc pthread_create_demo.c -o pthread_create_demo -pthread
# 运行
./pthread_create_demo
6. 运行结果
主线程:成功创建线程,线程ID:140703345581824
线程ID:140703345581824,收到消息:Hello, Pthread!
线程ID:140703345581824,执行完毕,退出!
主线程:子线程退出,返回值:线程执行成功!

⚠️ 注意事项:

  1. 主线程需等待子线程退出:若主线程不等待(未调用pthread_join),主线程执行完毕后会终止进程,所有子线程也会被强制终止;
  2. 线程入口函数返回值:返回值类型为void *,若需返回复杂数据,可动态分配内存(需主线程释放,避免内存泄漏);
  3. 参数传递注意事项:传递给线程的参数若为局部变量,需确保线程执行期间变量未被销毁(建议使用全局变量或动态分配内存)。

1.3.2 线程等待:pthread_join

1. 函数原型
int pthread_join(pthread_t thread, void **retval);
2. 功能

等待指定线程(thread)退出,阻塞当前线程(通常是主线程)直到目标线程退出。

3. 参数说明
  • thread:要等待的线程ID(由pthread_create返回);
  • retval:输出参数,用于存储目标线程的退出返回值(即线程入口函数的返回值),若无需获取返回值,可设为NULL
4. 返回值
  • 成功:返回0;
  • 失败:返回非0错误码(如目标线程不存在、已被等待过)。
5. 用法示例:等待多个线程退出
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

// 线程入口函数:执行指定时长后退出
void *thread_func(void *arg) {
    int thread_num = *(int *)arg;
    int sleep_sec = thread_num + 1;  // 线程1睡眠1秒,线程2睡眠2秒...

    printf("线程%d:开始执行,睡眠%d秒\n", thread_num, sleep_sec);
    sleep(sleep_sec);
    printf("线程%d:执行完毕,退出!\n", thread_num);

    // 动态分配返回值(主线程需释放)
    int *ret = (int *)malloc(sizeof(int));
    *ret = thread_num * 10;  // 线程1返回10,线程2返回20...
    return (void *)ret;
}

int main() {
    const int THREAD_COUNT = 3;
    pthread_t tids[THREAD_COUNT];
    int thread_nums[THREAD_COUNT];
    int ret;

    // 创建3个线程
    for (int i = 0; i < THREAD_COUNT; i++) {
        thread_nums[i] = i + 1;
        ret = pthread_create(&tids[i], NULL, thread_func, &thread_nums[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
        printf("主线程:创建线程%d,ID:%lu\n", i+1, (unsigned long)tids[i]);
    }

    // 等待所有线程退出,获取返回值
    for (int i = 0; i < THREAD_COUNT; i++) {
        void *thread_ret;
        ret = pthread_join(tids[i], &thread_ret);
        if (ret != 0) {
            fprintf(stderr, "等待线程%d失败:%s\n", i+1, strerror(ret));
            continue;
        }
        printf("主线程:线程%d退出,返回值:%d\n", i+1, *(int *)thread_ret);
        free(thread_ret);  // 释放线程返回值的动态内存
    }

    printf("主线程:所有子线程执行完毕,退出!\n");
    return 0;
}
6. 运行结果
主线程:创建线程1,ID:140605501441792
线程1:开始执行,睡眠1秒
主线程:创建线程2,ID:140605493049088
线程2:开始执行,睡眠2秒
主线程:创建线程3,ID:140605484656384
线程3:开始执行,睡眠3秒
线程1:执行完毕,退出!
主线程:线程1退出,返回值:10
线程2:执行完毕,退出!
主线程:线程2退出,返回值:20
线程3:执行完毕,退出!
主线程:线程3退出,返回值:30
主线程:所有子线程执行完毕,退出!

💡 技巧:pthread_join是“阻塞等待”,若需非阻塞等待线程退出,可使用pthread_tryjoin_np(非标准API)或结合线程状态标记实现。

1.3.3 线程退出:pthread_exit

1. 函数原型
void pthread_exit(void *retval);
2. 功能

终止当前线程的执行,返回指定值(retval),该返回值可被pthread_join获取。

3. 与return的区别
  • return:从线程入口函数返回,终止线程执行,本质上会调用pthread_exit
  • pthread_exit:可在线程入口函数的任意位置调用(如子函数中),直接终止线程,更灵活。
4. 用法示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 子函数:模拟任务执行,执行失败则退出线程
void task(int thread_num) {
    printf("线程%d:执行子任务...\n", thread_num);
    sleep(2);

    // 模拟任务失败,退出线程
    printf("线程%d:子任务执行失败,退出线程!\n", thread_num);
    pthread_exit((void *)"任务失败");
}

// 线程入口函数
void *thread_func(void *arg) {
    int thread_num = *(int *)arg;
    printf("线程%d:开始执行\n", thread_num);

    task(thread_num);  // 调用子函数

    // 以下代码不会执行(线程已在task中退出)
    printf("线程%d:继续执行...\n", thread_num);
    return (void *)"任务成功";
}

int main() {
    pthread_t tid;
    int thread_num = 1;
    int ret;

    ret = pthread_create(&tid, NULL, thread_func, &thread_num);
    if (ret != 0) {
        fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
        return 1;
    }

    void *thread_ret;
    ret = pthread_join(tid, &thread_ret);
    if (ret != 0) {
        fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
        return 1;
    }

    printf("主线程:线程退出,返回值:%s\n", (char *)thread_ret);
    return 0;
}
5. 运行结果
线程1:开始执行
线程1:执行子任务...
线程1:子任务执行失败,退出线程!
主线程:线程退出,返回值:任务失败

1.3.4 线程取消:pthread_cancel

1. 函数原型
int pthread_cancel(pthread_t thread);
2. 功能

请求取消指定线程的执行,并非立即终止线程,而是向线程发送取消请求,线程在“取消点”响应请求。

3. 关键概念:取消点

线程检查是否有取消请求的位置,常见取消点包括:

  • 系统调用(如sleepreadwrite);
  • 线程库函数(如pthread_joinpthread_testcancel);
  • 手动调用pthread_testcancel函数设置取消点。
4. 参数与返回值
  • thread:要取消的线程ID;
  • 成功:返回0;
  • 失败:返回非0错误码。
5. 用法示例
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

// 线程入口函数:循环执行,定期检查取消点
void *thread_func(void *arg) {
    int count = 0;
    printf("线程:开始执行,定期检查取消请求...\n");

    while (1) {
        printf("线程:执行计数%d\n", count++);
        sleep(1);  // 系统调用,是取消点

        // 手动设置取消点(可选)
        pthread_testcancel();
    }

    // 不会执行(线程被取消)
    return (void *)"线程正常退出";
}

int main() {
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid, NULL, thread_func, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
        return 1;
    }

    // 主线程睡眠3秒后,取消子线程
    sleep(3);
    printf("主线程:发送取消请求...\n");
    ret = pthread_cancel(tid);
    if (ret != 0) {
        fprintf(stderr, "取消线程失败:%s\n", strerror(ret));
        return 1;
    }

    // 等待线程退出,获取返回值(被取消的线程返回PTHREAD_CANCELED)
    void *thread_ret;
    ret = pthread_join(tid, &thread_ret);
    if (ret != 0) {
        fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
        return 1;
    }

    if (thread_ret == PTHREAD_CANCELED) {
        printf("主线程:线程被成功取消!\n");
    } else {
        printf("主线程:线程正常退出,返回值:%s\n", (char *)thread_ret);
    }

    return 0;
}
6. 运行结果
线程:开始执行,定期检查取消请求...
线程:执行计数0
线程:执行计数1
线程:执行计数2
主线程:发送取消请求...
主线程:线程被成功取消!

⚠️ 注意事项:

  1. 线程取消是“协作式”的,需线程到达取消点才能响应;
  2. 被取消的线程返回值为PTHREAD_CANCELED(宏定义,值为(void *) -1);
  3. 若线程持有资源(如锁、动态内存),被取消前需释放资源,可通过“线程清理函数”实现。

1.3.5 线程清理函数:pthread_cleanup_push/pthread_cleanup_pop

1. 函数原型
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
2. 功能
  • pthread_cleanup_push:注册线程清理函数(routine),线程退出时(正常退出、被取消)会执行该函数,释放资源;
  • pthread_cleanup_pop:取消或执行清理函数,execute为1时执行清理函数,为0时不执行。
3. 用法示例:线程被取消时释放资源
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 线程清理函数:释放动态内存
void cleanup_func(void *arg) {
    int *ptr = (int *)arg;
    printf("清理函数:释放动态内存,地址:%p\n", ptr);
    free(ptr);
}

// 线程入口函数
void *thread_func(void *arg) {
    // 动态分配内存
    int *data = (int *)malloc(sizeof(int));
    *data = 100;

    // 注册清理函数(必须与pthread_cleanup_pop成对出现)
    pthread_cleanup_push(cleanup_func, data);

    printf("线程:开始执行,数据:%d,内存地址:%p\n", *data, data);

    while (1) {
        sleep(1);
        pthread_testcancel();  // 取消点
    }

    // 取消清理函数(execute=0,不执行)
    pthread_cleanup_pop(0);
    return NULL;
}

int main() {
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid, NULL, thread_func, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建线程失败:%s\n", strerror(ret));
        return 1;
    }

    // 3秒后取消线程
    sleep(3);
    printf("主线程:发送取消请求...\n");
    ret = pthread_cancel(tid);
    if (ret != 0) {
        fprintf(stderr, "取消线程失败:%s\n", strerror(ret));
        return 1;
    }

    // 等待线程退出
    ret = pthread_join(tid, NULL);
    if (ret != 0) {
        fprintf(stderr, "等待线程失败:%s\n", strerror(ret));
        return 1;
    }

    printf("主线程:线程退出,资源已释放!\n");
    return 0;
}
4. 运行结果
线程:开始执行,数据:100,内存地址:0x55f8d7a742a0
主线程:发送取消请求...
清理函数:释放动态内存,地址:0x55f8d7a742a0
主线程:线程退出,资源已释放!

💡 技巧:pthread_cleanup_pushpthread_cleanup_pop必须成对出现,且需在同一代码块中,否则会导致编译错误。

1.4 线程同步与互斥:解决竞态条件

多线程并发访问共享资源(如全局变量、静态变量、文件)时,若缺乏同步机制,会导致“竞态条件”——多个线程同时读写共享资源,导致数据不一致。线程同步与互斥是解决竞态条件的核心手段,常用机制包括互斥锁、条件变量、信号量。

1.4.1 竞态条件示例:无同步机制的共享资源访问

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_COUNT 2
#define LOOP_COUNT 1000000

// 共享变量:计数器
int g_count = 0;

// 线程入口函数:对共享变量执行100万次自增
void *thread_incr(void *arg) {
    for (int i = 0; i < LOOP_COUNT; i++) {
        g_count++;  // 共享资源访问:读g_count → 加1 → 写回g_count
    }
    printf("线程%d:执行完毕,g_count = %d\n", *(int *)arg, g_count);
    return NULL;
}

int main() {
    pthread_t tids[THREAD_COUNT];
    int thread_nums[THREAD_COUNT];
    int ret;

    // 创建2个线程,同时自增共享变量
    for (int i = 0; i < THREAD_COUNT; i++) {
        thread_nums[i] = i + 1;
        ret = pthread_create(&tids[i], NULL, thread_incr, &thread_nums[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    // 等待所有线程退出
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tids[i], NULL);
    }

    printf("主线程:所有线程执行完毕,最终g_count = %d\n", g_count);
    return 0;
}
运行结果(预期2000000,实际结果不确定)
线程1:执行完毕,g_count = 1234567
线程2:执行完毕,g_count = 1897654
主线程:所有线程执行完毕,最终g_count = 1897654
问题分析

g_count++看似是一条语句,实际编译后会分解为三条机器指令:

  1. 从内存读取g_count的值到CPU寄存器;
  2. 寄存器中的值加1;
  3. 将寄存器中的值写回内存。

两个线程并发执行时,指令可能交叉执行,导致数据丢失:

  • 线程1读取g_count=100 → 线程2读取g_count=100 → 线程1加1→101 → 线程2加1→101 → 写回内存,最终g_count=101(预期102)。

✅ 结论:多个线程并发读写共享资源时,必须通过同步互斥机制保证“原子操作”(不可分割的操作),避免竞态条件。

1.4.2 互斥锁(Mutex):最常用的同步机制

互斥锁(Mutual Exclusion Lock)是最常用的线程同步机制,核心功能是“保证同一时刻只有一个线程能持有锁,从而独占访问共享资源”。

1. 互斥锁的核心操作
  • 初始化:创建互斥锁,设置锁的属性;
  • 加锁(P操作):线程尝试获取锁,若锁未被持有则成功获取,若锁已被持有则线程阻塞;
  • 解锁(V操作):线程释放锁,唤醒等待该锁的线程;
  • 销毁:释放互斥锁占用的资源。
2. 互斥锁核心API
功能函数原型说明
初始化int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);初始化互斥锁,attr=NULL使用默认属性
加锁int pthread_mutex_lock(pthread_mutex_t *mutex);阻塞加锁,若锁被占用则阻塞线程
尝试加锁int pthread_mutex_trylock(pthread_mutex_t *mutex);非阻塞加锁,若锁被占用则立即返回错误(EBUSY)
解锁int pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁,仅持有锁的线程可解锁
销毁int pthread_mutex_destroy(pthread_mutex_t *mutex);销毁互斥锁,释放资源
3. 用法示例:用互斥锁解决竞态条件
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_COUNT 2
#define LOOP_COUNT 1000000

// 共享变量
int g_count = 0;
// 互斥锁
pthread_mutex_t g_mutex;

// 线程入口函数:加锁后自增共享变量
void *thread_incr(void *arg) {
    int thread_num = *(int *)arg;

    for (int i = 0; i < LOOP_COUNT; i++) {
        // 加锁:保证g_count++是原子操作
        pthread_mutex_lock(&g_mutex);
        g_count++;
        // 解锁:释放锁,允许其他线程访问
        pthread_mutex_unlock(&g_mutex);
    }

    printf("线程%d:执行完毕,g_count = %d\n", thread_num, g_count);
    return NULL;
}

int main() {
    pthread_t tids[THREAD_COUNT];
    int thread_nums[THREAD_COUNT];
    int ret;

    // 初始化互斥锁(默认属性)
    ret = pthread_mutex_init(&g_mutex, NULL);
    if (ret != 0) {
        fprintf(stderr, "初始化互斥锁失败:%s\n", strerror(ret));
        return 1;
    }

    // 创建线程
    for (int i = 0; i < THREAD_COUNT; i++) {
        thread_nums[i] = i + 1;
        ret = pthread_create(&tids[i], NULL, thread_incr, &thread_nums[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    // 等待线程退出
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tids[i], NULL);
    }

    printf("主线程:所有线程执行完毕,最终g_count = %d\n", g_count);

    // 销毁互斥锁
    pthread_mutex_destroy(&g_mutex);
    return 0;
}
4. 运行结果(预期2000000)
线程1:执行完毕,g_count = 1987654
线程2:执行完毕,g_count = 2000000
主线程:所有线程执行完毕,最终g_count = 2000000
5. 互斥锁的关键注意事项

⚠️ 注意1:加锁与解锁必须成对出现,避免死锁或资源泄漏。例如,线程加锁后未解锁就退出,会导致其他线程永久阻塞;
⚠️ 注意2:锁的粒度要适中。锁粒度太粗(如整个函数加锁)会导致线程串行执行,失去多线程优势;锁粒度太细(如每条语句加锁)会增加锁操作开销;
⚠️ 注意3:避免在持有锁时调用耗时操作(如睡眠、IO),减少其他线程的等待时间;
⚠️ 注意4:互斥锁是“线程私有”的,仅持有锁的线程可解锁,其他线程解锁会导致错误。

1.4.3 条件变量(Condition Variable):线程间通信

条件变量用于线程间的“通信”,允许一个线程等待某个条件成立,另一个线程在条件成立时唤醒等待的线程。条件变量通常与互斥锁配合使用,解决“线程等待某个条件”的场景(如生产者-消费者模型)。

1. 条件变量的核心API
功能函数原型说明
初始化int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);初始化条件变量,attr=NULL使用默认属性
等待条件int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);释放互斥锁,阻塞等待条件变量;被唤醒后重新获取互斥锁
唤醒单个线程int pthread_cond_signal(pthread_cond_t *cond);唤醒等待该条件变量的一个线程
唤醒所有线程int pthread_cond_broadcast(pthread_cond_t *cond);唤醒等待该条件变量的所有线程
销毁int pthread_cond_destroy(pthread_cond_t *cond);销毁条件变量,释放资源
2. 核心原理
  • pthread_cond_wait调用时,会先释放互斥锁,再阻塞线程,避免持有锁导致其他线程无法修改条件;
  • 线程被唤醒后,会自动重新获取互斥锁,确保后续访问共享条件时的线程安全。
3. 用法示例:生产者-消费者模型(单生产者-单消费者)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

#define BUFFER_SIZE 5  // 缓冲区大小
#define PRODUCT_COUNT 10  // 生产产品总数

// 缓冲区:存储产品(整数)
int g_buffer[BUFFER_SIZE];
// 缓冲区读写索引
int g_in = 0;  // 写索引(生产者使用)
int g_out = 0;  // 读索引(消费者使用)
// 产品数量
int g_product_num = 0;

// 互斥锁:保护缓冲区和产品数量
pthread_mutex_t g_mutex;
// 条件变量:缓冲区非空(消费者等待)、缓冲区非满(生产者等待)
pthread_cond_t g_not_empty;
pthread_cond_t g_not_full;

// 生产者线程:生产产品,放入缓冲区
void *producer(void *arg) {
    int product_id = 0;
    while (product_id < PRODUCT_COUNT) {
        // 加锁:保护共享资源
        pthread_mutex_lock(&g_mutex);

        // 缓冲区满,等待“缓冲区非满”条件
        while (g_product_num == BUFFER_SIZE) {
            printf("生产者:缓冲区满,等待消费者消费...\n");
            pthread_cond_wait(&g_not_full, &g_mutex);
        }

        // 生产产品,放入缓冲区
        g_buffer[g_in] = ++product_id;
        g_in = (g_in + 1) % BUFFER_SIZE;
        g_product_num++;
        printf("生产者:生产产品%d,缓冲区产品数:%d\n", product_id, g_product_num);

        // 唤醒消费者:缓冲区非空
        pthread_cond_signal(&g_not_empty);
        // 解锁
        pthread_mutex_unlock(&g_mutex);

        // 模拟生产耗时
        sleep(rand() % 2);
    }

    printf("生产者:完成所有产品生产,退出!\n");
    return NULL;
}

// 消费者线程:从缓冲区取出产品,消费
void *consumer(void *arg) {
    int product_id = 0;
    while (1) {
        // 加锁
        pthread_mutex_lock(&g_mutex);

        // 缓冲区空,等待“缓冲区非空”条件
        while (g_product_num == 0) {
            // 检查是否所有产品都已消费
            if (g_product_num == 0 && product_id >= PRODUCT_COUNT) {
                pthread_mutex_unlock(&g_mutex);
                break;
            }
            printf("消费者:缓冲区空,等待生产者生产...\n");
            pthread_cond_wait(&g_not_empty, &g_mutex);
        }

        // 检查是否退出
        if (g_product_num == 0 && product_id >= PRODUCT_COUNT) {
            break;
        }

        // 从缓冲区取出产品
        product_id = g_buffer[g_out];
        g_out = (g_out + 1) % BUFFER_SIZE;
        g_product_num--;
        printf("消费者:消费产品%d,缓冲区产品数:%d\n", product_id, g_product_num);

        // 唤醒生产者:缓冲区非满
        pthread_cond_signal(&g_not_full);
        // 解锁
        pthread_mutex_unlock(&g_mutex);

        // 模拟消费耗时
        sleep(rand() % 3);
    }

    printf("消费者:完成所有产品消费,退出!\n");
    return NULL;
}

int main() {
    pthread_t prod_tid, cons_tid;
    int ret;

    // 初始化互斥锁和条件变量
    ret = pthread_mutex_init(&g_mutex, NULL);
    ret |= pthread_cond_init(&g_not_empty, NULL);
    ret |= pthread_cond_init(&g_not_full, NULL);
    if (ret != 0) {
        fprintf(stderr, "初始化同步机制失败:%s\n", strerror(ret));
        return 1;
    }

    // 创建生产者和消费者线程
    ret = pthread_create(&prod_tid, NULL, producer, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建生产者线程失败:%s\n", strerror(ret));
        return 1;
    }

    ret = pthread_create(&cons_tid, NULL, consumer, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建消费者线程失败:%s\n", strerror(ret));
        return 1;
    }

    // 等待线程退出
    pthread_join(prod_tid, NULL);
    // 生产者退出后,唤醒消费者,告知所有产品已生产
    pthread_mutex_lock(&g_mutex);
    pthread_cond_signal(&g_not_empty);
    pthread_mutex_unlock(&g_mutex);

    pthread_join(cons_tid, NULL);

    // 销毁同步机制
    pthread_mutex_destroy(&g_mutex);
    pthread_cond_destroy(&g_not_empty);
    pthread_cond_destroy(&g_not_full);

    printf("主线程:所有线程执行完毕,退出!\n");
    return 0;
}
4. 运行结果(节选)
生产者:生产产品1,缓冲区产品数:1
消费者:消费产品1,缓冲区产品数:0
生产者:生产产品2,缓冲区产品数:1
生产者:生产产品3,缓冲区产品数:2
消费者:消费产品2,缓冲区产品数:1
生产者:生产产品4,缓冲区产品数:2
生产者:生产产品5,缓冲区产品数:3
生产者:生产产品6,缓冲区产品数:4
生产者:生产产品7,缓冲区产品数:5
生产者:缓冲区满,等待消费者消费...
消费者:消费产品3,缓冲区产品数:4
生产者:生产产品8,缓冲区产品数:5
生产者:缓冲区满,等待消费者消费...
消费者:消费产品4,缓冲区产品数:4
生产者:生产产品9,缓冲区产品数:5
生产者:缓冲区满,等待消费者消费...
消费者:消费产品5,缓冲区产品数:4
生产者:生产产品10,缓冲区产品数:5
生产者:完成所有产品生产,退出!
消费者:消费产品6,缓冲区产品数:4
消费者:消费产品7,缓冲区产品数:3
消费者:消费产品8,缓冲区产品数:2
消费者:消费产品9,缓冲区产品数:1
消费者:消费产品10,缓冲区产品数:0
消费者:完成所有产品消费,退出!
主线程:所有线程执行完毕,退出!

💡 技巧:条件变量的等待必须用while循环,而非if判断。因为线程可能被“虚假唤醒”(无其他线程发送信号却被唤醒),while循环可重新检查条件,确保条件成立后再执行后续操作。

1.4.4 信号量(Semaphore):灵活的同步机制

信号量是比互斥锁更灵活的同步机制,不仅支持“互斥”(二值信号量),还支持“计数”(计数信号量),可用于限制并发访问的线程数量、实现生产者-消费者模型等场景。

1. 信号量的核心概念
  • 信号量是一个非负整数,代表可用资源的数量;
  • P操作(sem_wait):信号量减1,若信号量为0则阻塞线程;
  • V操作(sem_post):信号量加1,唤醒等待的线程。
2. 信号量核心API(POSIX信号量)
功能函数原型说明
初始化int sem_init(sem_t *sem, int pshared, unsigned int value);初始化信号量,pshared=0表示线程间共享,value为信号量初始值
P操作int sem_wait(sem_t *sem);信号量减1,若为0则阻塞(阻塞型)
尝试P操作int sem_trywait(sem_t *sem);信号量减1,若为0则返回错误(非阻塞型)
V操作int sem_post(sem_t *sem);信号量加1,唤醒等待的线程
获取信号量值int sem_getvalue(sem_t *sem, int *sval);获取当前信号量值,存入sval
销毁int sem_destroy(sem_t *sem);销毁信号量,释放资源
3. 用法示例1:二值信号量(模拟互斥锁)
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define THREAD_COUNT 2
#define LOOP_COUNT 1000000

int g_count = 0;
// 二值信号量(初始值1,模拟互斥锁)
sem_t g_sem;

void *thread_incr(void *arg) {
    int thread_num = *(int *)arg;
    for (int i = 0; i < LOOP_COUNT; i++) {
        sem_wait(&g_sem);  // P操作:获取锁
        g_count++;
        sem_post(&g_sem);  // V操作:释放锁
    }
    printf("线程%d:执行完毕,g_count = %d\n", thread_num, g_count);
    return NULL;
}

int main() {
    pthread_t tids[THREAD_COUNT];
    int thread_nums[THREAD_COUNT];
    int ret;

    // 初始化二值信号量(初始值1)
    ret = sem_init(&g_sem, 0, 1);
    if (ret != 0) {
        fprintf(stderr, "初始化信号量失败:%s\n", strerror(ret));
        return 1;
    }

    for (int i = 0; i < THREAD_COUNT; i++) {
        thread_nums[i] = i + 1;
        ret = pthread_create(&tids[i], NULL, thread_incr, &thread_nums[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tids[i], NULL);
    }

    printf("主线程:最终g_count = %d\n", g_count);

    sem_destroy(&g_sem);
    return 0;
}
4. 用法示例2:计数信号量(限制并发线程数)
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdlib.h>

#define THREAD_COUNT 5  // 总线程数
#define MAX_CONCURRENT 2  // 最大并发线程数

// 计数信号量(初始值2,限制最多2个线程同时执行)
sem_t g_sem;

void *thread_func(void *arg) {
    int thread_num = *(int *)arg;

    // P操作:获取并发权限,若已达最大并发则阻塞
    sem_wait(&g_sem);
    printf("线程%d:获取并发权限,开始执行任务...\n", thread_num);

    // 模拟任务耗时
    sleep(rand() % 3);

    printf("线程%d:任务执行完毕,释放并发权限\n", thread_num);
    // V操作:释放并发权限
    sem_post(&g_sem);

    return NULL;
}

int main() {
    pthread_t tids[THREAD_COUNT];
    int thread_nums[THREAD_COUNT];
    int ret;

    // 初始化计数信号量(初始值2)
    ret = sem_init(&g_sem, 0, MAX_CONCURRENT);
    if (ret != 0) {
        fprintf(stderr, "初始化信号量失败:%s\n", strerror(ret));
        return 1;
    }

    // 创建5个线程
    for (int i = 0; i < THREAD_COUNT; i++) {
        thread_nums[i] = i + 1;
        ret = pthread_create(&tids[i], NULL, thread_func, &thread_nums[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    // 等待所有线程退出
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tids[i], NULL);
    }

    sem_destroy(&g_sem);
    printf("主线程:所有线程执行完毕!\n");
    return 0;
}
5. 运行结果(节选)
线程1:获取并发权限,开始执行任务...
线程2:获取并发权限,开始执行任务...
线程1:任务执行完毕,释放并发权限
线程3:获取并发权限,开始执行任务...
线程2:任务执行完毕,释放并发权限
线程4:获取并发权限,开始执行任务...
线程3:任务执行完毕,释放并发权限
线程5:获取并发权限,开始执行任务...
线程4:任务执行完毕,释放并发权限
线程5:任务执行完毕,释放并发权限
主线程:所有线程执行完毕!

✅ 结论:信号量功能更灵活,二值信号量可替代互斥锁,计数信号量可限制并发线程数;但互斥锁更适合纯互斥场景,支持“递归加锁”“错误恢复”等特性,需根据场景选择。

1.5 线程安全与常见并发问题

1.5.1 线程安全的定义与判断标准

  • 线程安全:多个线程并发访问某个函数、数据结构或资源时,不会出现数据不一致、逻辑错误等问题,且结果与单线程访问一致;
  • 非线程安全:多个线程并发访问时,可能出现竞态条件、数据损坏等问题。
线程安全的判断标准
  1. 不使用全局变量、静态变量(或对其进行同步保护);
  2. 不依赖函数调用顺序或外部状态;
  3. 动态分配的内存由调用者管理,或内部进行线程安全的内存管理;
  4. 对共享资源的访问必须通过同步机制(锁、信号量等)进行保护。
常见线程安全的函数与非线程安全的函数
  • 线程安全函数(如strlenmemcpy):仅操作传入的参数,不使用全局/静态变量;
  • 非线程安全函数(如strtokrand):使用静态变量存储中间状态,多个线程并发调用会导致状态混乱。

💡 技巧:非线程安全函数的线程安全版本通常以_r结尾(如strtok_rstrtok的线程安全版本),使用时需传入线程私有变量存储中间状态。

1.5.2 常见并发问题与解决方案

1. 死锁(Deadlock)
(1)定义与产生条件

死锁是指多个线程互相等待对方释放资源,导致所有线程永久阻塞,无法继续执行。

死锁产生的四个必要条件(缺一不可):

  • 互斥条件:资源只能被一个线程持有;
  • 占有并等待条件:线程持有一个资源,同时等待另一个资源;
  • 不可剥夺条件:资源只能被持有线程主动释放,无法强制剥夺;
  • 循环等待条件:多个线程形成循环等待链(线程1等待线程2的资源,线程2等待线程1的资源)。
(2)死锁示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 两个互斥锁
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

// 线程1:先加锁a,再加锁b
void *thread1(void *arg) {
    printf("线程1:尝试获取锁a...\n");
    pthread_mutex_lock(&mutex_a);
    printf("线程1:获取锁a成功,尝试获取锁b...\n");
    sleep(1);  // 模拟持有锁a时的耗时操作,让线程2有机会获取锁b
    pthread_mutex_lock(&mutex_b);

    printf("线程1:获取锁b成功,执行任务...\n");

    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    return NULL;
}

// 线程2:先加锁b,再加锁a
void *thread2(void *arg) {
    printf("线程2:尝试获取锁b...\n");
    pthread_mutex_lock(&mutex_b);
    printf("线程2:获取锁b成功,尝试获取锁a...\n");
    sleep(1);  // 模拟持有锁b时的耗时操作,让线程1有机会获取锁a
    pthread_mutex_lock(&mutex_a);

    printf("线程2:获取锁a成功,执行任务...\n");

    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    int ret;

    pthread_mutex_init(&mutex_a, NULL);
    pthread_mutex_init(&mutex_b, NULL);

    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&mutex_a);
    pthread_mutex_destroy(&mutex_b);
    return 0;
}
(3)运行结果(死锁)
线程1:尝试获取锁a...
线程1:获取锁a成功,尝试获取锁b...
线程2:尝试获取锁b...
线程2:获取锁b成功,尝试获取锁a...
// 死锁,程序卡住,无法继续执行
(4)死锁解决方案

💡 方案1:破坏循环等待条件——统一锁的获取顺序。所有线程按相同顺序获取锁(如先锁a后锁b),避免循环等待;
💡 方案2:破坏占有并等待条件——一次性获取所有资源。线程启动时,一次性获取所需的所有锁,获取失败则释放已获取的锁;
💡 方案3:破坏不可剥夺条件——设置锁的超时时间。使用pthread_mutex_trylockpthread_mutex_timedlock,超时未获取锁则释放已持有锁;
💡 方案4:破坏互斥条件——使用共享资源的并发访问机制。如读写锁、无锁数据结构,减少互斥锁的使用;
💡 方案5:死锁检测与恢复。定期检测线程状态,发现死锁时强制释放资源或终止部分线程。

2. 活锁(Livelock)
(1)定义

活锁是指多个线程不断修改自身状态以响应对方的状态变化,但始终无法推进任务,看似“活跃”实则“停滞”。例如,两个线程互相礼让资源,导致都无法获取资源。

(2)解决方案
  • 引入随机延迟:线程重试前加入随机睡眠时长,打破对称等待;
  • 固定优先级:为线程设置固定优先级,高优先级线程优先获取资源;
  • 限制重试次数:线程重试次数达到阈值后,暂停或退出,避免无限循环。
3. 优先级反转(Priority Inversion)
(1)定义

低优先级线程持有高优先级线程所需的资源,导致高优先级线程阻塞,低优先级线程因被中优先级线程抢占而无法释放资源,最终高优先级线程的执行优先级低于中优先级线程。

(2)解决方案
  • 优先级继承:低优先级线程持有高优先级线程所需的锁时,临时提升低优先级线程的优先级,使其能尽快释放锁;
  • 优先级天花板:为锁设置优先级天花板(高于所有使用该锁的线程优先级),持有锁的线程优先级提升至天花板,避免被中优先级线程抢占;
  • 避免长时间持有锁:低优先级线程持有锁时,尽量缩短执行时间,快速释放锁。

1.6 多线程实战案例

1.6.1 案例1:多线程并行计算(矩阵乘法)

需求描述

实现矩阵乘法的多线程并行计算,将矩阵A(M×N)与矩阵B(N×P)相乘,结果存储在矩阵C(M×P)中。通过多线程将每行的计算任务分配给不同线程,充分利用多核CPU资源,提升计算效率。

核心思路
  1. 定义矩阵数据结构,初始化输入矩阵A和B;
  2. 创建M个线程,每个线程负责计算结果矩阵C的一行;
  3. 线程通过参数获取矩阵数据、行号等信息,计算对应行的每个元素(矩阵乘法规则:C[i][j] = ΣA[i][k]×B[k][j],k=0~N-1);
  4. 主线程等待所有线程计算完成,输出结果矩阵。
代码实现
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// 矩阵维度定义
#define M 4  // 矩阵A的行数
#define N 4  // 矩阵A的列数 = 矩阵B的行数
#define P 4  // 矩阵B的列数
#define THREAD_COUNT M  // 线程数 = 矩阵C的行数

// 输入矩阵A、B,输出矩阵C
int g_matrix_A[M][N];
int g_matrix_B[N][P];
int g_matrix_C[M][P];

// 线程参数结构体:传递矩阵数据和行号
typedef struct {
    int row;  // 负责计算的行号
} ThreadParam;

// 线程入口函数:计算矩阵C的指定行
void *matrix_multiply_row(void *arg) {
    ThreadParam *param = (ThreadParam *)arg;
    int row = param->row;

    printf("线程%d:开始计算矩阵C的第%d行...\n", row+1, row+1);

    // 计算该行的每个元素
    for (int j = 0; j < P; j++) {
        g_matrix_C[row][j] = 0;
        for (int k = 0; k < N; k++) {
            g_matrix_C[row][j] += g_matrix_A[row][k] * g_matrix_B[k][j];
        }
    }

    printf("线程%d:完成矩阵C的第%d行计算\n", row+1, row+1);
    return NULL;
}

// 初始化矩阵A和B(随机生成1~10的整数)
void init_matrices() {
    srand(time(NULL));
    printf("初始化矩阵A(%d×%d):\n", M, N);
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            g_matrix_A[i][j] = rand() % 10 + 1;
            printf("%d\t", g_matrix_A[i][j]);
        }
        printf("\n");
    }

    printf("\n初始化矩阵B(%d×%d):\n", N, P);
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < P; j++) {
            g_matrix_B[i][j] = rand() % 10 + 1;
            printf("%d\t", g_matrix_B[i][j]);
        }
        printf("\n");
    }
}

// 打印矩阵C
void print_matrix_C() {
    printf("\n矩阵乘法结果C(%d×%d):\n", M, P);
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < P; j++) {
            printf("%d\t", g_matrix_C[i][j]);
        }
        printf("\n");
    }
}

int main() {
    pthread_t tids[THREAD_COUNT];
    ThreadParam params[THREAD_COUNT];
    int ret;

    // 初始化矩阵A和B
    init_matrices();

    // 记录开始时间
    clock_t start_time = clock();

    // 创建线程,每个线程计算一行
    for (int i = 0; i < THREAD_COUNT; i++) {
        params[i].row = i;
        ret = pthread_create(&tids[i], NULL, matrix_multiply_row, &params[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    // 等待所有线程计算完成
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tids[i], NULL);
    }

    // 记录结束时间,计算耗时
    clock_t end_time = clock();
    double cost_time = (double)(end_time - start_time) / CLOCKS_PER_SEC;

    // 打印结果
    print_matrix_C();
    printf("\n多线程矩阵乘法完成,总耗时:%.4f秒\n", cost_time);

    return 0;
}
运行结果(节选)
初始化矩阵A(4×4):
3       5       7       2
9       1       4       6
8       3       5       1
2       7       9       4

初始化矩阵B(4×4):
6       8       1       3
2       5       9       7
4       3       6       2
1       7       8       5

线程1:开始计算矩阵C的第1行...
线程2:开始计算矩阵C的第2行...
线程3:开始计算矩阵C的第3行...
线程4:开始计算矩阵C的第4行...
线程1:完成矩阵C的第1行计算
线程3:完成矩阵C的第3行计算
线程2:完成矩阵C的第2行计算
线程4:完成矩阵C的第4行计算

矩阵乘法结果C(4×4):
60      78      106     70
83      143     121     92
71      106     91      62
83      104     161     103

多线程矩阵乘法完成,总耗时:0.0001秒
代码解析
  • 任务拆分:按行拆分矩阵乘法任务,每个线程独立计算一行,无共享资源竞争(矩阵A、B为只读,矩阵C的每行由单独线程写入),无需同步机制,效率最高;
  • 并行优势:多核CPU上,4个线程并行计算,相比单线程效率提升接近4倍(矩阵越大,优势越明显);
  • 扩展性:可根据CPU核心数调整线程数,如8核CPU可创建8个线程,处理更大规模矩阵。

1.6.2 案例2:线程池(Thread Pool)设计与实现

需求描述

实现一个通用线程池,支持:

  1. 初始化线程池时指定线程数量;
  2. 向线程池提交任务(函数指针+参数);
  3. 线程池中的线程循环获取任务并执行,执行完毕后继续等待新任务;
  4. 支持关闭线程池,等待所有任务执行完毕后退出。
核心思路
  1. 任务队列:用链表存储待执行的任务,每个任务包含函数指针和参数;
  2. 线程池结构体:包含线程数组、任务队列、互斥锁(保护任务队列)、条件变量(任务队列非空/非满)、线程池状态(运行/关闭);
  3. 线程函数:线程池中的线程循环等待任务,获取任务后执行,执行完毕后继续等待;
  4. 任务提交函数:将任务添加到任务队列,唤醒等待的线程;
  5. 线程池关闭函数:设置线程池状态为“关闭”,唤醒所有线程,等待线程退出后释放资源。
代码实现
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// 任务结构体:存储任务函数和参数
typedef struct Task {
    void (*func)(void *);  // 任务函数指针
    void *arg;            // 任务参数
    struct Task *next;     // 下一个任务(链表节点)
} Task;

// 线程池结构体
typedef struct ThreadPool {
    pthread_t *threads;       // 线程数组
    int thread_count;         // 线程数量
    Task *task_queue;         // 任务队列头
    Task *task_tail;          // 任务队列尾
    int task_count;           // 任务队列中任务数量
    pthread_mutex_t mutex;    // 保护任务队列的互斥锁
    pthread_cond_t not_empty; // 任务队列非空条件变量
    int is_shutdown;          // 线程池状态:0=运行,1=关闭
} ThreadPool;

// 线程池全局实例(简化设计,实际可改为动态创建)
ThreadPool g_thread_pool;

// 线程函数:线程池中的线程循环执行任务
void *thread_pool_worker(void *arg) {
    printf("线程%d:启动,等待任务...\n", *(int *)arg);
    free(arg);  // 释放线程编号的动态内存

    while (1) {
        // 加锁保护任务队列
        pthread_mutex_lock(&g_thread_pool.mutex);

        // 任务队列为空且线程池未关闭,等待任务
        while (g_thread_pool.task_count == 0 && !g_thread_pool.is_shutdown) {
            pthread_cond_wait(&g_thread_pool.not_empty, &g_thread_pool.mutex);
        }

        // 线程池已关闭且任务队列为空,退出线程
        if (g_thread_pool.is_shutdown && g_thread_pool.task_count == 0) {
            pthread_mutex_unlock(&g_thread_pool.mutex);
            printf("线程%d:线程池关闭,退出!\n", pthread_self() % 1000);
            pthread_exit(NULL);
        }

        // 从任务队列头部取出任务
        Task *task = g_thread_pool.task_queue;
        g_thread_pool.task_queue = task->next;
        g_thread_pool.task_count--;
        if (g_thread_pool.task_count == 0) {
            g_thread_pool.task_tail = NULL;
        }

        // 解锁
        pthread_mutex_unlock(&g_thread_pool.mutex);

        // 执行任务
        printf("线程%d:开始执行任务...\n", pthread_self() % 1000);
        task->func(task->arg);
        printf("线程%d:任务执行完毕!\n", pthread_self() % 1000);

        // 释放任务内存
        free(task);
    }
}

// 初始化线程池
int thread_pool_init(int thread_count) {
    memset(&g_thread_pool, 0, sizeof(ThreadPool));

    // 初始化互斥锁和条件变量
    int ret = pthread_mutex_init(&g_thread_pool.mutex, NULL);
    ret |= pthread_cond_init(&g_thread_pool.not_empty, NULL);
    if (ret != 0) {
        fprintf(stderr, "初始化同步机制失败:%s\n", strerror(ret));
        return -1;
    }

    // 设置线程数量,创建线程数组
    g_thread_pool.thread_count = thread_count;
    g_thread_pool.threads = (pthread_t *)malloc(thread_count * sizeof(pthread_t));
    if (g_thread_pool.threads == NULL) {
        fprintf(stderr, "分配线程数组内存失败!\n");
        return -1;
    }

    // 创建线程
    for (int i = 0; i < thread_count; i++) {
        int *thread_num = (int *)malloc(sizeof(int));
        *thread_num = i + 1;
        ret = pthread_create(&g_thread_pool.threads[i], NULL, thread_pool_worker, thread_num);
        if (ret != 0) {
            fprintf(stderr, "创建线程%d失败:%s\n", i+1, strerror(ret));
            return -1;
        }
    }

    printf("线程池初始化成功!线程数量:%d\n", thread_count);
    return 0;
}

// 向线程池提交任务
int thread_pool_submit(void (*func)(void *), void *arg) {
    // 创建任务
    Task *task = (Task *)malloc(sizeof(Task));
    if (task == NULL) {
        fprintf(stderr, "创建任务失败!\n");
        return -1;
    }
    task->func = func;
    task->arg = arg;
    task->next = NULL;

    // 加锁,将任务添加到队列尾部
    pthread_mutex_lock(&g_thread_pool.mutex);

    if (g_thread_pool.task_tail == NULL) {
        g_thread_pool.task_queue = task;
        g_thread_pool.task_tail = task;
    } else {
        g_thread_pool.task_tail->next = task;
        g_thread_pool.task_tail = task;
    }
    g_thread_pool.task_count++;

    printf("提交任务成功!当前任务队列长度:%d\n", g_thread_pool.task_count);

    // 唤醒等待的线程
    pthread_cond_signal(&g_thread_pool.not_empty);
    // 解锁
    pthread_mutex_unlock(&g_thread_pool.mutex);

    return 0;
}

// 关闭线程池:等待所有任务执行完毕后退出
void thread_pool_shutdown() {
    // 加锁,设置线程池状态为关闭
    pthread_mutex_lock(&g_thread_pool.mutex);
    g_thread_pool.is_shutdown = 1;
    pthread_mutex_unlock(&g_thread_pool.mutex);

    printf("线程池开始关闭,唤醒所有线程...\n");
    // 唤醒所有等待的线程
    pthread_cond_broadcast(&g_thread_pool.not_empty);

    // 等待所有线程退出
    for (int i = 0; i < g_thread_pool.thread_count; i++) {
        pthread_join(g_thread_pool.threads[i], NULL);
    }

    // 释放资源
    free(g_thread_pool.threads);
    pthread_mutex_destroy(&g_thread_pool.mutex);
    pthread_cond_destroy(&g_thread_pool.not_empty);

    printf("线程池已关闭!\n");
}

// 测试任务1:打印数字
void test_task1(void *arg) {
    int num = *(int *)arg;
    sleep(1);  // 模拟任务耗时
    printf("任务1:打印数字%d\n", num);
    free(arg);  // 释放任务参数内存
}

// 测试任务2:打印字符串
void test_task2(void *arg) {
    char *str = (char *)arg;
    sleep(2);  // 模拟任务耗时
    printf("任务2:打印字符串%s\n", str);
    free(arg);  // 释放任务参数内存
}

int main() {
    // 初始化线程池(3个线程)
    if (thread_pool_init(3) != 0) {
        return 1;
    }

    // 提交5个任务
    for (int i = 0; i < 3; i++) {
        int *num = (int *)malloc(sizeof(int));
        *num = i + 1;
        thread_pool_submit(test_task1, num);
    }

    char *strs[] = {"Hello", "ThreadPool", "C语言"};
    for (int i = 0; i < 2; i++) {
        char *str = (char *)malloc(strlen(strs[i]) + 1);
        strcpy(str, strs[i]);
        thread_pool_submit(test_task2, str);
    }

    // 主线程睡眠5秒,等待任务执行
    sleep(5);

    // 关闭线程池
    thread_pool_shutdown();

    return 0;
}
运行结果(节选)
线程池初始化成功!线程数量:3
线程1:启动,等待任务...
线程2:启动,等待任务...
线程3:启动,等待任务...
提交任务成功!当前任务队列长度:1
提交任务成功!当前任务队列长度:2
提交任务成功!当前任务队列长度:3
提交任务成功!当前任务队列长度:4
提交任务成功!当前任务队列长度:5
线程1:开始执行任务...
线程2:开始执行任务...

线程3:开始执行任务...
任务1:打印数字1
线程1:任务执行完毕!
线程1:开始执行任务...
任务1:打印数字2
线程2:任务执行完毕!
线程2:开始执行任务...
任务1:打印数字3
线程3:任务执行完毕!
线程3:开始执行任务...
任务2:打印字符串Hello
线程1:任务执行完毕!
线程1:开始执行任务...
任务2:打印字符串ThreadPool
线程池开始关闭,唤醒所有线程...
线程2:任务执行完毕!
线程2:线程池关闭,退出!
线程3:任务执行完毕!
线程3:线程池关闭,退出!
线程1:任务执行完毕!
线程1:线程池关闭,退出!
线程池已关闭!
代码解析
  • 任务队列设计:采用链表结构存储任务,支持动态添加任务,无需预设任务数量上限;
  • 同步机制:互斥锁保护任务队列的读写操作,条件变量唤醒等待任务的线程,避免线程空轮询;
  • 线程复用:线程池中的线程不会执行完一个任务就退出,而是循环等待新任务,减少线程创建/销毁的开销;
  • 优雅关闭:关闭线程池时,先设置is_shutdown标志,再唤醒所有线程,确保所有任务执行完毕后线程才退出,避免任务丢失;
  • 通用性:支持提交任意类型的任务(只需传入任务函数和参数),可适配不同业务场景(如网络请求处理、数据计算)。
线程池的进阶优化方向
  • 任务队列长度限制:避免任务过多导致内存溢出,超过长度时拒绝提交或阻塞等待;
  • 任务优先级:支持按优先级提交任务,高优先级任务优先执行;
  • 线程动态扩容/缩容:根据任务队列长度自动增加或减少线程数量,平衡资源占用与执行效率;
  • 任务执行结果回调:支持任务执行完毕后调用回调函数,返回执行结果;
  • 异常处理:为任务执行添加异常捕获机制,避免单个任务崩溃导致整个线程退出。

1.6.3 案例3:读写锁实现高效并发访问

需求描述

实现一个支持高并发读写的共享数据结构(如缓存),要求:

  1. 多个读线程可同时访问共享数据(读-读不互斥);
  2. 写线程访问共享数据时,其他读线程和写线程需阻塞(写-读、写-写互斥);
  3. 读操作远多于写操作的场景下,提升并发效率(相比互斥锁,读写锁读并发更高)。
核心思路
  • 读写锁(pthread_rwlock_t):POSIX提供的读写锁API,支持读锁和写锁的分离;
  • 读锁(共享锁):多个线程可同时获取,适用于读操作;
  • 写锁(排他锁):仅一个线程可获取,适用于写操作;
  • 共享数据:模拟缓存,存储键值对(如用户ID与用户名的映射);
  • 线程设计:创建多个读线程和少量写线程,模拟高并发读、低并发写的场景。
代码实现
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// 缓存大小
#define CACHE_SIZE 10
// 读线程数量(模拟高并发读)
#define READ_THREAD_COUNT 5
// 写线程数量(模拟低并发写)
#define WRITE_THREAD_COUNT 2
// 每个线程执行操作的次数
#define OP_COUNT 10

// 缓存结构体:键值对(用户ID-用户名)
typedef struct {
    int user_id;
    char username[20];
    int is_valid;  // 缓存是否有效:1=有效,0=无效
} CacheItem;

// 共享缓存数组
CacheItem g_cache[CACHE_SIZE];
// 读写锁:保护缓存访问
pthread_rwlock_t g_rwlock;
// 随机数种子(确保多线程下随机数不同)
pthread_mutex_t g_rand_mutex;

// 初始化缓存和同步机制
void init_cache() {
    // 初始化缓存为无效状态
    for (int i = 0; i < CACHE_SIZE; i++) {
        g_cache[i].is_valid = 0;
    }

    // 初始化读写锁和随机数互斥锁
    int ret = pthread_rwlock_init(&g_rwlock, NULL);
    ret |= pthread_mutex_init(&g_rand_mutex, NULL);
    if (ret != 0) {
        fprintf(stderr, "初始化同步机制失败:%s\n", strerror(ret));
        exit(1);
    }

    printf("缓存初始化完成!缓存大小:%d\n", CACHE_SIZE);
}

// 写操作:更新缓存(模拟添加/修改用户信息)
void write_cache(int user_id, const char *username) {
    // 获取写锁(排他锁)
    pthread_rwlock_wrlock(&g_rwlock);

    // 查找缓存位置(简单哈希:user_id % CACHE_SIZE)
    int idx = user_id % CACHE_SIZE;
    g_cache[idx].user_id = user_id;
    strncpy(g_cache[idx].username, username, sizeof(g_cache[idx].username)-1);
    g_cache[idx].is_valid = 1;

    printf("写线程:更新缓存,用户ID:%d,用户名:%s,缓存索引:%d\n",
           user_id, username, idx);

    // 释放写锁
    pthread_rwlock_unlock(&g_rwlock);

    // 模拟写操作耗时
    usleep(rand() % 500000);  // 0.1~0.5秒
}

// 读操作:查询缓存(根据用户ID获取用户名)
int read_cache(int user_id, char *username, int buf_len) {
    // 获取读锁(共享锁)
    pthread_rwlock_rdlock(&g_rwlock);

    int ret = -1;
    // 查找缓存
    int idx = user_id % CACHE_SIZE;
    if (g_cache[idx].is_valid && g_cache[idx].user_id == user_id) {
        strncpy(username, g_cache[idx].username, buf_len-1);
        username[buf_len-1] = '\0';
        ret = 0;  // 查找成功
        printf("读线程:查询缓存成功,用户ID:%d,用户名:%s,缓存索引:%d\n",
               user_id, username, idx);
    } else {
        printf("读线程:查询缓存失败,用户ID:%d(缓存无效或不存在)\n", user_id);
    }

    // 释放读锁
    pthread_rwlock_unlock(&g_rwlock);

    // 模拟读操作耗时
    usleep(rand() % 100000);  // 0.01~0.1秒
    return ret;
}

// 写线程入口函数:随机生成用户ID和用户名,更新缓存
void *write_thread_func(void *arg) {
    int thread_num = *(int *)arg;
    free(arg);

    for (int i = 0; i < OP_COUNT; i++) {
        // 生成随机用户ID(1~100)
        pthread_mutex_lock(&g_rand_mutex);
        int user_id = rand() % 100 + 1;
        pthread_mutex_unlock(&g_rand_mutex);

        // 生成随机用户名(user_xxx)
        char username[20];
        snprintf(username, sizeof(username), "user_%d_%d", thread_num, user_id);

        // 执行写操作
        write_cache(user_id, username);
    }

    printf("写线程%d:执行完毕,退出!\n", thread_num);
    return NULL;
}

// 读线程入口函数:随机查询用户ID对应的用户名
void *read_thread_func(void *arg) {
    int thread_num = *(int *)arg;
    free(arg);

    char username[20];
    for (int i = 0; i < OP_COUNT; i++) {
        // 生成随机用户ID(1~100)
        pthread_mutex_lock(&g_rand_mutex);
        int user_id = rand() % 100 + 1;
        pthread_mutex_unlock(&g_rand_mutex);

        // 执行读操作
        read_cache(user_id, username, sizeof(username));
    }

    printf("读线程%d:执行完毕,退出!\n", thread_num);
    return NULL;
}

int main() {
    pthread_t read_tids[READ_THREAD_COUNT];
    pthread_t write_tids[WRITE_THREAD_COUNT];
    int ret;

    // 初始化随机数种子
    srand(time(NULL));
    // 初始化缓存和同步机制
    init_cache();

    // 记录开始时间
    clock_t start_time = clock();

    // 创建写线程
    for (int i = 0; i < WRITE_THREAD_COUNT; i++) {
        int *thread_num = (int *)malloc(sizeof(int));
        *thread_num = i + 1;
        ret = pthread_create(&write_tids[i], NULL, write_thread_func, thread_num);
        if (ret != 0) {
            fprintf(stderr, "创建写线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    // 创建读线程
    for (int i = 0; i < READ_THREAD_COUNT; i++) {
        int *thread_num = (int *)malloc(sizeof(int));
        *thread_num = i + 1;
        ret = pthread_create(&read_tids[i], NULL, read_thread_func, thread_num);
        if (ret != 0) {
            fprintf(stderr, "创建读线程%d失败:%s\n", i+1, strerror(ret));
            return 1;
        }
    }

    // 等待所有写线程退出
    for (int i = 0; i < WRITE_THREAD_COUNT; i++) {
        pthread_join(write_tids[i], NULL);
    }

    // 等待所有读线程退出
    for (int i = 0; i < READ_THREAD_COUNT; i++) {
        pthread_join(read_tids[i], NULL);
    }

    // 记录结束时间,计算耗时
    clock_t end_time = clock();
    double cost_time = (double)(end_time - start_time) / CLOCKS_PER_SEC;

    // 销毁同步机制
    pthread_rwlock_destroy(&g_rwlock);
    pthread_mutex_destroy(&g_rand_mutex);

    printf("\n所有线程执行完毕!总耗时:%.4f秒\n", cost_time);
    printf("读线程数量:%d,写线程数量:%d,每个线程操作次数:%d\n",
           READ_THREAD_COUNT, WRITE_THREAD_COUNT, OP_COUNT);
    return 0;
}
运行结果(节选)
缓存初始化完成!缓存大小:10
写线程:更新缓存,用户ID:35,用户名:user_1_35,缓存索引:5
读线程:查询缓存失败,用户ID:78(缓存无效或不存在)
读线程:查询缓存失败,用户ID:23(缓存无效或不存在)
读线程:查询缓存成功,用户ID:35,用户名:user_1_35,缓存索引:5
读线程:查询缓存失败,用户ID:56(缓存无效或不存在)
写线程:更新缓存,用户ID:67,用户名:user_2_67,缓存索引:7
读线程:查询缓存成功,用户ID:67,用户名:user_2_67,缓存索引:7
读线程:查询缓存成功,用户ID:35,用户名:user_1_35,缓存索引:5
读线程:查询缓存成功,用户ID:67,用户名:user_2_67,缓存索引:7
...
写线程1:执行完毕,退出!
写线程2:执行完毕,退出!
读线程3:执行完毕,退出!
读线程1:执行完毕,退出!
读线程5:执行完毕,退出!
读线程2:执行完毕,退出!
读线程4:执行完毕,退出!

所有线程执行完毕!总耗时:1.8762秒
读线程数量:5,写线程数量:2,每个线程操作次数:10
代码解析
  • 读写分离:读操作获取读锁(共享),多个读线程可同时访问;写操作获取写锁(排他),确保写操作的原子性,解决“读-写”“写-写”冲突;
  • 高效并发:读操作远多于写操作时,读写锁的并发效率远高于互斥锁(互斥锁会导致所有读线程串行执行);
  • 随机数线程安全:使用互斥锁保护rand()函数调用(rand()是非线程安全的),避免多线程并发调用导致随机数生成异常;
  • 缓存设计:采用简单哈希映射(user_id % CACHE_SIZE)定位缓存位置,模拟真实缓存的存储逻辑,易于扩展为更复杂的缓存策略(如LRU)。

1.7 多线程编程常见问题与避坑指南

1.7.1 线程安全函数与非线程安全函数混用

问题:在多线程环境中使用非线程安全函数(如strtokrand),导致数据混乱或逻辑错误。
示例:

// 错误:strtok是非线程安全的,多个线程并发调用会覆盖静态中间状态
void *thread_func(void *arg) {
    char str[] = "a,b,c,d";
    char *token = strtok(str, ",");
    while (token != NULL) {
        printf("线程%d:token = %s\n", *(int *)arg, token);
        token = strtok(NULL, ",");
    }
    return NULL;
}

避坑方案:

  • 优先使用线程安全版本的函数(如strtok_r替代strtokrand_r替代rand);
  • 若必须使用非线程安全函数,需通过互斥锁保护,确保同一时刻只有一个线程调用;
  • 线程安全函数列表可参考POSIX标准或编译器文档,避免凭经验判断。

1.7.2 共享资源未加同步保护

问题:多个线程并发读写共享资源(全局变量、静态变量、堆内存),未使用锁或信号量保护,导致竞态条件。
避坑方案:

  • 明确共享资源:梳理程序中的共享资源,仅对必要的资源进行共享(减少同步开销);
  • 最小化锁粒度:仅对共享资源的访问代码加锁,避免整个函数或大段代码加锁;
  • 选择合适的同步机制:纯互斥场景用互斥锁,读多写少场景用读写锁,限制并发数用信号量。

1.7.3 线程退出时未释放资源

问题:线程退出时未释放持有的锁、动态内存、文件描述符等资源,导致资源泄漏或死锁。
示例:

// 错误:线程加锁后未解锁就退出,导致其他线程死锁
void *thread_func(void *arg) {
    pthread_mutex_lock(&mutex);
    // 模拟异常退出
    if (some_error) {
        return NULL;  // 未解锁,导致死锁
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

避坑方案:

  • 使用线程清理函数:通过pthread_cleanup_push注册清理函数,释放锁、内存等资源;
  • 检查所有退出路径:确保线程在任何退出路径(正常返回、pthread_exit、异常)都能释放资源;
  • 资源申请与释放成对出现:分配资源后立即规划释放时机,避免遗漏。

1.7.4 线程间传递局部变量指针

问题:将栈区的局部变量指针传递给其他线程,线程执行时局部变量已被销毁,导致野指针访问。
示例:

// 错误:传递局部变量指针给线程
void *thread_func(void *arg) {
    int *num = (int *)arg;
    printf("线程:num = %d\n", *num);  // 局部变量已销毁,野指针访问
    return NULL;
}

int main() {
    pthread_t tid;
    int local_num = 10;
    pthread_create(&tid, NULL, thread_func, &local_num);  // 传递局部变量指针
    pthread_join(tid, NULL);
    return 0;
}

避坑方案:

  • 传递全局变量或静态变量的指针(生命周期为程序全程);
  • 动态分配内存存储参数,线程执行完毕后释放(需确保线程已使用完参数);
  • 若传递局部变量,需确保线程执行期间局部变量未被销毁(如主线程等待线程退出后再销毁局部变量)。

1.7.5 过度依赖线程优先级

问题:通过设置线程优先级控制执行顺序,导致优先级反转或程序行为不稳定。
避坑方案:

  • 优先级仅用于提示调度器,不依赖优先级保证执行顺序(不同系统调度策略可能不同);
  • 关键执行顺序通过同步机制(条件变量、信号量)控制,而非优先级;
  • 若需设置优先级,需了解系统的调度策略(如Linux的SCHED_FIFO、SCHED_RR),避免设置无效优先级。

1.7.6 忽视线程创建失败处理

问题:调用pthread_create后未检查返回值,线程创建失败后程序继续执行,导致逻辑错误。
避坑方案:

  • 所有线程创建、同步机制初始化等操作都必须检查返回值;
  • 线程创建失败时,需释放已分配的资源,优雅退出程序或降级处理(如使用单线程执行);
  • 使用strerror函数输出错误描述,便于排查问题(如权限不足、资源耗尽)。

1.8 本章总结

本章系统讲解了C语言多线程编程的核心知识,核心内容包括:

  1. 多线程基础概念:明确了线程与进程的差异、并发与并行的区别,阐述了多线程编程的优势与潜在风险;
  2. POSIX线程库核心API:详细讲解了线程创建(pthread_create)、等待(pthread_join)、退出(pthread_exit)、取消(pthread_cancel)等基础操作,以及线程清理函数的使用;
  3. 线程同步与互斥机制:深入讲解了互斥锁(解决竞态条件)、条件变量(线程间通信)、信号量(灵活同步)、读写锁(读多写少场景优化)的原理与用法;
  4. 线程安全与并发问题:定义了线程安全的判断标准,分析了死锁、活锁、优先级反转等常见并发问题的产生原因与解决方案;
  5. 实战案例:实现了多线程矩阵乘法(并行计算)、通用线程池(线程复用)、读写锁缓存(高并发读写)三个工程常用案例,巩固了多线程编程技巧;
  6. 避坑指南:总结了线程安全函数混用、共享资源未保护、资源泄漏等常见问题,提供了具体的避坑方案。

多线程编程是C语言开发的高级技能,核心是“平衡并发效率与线程安全”。掌握本章内容后,你将能够充分利用多核CPU资源,实现高效的并发程序,应对服务器开发、嵌入式实时系统、数据并行计算等场景的需求。

下一章,我们将学习C语言的网络编程基础,包括TCP/UDP协议、Socket编程、客户端/服务器模型等核心知识点,实现网络数据传输功能。

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值