当前位置: 首页 > news >正文

pthread_mutex_lock函数深度解析

摘要

pthread_mutex_lock是POSIX线程库中用于实现线程同步的核心函数,它通过对互斥锁的加锁操作来确保多个线程对共享资源的安全访问。本文从互斥锁的历史背景和发展脉络入手,详细解析了pthread_mutex_lock函数的设计理念、实现机制和使用场景。通过生产者-消费者模型、线程安全数据结构和读写锁三个典型实例,深入探讨了该函数在实际应用中的具体实现方式。文章提供了完整的代码示例、Makefile配置以及Mermaid流程图和时序图,详细说明了编译运行方法和结果解读。最后,总结了pthread_mutex_lock的最佳实践和常见陷阱,为开发者提供全面的技术参考。


解析

1. 背景与核心概念

1.1 历史背景与发展历程

多线程编程的概念最早可以追溯到20世纪60年代,但直到90年代初期,随着对称多处理(SMP)系统的普及和硬件价格的下降,多线程编程才真正成为主流开发范式。在这个背景下,需要一种标准化的方式来管理线程间的同步问题。

1995年,IEEE制定了POSIX.1c标准(也称为pthreads),这是线程操作的第一个标准化接口。pthread_mutex_lock作为其中的核心同步原语,为多线程程序提供了一种可靠的互斥机制。

互斥锁(Mutual Exclusion Lock)的概念源于荷兰计算机科学家Edsger Dijkstra在1965年提出的信号量(Semaphore)概念。与信号量相比,互斥锁提供了更简单的接口和更明确的语义:一次只允许一个线程访问受保护的资源。

在Linux系统中,pthread库的实现经历了从LinuxThreads到NPTL(Native POSIX Threads Library)的演进。NPTL在Linux 2.6内核中引入,提供了更好的性能和可扩展性,使pthread_mutex_lock在各种场景下都能高效工作。

1.2 核心概念解析

互斥锁(Mutex) 是一种同步原语,用于保护共享资源,防止多个线程同时访问导致的竞态条件。pthread_mutex_lock函数是使用互斥锁的关键接口之一。

关键术语说明

术语解释
临界区需要互斥访问的代码段
竞态条件多个线程并发访问共享资源时的不确定行为
死锁两个或多个线程相互等待对方释放锁
饥饿线程长时间无法获取所需资源

互斥锁的状态转移图

初始化
pthread_mutex_lock()成功
pthread_mutex_unlock()
pthread_mutex_lock()阻塞
Unlocked
Locked
1.3 互斥锁的属性与类型

POSIX标准定义了多种互斥锁类型,每种类型有不同的行为特性:

类型特性适用场景
PTHREAD_MUTEX_NORMAL标准互斥锁,不检测死锁一般用途
PTHREAD_MUTEX_ERRORCHECK检测错误操作(如重复加锁)调试阶段
PTHREAD_MUTEX_RECURSIVE允许同一线程重复加锁递归函数
PTHREAD_MUTEX_DEFAULT系统默认类型,通常是NORMAL兼容性

2. 设计意图与考量

2.1 核心设计目标

pthread_mutex_lock的设计主要围绕以下几个目标:

  1. 原子性保证:确保锁的获取和释放操作是原子的,不会被打断
  2. 线程阻塞:当锁不可用时,调用线程能够高效地进入等待状态
  3. 公平性:避免线程饥饿,确保等待线程最终能获得锁
  4. 性能:在无竞争情况下开销最小
2.2 实现机制深度剖析

pthread_mutex_lock的实现通常依赖于底层硬件的原子操作指令(如x86的CMPXCHG)和操作系统内核的支持。其内部实现可以概括为以下几个步骤:

  1. 快速路径:尝试通过原子操作直接获取锁
  2. 中速路径:有限次数的自旋等待,避免立即进入内核态
  3. 慢速路径:进入内核态,将线程加入等待队列

这种多层次的实现策略能够在各种竞争情况下都保持良好的性能。

2.3 设计权衡因素

pthread_mutex_lock的设计需要考虑多个权衡因素:

  1. 响应时间 vs 吞吐量:自旋等待可以减少响应时间,但会增加CPU使用率
  2. 公平性 vs 性能:严格的公平性保证可能降低整体吞吐量
  3. 通用性 vs 特异性:通用实现适合大多数场景,但特定场景可能需要定制化实现

