【Linux】线程同步与互斥(一):线程互斥原理与mutex详解

文章目录

    • Linux线程同步与互斥(一):线程互斥原理与mutex详解
    • 一、为什么需要互斥
      • [1.1 几个基本概念](#1.1 几个基本概念)
      • [1.2 售票系统的数据竞争问题](#1.2 售票系统的数据竞争问题)
      • [1.3 汇编层面看 ticket-- 的非原子性](#1.3 汇编层面看 ticket-- 的非原子性)
    • 二、pthread_mutex:互斥锁
      • [2.1 互斥锁的基本使用](#2.1 互斥锁的基本使用)
        • [2.1.1 初始化](#2.1.1 初始化)
        • [2.1.2 加锁和解锁](#2.1.2 加锁和解锁)
        • [2.1.3 销毁锁](#2.1.3 销毁锁)
      • [2.2 修复售票系统](#2.2 修复售票系统)
      • [2.3 一个常见错误](#2.3 一个常见错误)
    • 三、互斥量的底层原理
      • [3.1 如何实现原子操作](#3.1 如何实现原子操作)
        • [3.1.1 Test-And-Set指令](#3.1.1 Test-And-Set指令)
        • [3.1.2 Compare-And-Swap指令](#3.1.2 Compare-And-Swap指令)
      • [3.2 mutex的实现原理](#3.2 mutex的实现原理)
        • [3.2.1 简化的实现思路](#3.2.1 简化的实现思路)
        • [3.2.2 为什么要先自旋再休眠?](#3.2.2 为什么要先自旋再休眠?)
    • 四、互斥量的RAII封装
      • [4.1 手动加解锁的问题](#4.1 手动加解锁的问题)
      • [4.2 RAII风格的封装](#4.2 RAII风格的封装)
        • [4.2.1 Mutex类封装](#4.2.1 Mutex类封装)
        • [4.2.2 LockGuard自动管理](#4.2.2 LockGuard自动管理)
        • [4.2.3 使用示例](#4.2.3 使用示例)
      • [4.3 对比C++11的std::lock_guard](#4.3 对比C++11的std::lock_guard)
    • 五、互斥的三原则
      • [5.1 三原则内容](#5.1 三原则内容)
      • [5.2 违反原则的例子](#5.2 违反原则的例子)
    • 六、本篇总结
      • [6.1 核心知识点](#6.1 核心知识点)
      • [6.2 最重要的理解](#6.2 最重要的理解)

Linux线程同步与互斥(一):线程互斥原理与mutex详解

💬 重磅来袭 :前面的文章把线程创建、管理、内存布局都讲清楚了,线程能跑了。但多个线程一起跑,马上就会遇到新麻烦:它们要访问同一份数据怎么办?比如售票系统,四个窗口同时卖票,票数是共享的,不加控制就会卖出负数票。这就是本篇要解决的核心问题------如何让多个线程安全地访问共享资源。我们会从一个会出错的售票代码开始,看看数据竞争是怎么发生的,然后引入互斥锁(mutex)来解决问题,最后深入到汇编层面理解为什么需要原子操作,并用RAII风格封装出好用的锁工具。

👍 点赞、收藏与分享:本篇包含大量实战代码、汇编分析、底层原理图示,是理解多线程并发安全的基础!如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:从问题复现到原理分析,再到工具封装,一步步掌握互斥锁的使用。


一、为什么需要互斥

1.1 几个基本概念

先把后面要用到的术语理解清楚,这些概念是理解多线程安全的基础。

临界资源 :多个线程都能访问的共享数据。比如一个全局变量 int ticket = 100;,所有线程都能读写它,它就是临界资源。

临界区 :访问临界资源的那段代码。比如下面的 ticket--; 这一行就是临界区:

c 复制代码
if (ticket > 0) {
    ticket--;  // ← 这是临界区
}

互斥:任何时刻只允许一个线程进入临界区。这是保护临界资源的核心手段。

原子性 :一个操作要么完成,要么未完成,中间不会被打断。比如 ticket--; 看起来是一条语句,但实际上不是原子的(后面会看汇编证明)。

📌 这里要明确:临界资源和临界区是一体的,保护临界资源就是保护临界区,让同一时刻只有一个线程在临界区里执行。

1.2 售票系统的数据竞争问题

看一个典型场景------多线程抢票系统。四个线程模拟四个售票窗口,共享一个票池。

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.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;
        }
    }
    return NULL;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

直觉上这代码没问题:先判断 ticket > 0,然后卖票,最后 ticket--。但实际运行结果:

bash 复制代码
thread 4 sells ticket:100
thread 1 sells ticket:99
thread 3 sells ticket:98
...
thread 4 sells ticket:3
thread 2 sells ticket:2
thread 1 sells ticket:1
thread 4 sells ticket:0
thread 3 sells ticket:-1
thread 2 sells ticket:-2

票卖成负数了!问题出在三个地方:

问题1:if 判断后可能切换线程

线程1判断 ticket > 0 为真,但还没执行 ticket--,就被调度走了。这时线程2、3、4进来,都看到 ticket > 0,都进入了临界区。

问题2:usleep 期间大量线程进入

这个模拟业务处理的 sleep,给了其他线程充足时间进入临界区。等线程1从 sleep 醒来,可能有好几个线程都拿到了同一张票。

问题3:ticket-- 不是原子操作

这是最关键的。我们以为 ticket-- 是一条指令,但实际上它会被编译成多条机器指令。

1.3 汇编层面看 ticket-- 的非原子性

objdump -d 反汇编看看 ticket--; 对应的机器码:

bash 复制代码
$ gcc test.c -o test -pthread
$ objdump -d test > test.asm

找到 ticket--; 对应的汇编:

asm 复制代码
40064b: 8b 05 e3 04 20 00    mov    0x2004e3(%rip),%eax  # 从内存加载ticket到eax
400651: 83 e8 01             sub    $0x1,%eax             # eax - 1
400654: 89 05 da 04 20 00    mov    %eax,0x2004da(%rip)  # 写回内存

三条指令,对应三个步骤:

bash 复制代码
1. load:从内存读取 ticket 到寄存器 eax
2. update:寄存器 eax 的值减1
3. store:把 eax 的值写回内存

这三步之间都可能发生线程切换!假设当前 ticket = 5,看看两个线程交错执行会怎样:

bash 复制代码
时刻  线程1              线程2           ticket值(内存)
T1    load (读到5)                        5
T2                      load (读到5)      5
T3    update (eax=4)                      5
T4                      update (eax=4)    5
T5    store (写入4)                       4
T6                      store (写入4)     4

两个线程都执行了 ticket--,但 ticket 只减了1!这就是数据竞争(data race)

📌 记住这个结论:C语言里一行代码不等于原子操作。除非用特殊的原子指令(后面会讲),否则任何操作都可能被拆成多条指令,在多线程环境下就不安全。


二、pthread_mutex:互斥锁

2.1 互斥锁的基本使用

既然问题是"多个线程同时进临界区",解决办法就是"加锁":谁拿到锁谁进去,其他人等着。Linux 提供的这把锁叫互斥量(mutex)

2.1.1 初始化

两种方式:

方式1:静态初始化(适合全局变量)

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

方式2:动态初始化(适合在函数中或结构体中)

c 复制代码
pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

参数:
  mutex:要初始化的互斥量
  attr:NULL 表示默认属性
返回值:成功返回0,失败返回错误号
2.1.2 加锁和解锁
c 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);    // 加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);  // 解锁
返回值:成功返回0,失败返回错误号

pthread_mutex_lock 的行为:

bash 复制代码
情况1:锁是空闲的
  → 直接拿到锁,函数立即返回

情况2:锁被其他线程占用
  → 当前线程阻塞,进入等待队列
  → 注意是阻塞,不是忙等
  → 线程被操作系统挂起,不占用CPU
  → 等锁的线程被唤醒后,还要再竞争锁
  → 竞争成功,lock 函数才返回
2.1.3 销毁锁
c 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意事项:

bash 复制代码
1. 静态初始化的 mutex 在进程结束时会被回收,通常不强制 destroy;但如果要严格资源管理或复用,仍可以在确保未加锁状态下 destroy。
2. 不要销毁已加锁的锁
3. 销毁后不能再使用

2.2 修复售票系统

加上锁之后:

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.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);  // ← 解锁
        } else {
            pthread_mutex_unlock(&mutex);  // ← 别忘了这里也要解锁
            break;
        }
    }
    return NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);  // 初始化
    
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    
    pthread_mutex_destroy(&mutex);  // 销毁
    return 0;
}

编译运行:

bash 复制代码
$ gcc test.c -o test -pthread
$ ./test
thread 1 sells ticket:100
thread 2 sells ticket:99
thread 3 sells ticket:98
thread 4 sells ticket:97
...
thread 2 sells ticket:3
thread 1 sells ticket:2
thread 4 sells ticket:1

现在正常了,ticket 从 100 减到 1,不会出现负数。

📌 关键点pthread_mutex_lockpthread_mutex_unlock 之间的代码就是临界区,同一时刻只有一个线程能执行。加锁的粒度要合适,太大影响并发性能,太小又保护不了数据。

2.3 一个常见错误

c 复制代码
if (ticket > 0) {                // 判断在锁外(错误点)
    pthread_mutex_lock(&mutex);
    usleep(1000);
    printf("%s sells ticket:%d\n", id, ticket);
    ticket--;
    pthread_mutex_unlock(&mutex);
}

看起来也加锁了,有问题吗?问题在于if 判断在锁外

bash 复制代码
时刻  线程1                      线程2                ticket值
T1    if (ticket > 0) √                               1
T2                              if (ticket > 0) √     1
T3    lock                                            1
T4    ticket--                                        0
T5    unlock                                          0
T6                              lock                  0
T7                              ticket--              -1

if 判断不在保护范围内,两个线程都看到 ticket = 1,都进入了 if 块,最后 ticket 变成 -1。

正确做法:判断和操作都要在锁的保护下

本质是:检查和修改必须在同一把锁的保护下,组成一个不可分割的临界区。


三、互斥量的底层原理

3.1 如何实现原子操作

前面看到 ticket--; 被编译成三条指令,不是原子的。那怎么实现原子操作呢?

硬件提供了一些特殊指令,保证操作的原子性。最常用的是Test-And-Set (TAS)Compare-And-Swap (CAS)

3.1.1 Test-And-Set指令
c 复制代码
// 硬件提供的原子操作(伪代码)
int test_and_set(int *lock) {
    int old = *lock;
    *lock = 1;
    return old;
}
// 整体原子执行

用 TAS 实现自旋锁:

c 复制代码
typedef struct {
    int flag;
} spinlock_t;

void spin_lock(spinlock_t *lock) {
    while (test_and_set(&lock->flag) == 1) {
        // 如果返回1,说明锁已被占用,继续自旋
    }
    // 如果返回0,说明拿到锁了
}

void spin_unlock(spinlock_t *lock) {
    lock->flag = 0;  // 释放锁
}
3.1.2 Compare-And-Swap指令
c 复制代码
// 硬件提供的原子操作(伪代码)
int compare_and_swap(int *ptr, int old_val, int new_val) {
    int old = *ptr;
    if (old == old_val) {
        *ptr = new_val;
        return 1;  // 成功
    }
    return 0;      // 失败
}
// 这整个过程是原子的

x86 的 cmpxchg 指令就是 CAS 的实现:

asm 复制代码
# AT&T语法
lock cmpxchg %ebx, (%ecx)

# 作用:
# 比较 eax 和 (%ecx) 的值
# 如果相等,把 ebx 的值写入 (%ecx)
# 如果不等,把 (%ecx) 的值读入 eax
# lock前缀保证操作的原子性

3.2 mutex的实现原理

pthread_mutex 的底层实现结合了自旋休眠两种策略。

多数 Linux/glibc 的 mutex 在特定条件下会采用自适应策略:轻度争用时可能短暂自旋,争用严重时通过 futex

休眠/唤醒。下面是"简化示意",用于理解思想,不代表真实源码。

3.2.1 简化的实现思路
c 复制代码
struct pthread_mutex {
    int lock;        // 0:未锁,1:已锁
    int owner;       // 持有锁的线程ID
    int waiters;     // 等待队列中的线程数
    // ... 其他字段
};

int pthread_mutex_lock(pthread_mutex_t *mutex) {
    // 1. 先自旋一小段时间(几十到几百次循环)
    for (int i = 0; i < SPIN_COUNT; i++) {
        if (atomic_compare_and_swap(&mutex->lock, 0, 1)) {
            mutex->owner = gettid();
            return 0;  // 拿到锁了
        }
        cpu_relax();  // 暂停一下,减少总线竞争
    }
    
    // 2. 自旋拿不到锁,进入休眠
    mutex->waiters++;
    futex_wait(&mutex->lock, 1);  // 系统调用,线程休眠
    mutex->waiters--;
    
    // 3. 被唤醒后,再次尝试获取锁
    while (!atomic_compare_and_swap(&mutex->lock, 0, 1)) {
        futex_wait(&mutex->lock, 1);
    }
    mutex->owner = gettid();
    return 0;
}

int pthread_mutex_unlock(pthread_mutex_t *mutex) {
    mutex->owner = 0;
    atomic_store(&mutex->lock, 0);
    
    // 如果有等待者,唤醒一个
    if (mutex->waiters > 0) {
        futex_wake(&mutex->lock, 1);  // 系统调用,唤醒一个线程
    }
    return 0;
}
3.2.2 为什么要先自旋再休眠?
bash 复制代码
原因1:系统调用开销大
  - futex_wait/futex_wake 是系统调用
  - 用户态→内核态切换开销大(几千个CPU周期)
  - 如果锁很快就被释放,自旋更高效

原因2:很多锁持有时间很短
  - 数据显示,大部分锁的持有时间 < 100个CPU周期
  - 自旋几十次就能拿到锁,比休眠划算

策略:
  - 短时间持有的锁:自旋效率高
  - 长时间持有的锁:休眠避免浪费CPU
  - pthread_mutex 结合两者,先自旋后休眠

📌 这里要理解 :mutex不是纯粹的自旋锁(一直占CPU),也不是纯粹的休眠锁(立即休眠),而是混合锁,根据实际情况动态调整策略。


四、互斥量的RAII封装

4.1 手动加解锁的问题

看一段代码:

cpp 复制代码
void process() {
    pthread_mutex_lock(&mutex);
    
    if (condition1) {
        // ... 一些操作
        pthread_mutex_unlock(&mutex);
        return;  // 提前返回
    }
    
    if (condition2) {
        // ... 一些操作
        // 忘记解锁了!!!
        return;  // 提前返回
    }
    
    // ... 正常流程
    pthread_mutex_unlock(&mutex);
}

问题:

bash 复制代码
1. 多个返回路径,每个都要记得解锁
2. 中间可能抛异常(C++),跳过解锁
3. 代码复杂后,很容易漏掉unlock
4. 造成死锁,其他线程永远等不到锁

4.2 RAII风格的封装

RAII (Resource Acquisition Is Initialization):资源获取即初始化。核心思想:利用对象的构造和析构来管理资源。

4.2.1 Mutex类封装

Lock.hpp:

cpp 复制代码
#pragma once
#include <pthread.h>

namespace LockModule
{
    // 对锁进行封装
    class Mutex
    {
    public:
        Mutex(const Mutex &) = delete;  // 禁止拷贝
        const Mutex &operator=(const Mutex &) = delete;  // 禁止赋值
        
        Mutex() {
            pthread_mutex_init(&_mutex, nullptr);
        }
        
        void Lock() {
            pthread_mutex_lock(&_mutex);
        }
        
        void Unlock() {
            pthread_mutex_unlock(&_mutex);
        }
        
        pthread_mutex_t *GetMutexOriginal() {
            return &_mutex;  // 给条件变量用
        }
        
        ~Mutex() {
            pthread_mutex_destroy(&_mutex);
        }
        
    private:
        pthread_mutex_t _mutex;
    };
}
4.2.2 LockGuard自动管理

继续在 Lock.hpp 中添加:

cpp 复制代码
namespace LockModule
{
    // ... Mutex类定义 ...
    
    // 采用RAII风格,自动加锁解锁
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex) {
            _mutex.Lock();  // 构造时加锁
        }
        
        ~LockGuard() {
            _mutex.Unlock();  // 析构时解锁
        }
        
    private:
        Mutex &_mutex;
    };
}
4.2.3 使用示例
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "Lock.hpp"

using namespace LockModule;

int ticket = 1000;
Mutex mutex;

void *route(void *arg) {
    char *id = (char *)arg;
    while (1) {
        {
            LockGuard lockguard(mutex);  // ← 构造时自动加锁
            if (ticket > 0) {
                usleep(1000);
                printf("%s sells ticket:%d\n", id, ticket);
                ticket--;
            } else {
                break;
            }
        }  // ← 析构时自动解锁,即使上面有return或throw也会执行
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");
    
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    
    return 0;
}

编译运行:

bash 复制代码
$ g++ test.cpp -o test -std=c++11 -lpthread
$ ./test
thread 1 sells ticket:1000
thread 2 sells ticket:999
thread 3 sells ticket:998
...
thread 4 sells ticket:2
thread 1 sells ticket:1

📌 RAII的好处

bash 复制代码
1. 不会忘记解锁
   - 离开作用域,析构函数自动调用
   
2. 异常安全
   - 即使中间抛异常,栈展开会调用析构
   
3. 代码简洁
   - 不用写unlock,减少出错

4. 符合C++习惯
   - std::lock_guard 也是这个思路

4.3 对比C++11的std::lock_guard

C++11 标准库提供了类似的工具:

cpp 复制代码
#include <mutex>

std::mutex mtx;

void process() {
    std::lock_guard<std::mutex> guard(mtx);  // 自动加锁
    
    // ... 临界区代码
    
}  // 自动解锁

我们封装的 LockGuard 和它原理一样,只是底层用的是 pthread_mutex 而不是 std::mutex


五、互斥的三原则

5.1 三原则内容

设计良好的互斥机制要满足三个条件:

1. 互斥性

bash 复制代码
同一时刻,只有一个线程在临界区执行
- 这是最基本的要求
- pthread_mutex 通过原子操作保证

2. 有限等待

bash 复制代码
如果多个线程同时要求进入临界区,
并且临界区没有线程在执行,
那么只能允许一个线程进入,且选择过程不能无限推迟

- 不能让某个线程永远饿死
- pthread_mutex 通过等待队列保证公平性
- pthread_mutex 通常能避免长期占用导致的严重饥饿,但标准并不保证严格公平/严格有限等待。

3. 空闲让进

bash 复制代码
如果没有线程在临界区,
那么任何申请进入临界区的线程都应该被允许进入

- 临界区空着,不能阻止线程进入
- 不能因为之前的状态阻止新线程

5.2 违反原则的例子

违反空闲让进:

c 复制代码
// 错误的"轮流"实现
int turn = 0;  // 0表示线程0的回合,1表示线程1的回合

// 线程0
while (1) {
    while (turn != 0);  // 等待自己的回合
    // 临界区
    turn = 1;
}

// 线程1
while (1) {
    while (turn != 1);  // 等待自己的回合
    // 临界区
    turn = 0;
}

问题:

该算法采用严格轮流方式进入临界区。 当某线程不请求进入临界区时,另一线程仍可能因 turn

值不符而被阻塞,即使临界区处于空闲状态,因此违反"空闲让进"原则。

同时,该算法使用忙等待,会导致CPU资源浪费。

📌 记住 :设计并发控制机制时,要满足这三原则。pthread_mutex 已经帮我们实现好了,直接用就行。


六、本篇总结

6.1 核心知识点

1. 基本概念

  • 临界资源:多线程共享的数据
  • 临界区:访问临界资源的代码段
  • 互斥:同一时刻只有一个线程在临界区
  • 原子性:操作不可分割,要么完成要么未完成

2. 数据竞争

  • C语言的一行代码不等于原子操作
  • ticket--; 被编译成 load-update-store 三条指令
  • 多线程交错执行会导致数据不一致

3. pthread_mutex

函数 作用
pthread_mutex_init 初始化锁
pthread_mutex_lock 加锁
pthread_mutex_unlock 解锁
pthread_mutex_destroy 销毁锁

4. 底层原理

  • 硬件提供原子指令:TAS、CAS
  • mutex 混合策略:先自旋后休眠
  • futex 系统调用管理休眠和唤醒

5. RAII封装

  • 利用构造/动管理资源
  • 避免忘记解锁
  • 异常安全

6. 互斥三原则

  • 互斥性:同一时刻只有一个线程在临界区
  • 有限等待:不能无限推迟,不能饿死线程
  • 空闲让进:临界区空闲时允许线程进入

6.2 最重要的理解

📌 多线程编程的核心

bash 复制代码
问题:多个线程访问共享数据会产生数据竞争
原因:操作不是原子的,线程切换导致交错执行
解决:用互斥锁保护临界区

记住:
1. 共享数据必须加锁保护
2. 加锁粒度要合适(太大影响性能,太小保护不了)
3. 判断和操作都要在锁的保护下
4. 用RAII避免忘记解锁

💬 下篇预告:互斥锁解决了安全问题,但还不够。比如生产者-消费者模型,生产者生产数据,消费者消费数据,两者要协调:队列空的时候消费者要等,队列满的时候生产者要等。这就需要**条件变量(condition variable)**来实现线程同步。下篇我们会详细讲解条件变量的原理和使用,并实现经典的生产者-消费者模型。

👍 点赞、收藏与分享:如果这篇文章对你有帮助,请点赞、收藏并分享!

相关推荐
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.3 小时前
Keepalived 双主(Active‑Active)模式
运维·服务器
HalvmånEver3 小时前
Linux:进程 vs 线程:资源共享与独占全解析(线程四)
java·linux·运维
2501_940315263 小时前
leetcode统计一致字符串的数目(哈希表)
算法·哈希算法·散列表
清铎3 小时前
项目_Agent实战
开发语言·人工智能·深度学习·算法·机器学习
Queenie_Charlie3 小时前
位移运算
c++·位运算
J_liaty3 小时前
SpringBoot 自定义注解实现接口加解密:一套完整的多算法方案
java·spring boot·算法
hurrycry_小亦3 小时前
洛谷题目:P1365 WJMZBMR打osu! / Easy 题解(本题较简)
c++
m0_748708053 小时前
C++代码移植性设计
开发语言·c++·算法
yuanjj883 小时前
域格移芯平台模块Linux下RNDIS、ECM拨号及网口名称修改
linux·rndis·ecm·ttyacm