3. 实例与应用场景

3.1 生产者-消费者模型

生产者-消费者问题是并发编程中的经典问题,pthread_mutex_lock在这里起到关键作用。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>#define BUFFER_SIZE 5typedef struct {int buffer[BUFFER_SIZE];int count;int in;int out;pthread_mutex_t mutex;pthread_cond_t not_empty;pthread_cond_t not_full;
} buffer_t;void buffer_init(buffer_t *b) {b->count = 0;b->in = 0;b->out = 0;pthread_mutex_init(&b->mutex, NULL);pthread_cond_init(&b->not_empty, NULL);pthread_cond_init(&b->not_full, NULL);
}void buffer_put(buffer_t *b, int item) {pthread_mutex_lock(&b->mutex);// 等待缓冲区不满while (b->count == BUFFER_SIZE) {pthread_cond_wait(&b->not_full, &b->mutex);}// 放入项目b->buffer[b->in] = item;b->in = (b->in + 1) % BUFFER_SIZE;b->count++;pthread_cond_signal(&b->not_empty);pthread_mutex_unlock(&b->mutex);
}int buffer_get(buffer_t *b) {int item;pthread_mutex_lock(&b->mutex);// 等待缓冲区不空while (b->count == 0) {pthread_cond_wait(&b->not_empty, &b->mutex);}// 取出项目item = b->buffer[b->out];b->out = (b->out + 1) % BUFFER_SIZE;b->count--;pthread_cond_signal(&b->not_full);pthread_mutex_unlock(&b->mutex);return item;
}void* producer(void *arg) {buffer_t *b = (buffer_t*)arg;for (int i = 0; i < 10; i++) {printf("生产者放入: %d\n", i);buffer_put(b, i);sleep(1);}return NULL;
}void* consumer(void *arg) {buffer_t *b = (buffer_t*)arg;for (int i = 0; i < 10; i++) {int item = buffer_get(b);printf("消费者取出: %d\n", item);sleep(2);}return NULL;
}int main() {buffer_t b;pthread_t prod_thread, cons_thread;buffer_init(&b);pthread_create(&prod_thread, NULL, producer, &b);pthread_create(&cons_thread, NULL, consumer, &b);pthread_join(prod_thread, NULL);pthread_join(cons_thread, NULL);return 0;
}

流程图

ProducerBufferConsumer初始化缓冲区pthread_mutex_lock()获取互斥锁检查缓冲区是否满如果满,等待not_full条件放入物品pthread_cond_signal(not_empty)pthread_mutex_unlock()释放互斥锁loop[生产10个物品]pthread_mutex_lock()获取互斥锁检查缓冲区是否空如果空,等待not_empty条件取出物品pthread_cond_signal(not_full)pthread_mutex_unlock()释放互斥锁loop[消费10个物品]ProducerBufferConsumer
3.2 线程安全的数据结构

实现一个线程安全的链表结构,展示pthread_mutex_lock在数据结构保护中的应用。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>typedef struct node {int data;struct node *next;
} node_t;typedef struct {node_t *head;pthread_mutex_t mutex;
} thread_safe_list_t;void list_init(thread_safe_list_t *list) {list->head = NULL;pthread_mutex_init(&list->mutex, NULL);
}void list_insert(thread_safe_list_t *list, int data) {node_t *new_node = malloc(sizeof(node_t));new_node->data = data;pthread_mutex_lock(&list->mutex);new_node->next = list->head;list->head = new_node;pthread_mutex_unlock(&list->mutex);
}int list_remove(thread_safe_list_t *list, int data) {pthread_mutex_lock(&list->mutex);node_t *current = list->head;node_t *previous = NULL;int found = 0;while (current != NULL) {if (current->data == data) {if (previous == NULL) {list->head = current->next;} else {previous->next = current->next;}free(current);found = 1;break;}previous = current;current = current->next;}pthread_mutex_unlock(&list->mutex);return found;
}void list_print(thread_safe_list_t *list) {pthread_mutex_lock(&list->mutex);node_t *current = list->head;printf("链表内容: ");while (current != NULL) {printf("%d ", current->data);current = current->next;}printf("\n");pthread_mutex_unlock(&list->mutex);
}void* thread_func(void *arg) {thread_safe_list_t *list = (thread_safe_list_t*)arg;for (int i = 0; i < 5; i++) {int value = rand() % 100;list_insert(list, value);printf("线程 %ld 插入: %d\n", pthread_self(), value);list_print(list);}return NULL;
}int main() {thread_safe_list_t list;pthread_t threads[3];list_init(&list);for (int i = 0; i < 3; i++) {pthread_create(&threads[i], NULL, thread_func, &list);}for (int i = 0; i < 3; i++) {pthread_join(threads[i], NULL);}list_print(&list);return 0;
}
3.3 读写锁的实现

基于pthread_mutex_lock实现一个简单的读写锁,展示如何构建更高级的同步原语。

#include <stdio.h>
#include <pthread.h>typedef struct {pthread_mutex_t mutex;pthread_cond_t readers_cond;pthread_cond_t writers_cond;int readers_count;int writers_count;int writers_waiting;
} rwlock_t;void rwlock_init(rwlock_t *rw) {pthread_mutex_init(&rw->mutex, NULL);pthread_cond_init(&rw->readers_cond, NULL);pthread_cond_init(&rw->writers_cond, NULL);rw->readers_count = 0;rw->writers_count = 0;rw->writers_waiting = 0;
}void read_lock(rwlock_t *rw) {pthread_mutex_lock(&rw->mutex);// 等待没有写者正在写或等待写while (rw->writers_count > 0 || rw->writers_waiting > 0) {pthread_cond_wait(&rw->readers_cond, &rw->mutex);}rw->readers_count++;pthread_mutex_unlock(&rw->mutex);
}void read_unlock(rwlock_t *rw) {pthread_mutex_lock(&rw->mutex);rw->readers_count--;// 如果没有读者了,唤醒等待的写者if (rw->readers_count == 0) {pthread_cond_signal(&rw->writers_cond);}pthread_mutex_unlock(&rw->mutex);
}void write_lock(rwlock_t *rw) {pthread_mutex_lock(&rw->mutex);rw->writers_waiting++;// 等待没有读者和写者while (rw->readers_count > 0 || rw->writers_count > 0) {pthread_cond_wait(&rw->writers_cond, &rw->mutex);}rw->writers_waiting--;rw->writers_count++;pthread_mutex_unlock(&rw->mutex);
}void write_unlock(rwlock_t *rw) {pthread_mutex_lock(&rw->mutex);rw->writers_count--;// 优先唤醒等待的写者,否则唤醒所有读者if (rw->writers_waiting > 0) {pthread_cond_signal(&rw->writers_cond);} else {pthread_cond_broadcast(&rw->readers_cond);}pthread_mutex_unlock(&rw->mutex);
}

4. 代码实现与详细解析

4.1 pthread_mutex_lock的完整示例

下面是一个完整的示例,展示pthread_mutex_lock在各种场景下的使用:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>#define THREAD_COUNT 5// 全局共享资源
int shared_counter = 0;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;// 错误检查互斥锁
pthread_mutex_t errorcheck_mutex;// 递归互斥锁
pthread_mutex_t recursive_mutex;void init_mutexes() {// 初始化错误检查互斥锁pthread_mutexattr_t attr;pthread_mutexattr_init(&attr);pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);pthread_mutex_init(&errorcheck_mutex, &attr);// 初始化递归互斥锁pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);pthread_mutex_init(&recursive_mutex, &attr);pthread_mutexattr_destroy(&attr);
}// 简单的计数器线程函数
void* counter_thread(void* arg) {int id = *(int*)arg;for (int i = 0; i < 10000; i++) {pthread_mutex_lock(&counter_mutex);shared_counter++;pthread_mutex_unlock(&counter_mutex);}printf("线程 %d 完成\n", id);return NULL;
}// 错误检查互斥锁示例
void* errorcheck_thread(void* arg) {int result;// 第一次加锁应该成功result = pthread_mutex_lock(&errorcheck_mutex);if (result == 0) {printf("第一次加锁成功\n");}// 尝试重复加锁(应该失败)result = pthread_mutex_lock(&errorcheck_mutex);if (result == EDEADLK) {printf("检测到死锁:重复加锁\n");}pthread_mutex_unlock(&errorcheck_mutex);return NULL;
}// 递归函数使用递归互斥锁
void recursive_function(int depth) {if (depth <= 0) return;pthread_mutex_lock(&recursive_mutex);printf("递归深度: %d, 锁计数增加\n", depth);recursive_function(depth - 1);printf("递归深度: %d, 锁计数减少\n", depth);pthread_mutex_unlock(&recursive_mutex);
}// 递归互斥锁示例
void* recursive_thread(void* arg) {printf("开始递归函数演示\n");recursive_function(3);printf("结束递归函数演示\n");return NULL;
}int main() {pthread_t threads[THREAD_COUNT];int thread_ids[THREAD_COUNT];init_mutexes();// 测试1: 基本互斥锁printf("=== 测试1: 基本互斥锁 ===\n");for (int i = 0; i < THREAD_COUNT; i++) {thread_ids[i] = i;pthread_create(&threads[i], NULL, counter_thread, &thread_ids[i]);}for (int i = 0; i < THREAD_COUNT; i++) {pthread_join(threads[i], NULL);}printf("最终计数器值: %d (期望: %d)\n", shared_counter, THREAD_COUNT * 10000);// 测试2: 错误检查互斥锁printf("\n=== 测试2: 错误检查互斥锁 ===\n");pthread_t errorcheck_thread_obj;pthread_create(&errorcheck_thread_obj, NULL, errorcheck_thread, NULL);pthread_join(errorcheck_thread_obj, NULL);// 测试3: 递归互斥锁printf("\n=== 测试3: 递归互斥锁 ===\n");pthread_t recursive_thread_obj;pthread_create(&recursive_thread_obj, NULL, recursive_thread, NULL);pthread_join(recursive_thread_obj, NULL);// 清理资源pthread_mutex_destroy(&errorcheck_mutex);pthread_mutex_destroy(&recursive_mutex);printf("\n所有测试完成\n");return 0;
}
4.2 Makefile配置
# Compiler and flags
CC = gcc
CFLAGS = -Wall -Wextra -pedantic -std=c11 -pthread -D_GNU_SOURCE# Targets
TARGETS = mutex_demo producer_consumer thread_safe_list rwlock_demo# Default target
all: $(TARGETS)# Individual targets
mutex_demo: mutex_demo.c$(CC) $(CFLAGS) -o $@ $<producer_consumer: producer_consumer.c$(CC) $(CFLAGS) -o $@ $<thread_safe_list: thread_safe_list.c$(CC) $(CFLAGS) -o $@ $<rwlock_demo: rwlock_demo.c$(CC) $(CFLAGS) -o $@ $<# Clean up
clean:rm -f $(TARGETS) *.o# Run all demos
run: all@echo "=== 运行互斥锁演示 ==="./mutex_demo@echo ""@echo "=== 运行生产者-消费者演示 ==="./producer_consumer@echo ""@echo "=== 运行线程安全链表演示 ==="./thread_safe_list@echo ""@echo "=== 运行读写锁演示 ==="./rwlock_demo.PHONY: all clean run
4.3 编译与运行

编译方法

make

运行方法

make run

预期输出

=== 运行互斥锁演示 ===
线程 0 完成
线程 1 完成
线程 2 完成
线程 3 完成
线程 4 完成
最终计数器值: 50000 (期望: 50000)=== 测试2: 错误检查互斥锁 ===
第一次加锁成功
检测到死锁:重复加锁=== 测试3: 递归互斥锁 ===
开始递归函数演示
递归深度: 3, 锁计数增加
递归深度: 2, 锁计数增加
递归深度: 1, 锁计数增加
递归深度: 1, 锁计数减少
递归深度: 2, 锁计数减少
递归深度: 3, 锁计数减少
结束递归函数演示所有测试完成
4.4 pthread_mutex_lock的内部流程
调用 pthread_mutex_lock
锁是否可用?
获取锁
是自旋锁且自旋次数未满?
自旋等待
加入等待队列
线程阻塞
等待唤醒
返回成功

5. 交互性内容解析

5.1 多线程竞争时序分析

当多个线程同时竞争同一个互斥锁时,pthread_mutex_lock的内部行为可以通过以下时序图展示:

线程1线程2线程3互斥锁内核调度器初始状态: 锁可用pthread_mutex_lock()锁状态: 已锁定(线程1)返回成功pthread_mutex_lock()锁状态: 已锁定(线程1)线程2加入等待队列线程2阻塞调度出CPUpthread_mutex_lock()锁状态: 已锁定(线程1)线程3加入等待队列线程3阻塞调度出CPUpthread_mutex_unlock()锁状态: 可用唤醒等待队列中的线程唤醒线程2调度到CPU获取锁成功锁状态: 已锁定(线程2)返回成功pthread_mutex_unlock()锁状态: 可用唤醒线程3唤醒线程3调度到CPU获取锁成功锁状态: 已锁定(线程3)返回成功线程1线程2线程3互斥锁内核调度器
5.2 优先级反转问题

优先级反转是多线程系统中的经典问题,发生在高优先级线程等待低优先级线程持有的锁时:

高优先级线程中优先级线程低优先级线程互斥锁时间点1: 低优先级线程获取锁pthread_mutex_lock()成功时间点2: 高优先级线程就绪pthread_mutex_lock()阻塞高优先级线程等待低优先级线程时间点3: 中优先级线程就绪执行任务(无阻塞)低优先级线程被中优先级线程抢占时间点4: 优先级反转发生高优先级线程间接等待中优先级线程尽管它的优先级更高高优先级线程中优先级线程低优先级线程互斥锁

解决优先级反转的方法包括优先级继承和优先级天花板协议,Linux的pthread_mutex_lock实现了优先级继承协议。

6. 最佳实践与常见陷阱

6.1 最佳实践
  1. 锁的粒度控制:锁的粒度应该尽可能小,只保护必要的共享数据,减少锁的持有时间

  2. 避免嵌套锁:尽量避免在持有一个锁的情况下获取另一个锁,这容易导致死锁

  3. 使用RAII模式:在C++中,使用RAII(Resource Acquisition Is Initialization)模式管理锁的生命周期

    class ScopedLock {
    public:ScopedLock(pthread_mutex_t& mutex) : mutex_(mutex) {pthread_mutex_lock(&mutex_);}~ScopedLock() {pthread_mutex_unlock(&mutex_);}
    private:pthread_mutex_t& mutex_;
    };
    
  4. 错误检查:始终检查pthread_mutex_lock的返回值,处理可能的错误情况

  5. 锁的顺序:如果必须使用多个锁,确保所有线程以相同的顺序获取锁

6.2 常见陷阱
  1. 死锁:多个线程相互等待对方释放锁

    • 解决方案:使用锁层次结构、超时机制或死锁检测算法
  2. 活锁:线程不断重试某个操作但无法取得进展

    • 解决方案:引入随机退避机制
  3. 优先级反转:高优先级线程被低优先级线程阻塞

    • 解决方案:使用优先级继承协议
  4. 锁护送(Lock Convoy):多个线程频繁竞争同一个锁,导致性能下降

    • 解决方案:减少锁的粒度或使用无锁数据结构
  5. 忘记释放锁:导致其他线程无法获取锁

    • 解决方案:使用RAII模式或静态分析工具

7. 性能优化技巧

  1. 选择适当的锁类型:根据场景选择最合适的锁类型(自旋锁、互斥锁、读写锁等)

  2. 减少锁竞争:通过数据分片(sharding)减少对单个锁的竞争

  3. 使用读写锁:当读操作远多于写操作时,使用读写锁可以提高并发性

  4. 无锁编程:对于性能关键区域,考虑使用无锁数据结构和算法

  5. 本地化处理:尽可能在线程本地处理数据,减少共享数据的使用

8. 调试与诊断

  1. 使用调试版本:在调试时使用PTHREAD_MUTEX_ERRORCHECK类型,可以检测常见的错误用法

  2. 死锁检测工具:使用Helgrind、DRD等工具检测死锁和锁 misuse

  3. 性能分析:使用perf、strace等工具分析锁竞争情况

  4. 日志记录:在关键部分添加日志记录,跟踪锁的获取和释放顺序

9. 总结

pthread_mutex_lock是POSIX线程编程中最基础和重要的同步原语之一。正确理解和使用pthread_mutex_lock对于编写正确、高效的多线程程序至关重要。通过本文的详细解析,我们深入探讨了:

  1. pthread_mutex_lock的历史背景和核心概念
  2. 其设计目标和实现机制
  3. 在实际应用中的各种使用场景和示例
  4. 最佳实践和常见陷阱
  5. 性能优化和调试技巧

掌握pthread_mutex_lock不仅意味着理解一个API函数的用法,更重要的是理解多线程同步的核心思想和原则。在实际开发中,应该根据具体需求选择合适的同步策略,并始终注意避免常见的并发编程陷阱。


文章转载自:

http://uXtqjvnU.cfcpb.cn
http://PkFwdbVP.cfcpb.cn
http://tEL9dy9G.cfcpb.cn
http://oSoHKfgS.cfcpb.cn
http://1G0XRk1a.cfcpb.cn
http://kqCuKGhO.cfcpb.cn
http://3JJNsv4D.cfcpb.cn
http://iiPN5gyv.cfcpb.cn
http://w0aILrjz.cfcpb.cn
http://2RaZ38DW.cfcpb.cn
http://uDYyw1f1.cfcpb.cn
http://DcfUbDiy.cfcpb.cn
http://iuLg8ujB.cfcpb.cn
http://McCdAGGr.cfcpb.cn
http://gMXcpoNT.cfcpb.cn
http://4TljaZOc.cfcpb.cn
http://7QEpnnqS.cfcpb.cn
http://JJsbNmt5.cfcpb.cn
http://rwCi0oqX.cfcpb.cn
http://HPA0wNhV.cfcpb.cn
http://WKki6TCf.cfcpb.cn
http://Nt0VhkKe.cfcpb.cn
http://6bdFptDx.cfcpb.cn
http://b23K0Viv.cfcpb.cn
http://0lmmblld.cfcpb.cn
http://bXQnpUsk.cfcpb.cn
http://gdkaUnM0.cfcpb.cn
http://nBx5DWNg.cfcpb.cn
http://iOfuIB7x.cfcpb.cn
http://Jy5YAjzK.cfcpb.cn
http://www.dtcms.com/a/383260.html

相关文章:

  • 【记录】初赛复习 Day1
  • 深入理解跳表(Skip List):原理、实现与应用
  • SciKit-Learn 全面分析 20newsgroups 新闻组文本数据集(文本分类)
  • 使用 Neo4j 和 Ollama 在本地构建知识图谱
  • 【愚公系列】《人工智能70年》018-语音识别的历史性突破(剑桥语音的黄金十年)
  • Debezium日常分享系列之:MongoDB 新文档状态提取
  • Linux 日志分析:用 ELK 搭建个人运维监控平台
  • docker内如何用ollama启动大模型
  • Flask学习笔记(二)--路由和变量
  • FlashAttention(V3)深度解析:从原理到工程实现-Hopper架构下的注意力机制优化革命
  • 一文入门:机器学习
  • Uniswap:DeFi领域的革命性交易协议
  • 3. 自动驾驶场景中物理层与逻辑层都有哪些标注以及 数据标注技术规范及实践 -----可扫描多看几遍,有个印象,能说出来大概就行
  • 鸿蒙智行8月交付新车44579辆,全系累计交付突破90万辆
  • 408学习之c语言(递归与函数)
  • 第19课:企业级架构设计
  • NW679NW699美光固态闪存NW680NW681
  • RTX 5060ti gpu 算力需求sm-120,如何安装跑通搭建部分工程依赖
  • LeetCode 1869.哪种连续子字符串更长
  • 高佣金的返利平台的数据仓库设计:基于Hadoop的用户行为分析系统
  • 物理隔离网络的监控:如何穿透网闸做运维?
  • 知识图谱网页版可视化可移动代码
  • 【iOS】static、const、extern关键字
  • Grafana+Loki+Alloy构建企业级日志平台
  • Redis 实现分布式锁的探索与实践
  • 设计模式-适配器模式详解
  • Java 分布式缓存实现:结合 RMI 与本地文件缓存
  • Ajax-day2(图书管理)-渲染列表
  • 在Excel和WPS表格中快速复制上一行内容
  • 11-复习java程序设计中学习的面向对象编程