C++ 原子操作

原子操作是 C++11 引入的并发编程核心基础设施,它能在无锁 的前提下,保证多线程环境下变量操作的不可分割性、内存可见性与指令有序性,彻底解决多线程数据竞争问题,同时实现远高于互斥锁的低延迟高性能。

一、并发编程的核心痛点

一个极简的例子,直观理解多线程并发的核心问题:数据竞争

cpp 复制代码
#include <thread>
#include <iostream>

// 普通全局变量,多线程同时修改
int counter = 0;

void add_10w_times() {
    for (int i = 0; i < 100000; ++i) {
        counter++; // 看似一行代码,底层是3步非原子操作
    }
}

int main() {
    std::thread t1(add_10w_times);
    std::thread t2(add_10w_times);
    t1.join();
    t2.join();

    // 预期结果:200000,实际每次运行结果都不同,且远小于200000
    std::cout << "最终结果:" << counter << std::endl;
    return 0;
}

counter++ 这行代码,在 CPU 底层会被拆分为3 步独立指令:

  • 读:从内存把 counter 的值加载到 CPU 寄存器;
  • 改:在寄存器中执行 + 1 操作;
  • 写:把计算结果写回内存。

这 3 步操作中间可以被线程切换打断,多线程并发执行时会出现累加丢失:

  • 线程 1 读到 counter=100,刚要执行 + 1,被切换到线程 2;
  • 线程 2 也读到 counter=100,执行 + 1 后写回内存,counter=101;
  • 线程 1 恢复执行,把寄存器中的 101 写回内存,覆盖了线程 2 的结果。

两次累加,最终只加了 1,这就是数据竞争(Data Race):多个线程同时访问同一个内存位置,且至少有一个是写操作,导致未定义行为。

针对数据竞争问题,有两种主流解决方案:加锁或原子操作

1.1 用互斥锁保护临界区

锁的核心逻辑是:同一时间只有一个线程能进入临界区,保证操作的独占性。

cpp 复制代码
#include <mutex>
#include <thread>
#include <iostream>

int counter = 0;
std::mutex mtx;

void add_10w_times() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        counter++;
    } // 自动解锁
}

// main函数不变,最终结果一定是200000

但锁的开销极大,加锁 / 解锁会触发内核态上下文切换,高并发下锁竞争会导致性能急剧下降,甚至出现死锁。

1.2 用原子操作实现无锁安全

原子操作把 "读 - 改 - 写" 3 步指令合并成一个不可分割的 CPU 操作,中间不会被线程切换打断,无需加锁就能保证线程安全,且全程在用户态执行,无内核切换开销,性能远高于锁。

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

// 原子计数器,所有操作都是原子的
std::atomic<int> counter = 0;

void add_10w_times() {
    for (int i = 0; i < 100000; ++i) {
        counter++; // 原子自增,无数据竞争
    }
}

// main函数不变,最终结果一定是200000

二、原子操作

原子操作的核心是不可分割性:一个操作要么完全执行完成,要么完全不执行,不存在中间执行状态,中间状态对其他线程完全不可见。

保证 核心含义 解决的问题
原子性 操作不可分割,要么全成,要么全败,中间状态不可见 多线程同时读写的操作撕裂、数据竞争
可见性 一个线程对原子变量的修改,能保证在指定规则内被其他线程看到 编译器 / CPU 缓存导致的多线程间数据不一致
有序性 通过内存序控制编译器和 CPU 的指令重排,保证操作执行顺序符合预期 指令重排导致的多线程逻辑异常

原子操作与互斥锁的核心区别

特性 原子操作 互斥锁(std::mutex)
执行模式 绝大多数平台无锁实现,用户态完成,无线程阻塞 内核态同步,线程阻塞会触发上下文切换,开销大
保护范围 仅保护单个变量的单次操作 可保护任意复杂的临界区代码块、多步操作
死锁风险 无死锁风险 多锁场景下有死锁风险
性能表现 低延迟,高并发下性能稳定,无竞争退化 高并发下锁竞争激烈,性能急剧下降
调试难度 内存序、ABA 问题导致的 bug 极难调试 临界区逻辑清晰,调试相对简单

选型原则:

  • 简单的变量读写、累加、状态切换,优先用原子操作;
  • 复杂的多步操作、资源访问控制(如修改多个关联变量、操作复杂结构体),优先用互斥锁。

三、C++ 原子库基础:std::atomic

C++ 原子操作的核心载体是模板类 std::atomic,它封装了一个类型为 T 的变量,提供一整套原子操作接口。

cpp 复制代码
#include <atomic>

// 1. 基础初始化:用值初始化原子变量
std::atomic<int> a(10);
std::atomic<bool> flag(false);
std::atomic<int*> ptr(nullptr);

// 2. 列表初始化(C++11起支持)
std::atomic<long> b{100};

// 3. 注意:std::atomic 不可拷贝、不可移动
// std::atomic<int> c = a; // C++11/14 编译错误(拷贝构造被delete)
// C++17起支持拷贝初始化,但仍不支持拷贝赋值

std::atomic 的对象本身是不可拷贝、不可赋值 的,要读取 / 修改它的值,必须通过专属的原子操作接口;

全局静态的 std::atomic 变量会被静态初始化,不会出现初始化顺序问题;

必须保证原子变量的内存对齐(编译器自动处理),不对齐的原子操作会失去原子性,甚至崩溃。

std::atomic 对不同类型有不同的特化与操作支持,分为 4 大类:

  • 基础整型:bool、char、int、long、long long 等,全特化,支持最完整的操作(算术、位运算、CAS 等);
  • 指针类型:std::atomic<T*> 专属特化,支持指针算术操作(地址偏移);
  • 平凡可拷贝类型(Trivially Copyable):POD 结构体、枚举等,只要满足平凡拷贝、无虚函数、无自定义析构,就可以用 std::atomic 封装,但仅支持基础的读写、交换、CAS 操作,无算术 / 位运算;
  • C++20 扩展类型:浮点数(float/double)、智能指针(std::shared_ptr/std::weak_ptr)。

读写(load/store)

所有 std::atomic 类型都支持这两个最核心的操作,用于原子地读取和写入变量值。

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<bool> ready_flag(false);
std::atomic<int> shared_data(0);

void writer_thread() {
    // 先写数据
    shared_data.store(42, std::memory_order_relaxed);
    // 再设置就绪标志
    ready_flag.store(true, std::memory_order_release);
}

void reader_thread() {
    // 等待就绪标志变为true
    while (!ready_flag.load(std::memory_order_acquire)) {
        // 自旋等待
    }
    // 读到ready_flag=true,一定能看到shared_data=42
    std::cout << "共享数据:" << shared_data.load(std::memory_order_relaxed) << std::endl;
}

int main() {
    std::thread t1(writer_thread);
    std::thread t2(reader_thread);
    t1.join();
    t2.join();
    return 0;
}

原子交换操作:exchange

exchange 是原子的替换并返回旧值操作,所有 std::atomic 类型都支持,原子地将原子变量的值设置为 desired,同时返回设置之前的旧值,整个过程不可分割。

cpp 复制代码
T exchange(T desired, std::memory_order order = seq_cst) noexcept;
cpp 复制代码
#include <atomic>
#include <iostream>

std::atomic<int> task_state(0); // 0:空闲 1:运行中 2:已完成

int main() {
    // 原子地将状态从0改为1,返回旧状态
    int old_state = task_state.exchange(1);
    if (old_state == 0) {
        std::cout << "成功抢占任务,开始执行" << std::endl;
    } else {
        std::cout << "任务已被其他线程抢占,当前状态:" << old_state << std::endl;
    }
    return 0;
}

四、CAS 操作(比较并交换)

CAS(Compare And Swap,比较并交换)是无锁并发编程的基石,几乎所有无锁数据结构(无锁栈、无锁队列)都依赖 CAS 实现。std::atomic 提供了两个版本的 CAS:强版本 compare_exchange_strong 和弱版本 compare_exchange_weak。

CAS 操作有两个核心参数:expected(期望值)、desired(目标值),执行逻辑如下:

  • 原子地比较当前原子变量的值和 expected 的值是否完全相等;
  • 相等:将原子变量的值设置为 desired,返回 true(操作成功);
  • 不相等:将 expected 的值更新为原子变量当前的实际值,返回 false(操作失败)。

整个过程是完全原子的,不会出现比较完还没赋值,就被其他线程修改的情况。

版本 核心特性 适用场景
compare_exchange_strong 无伪失败,只要值相等就一定成功,性能略低 单次判断、无需循环重试的场景
compare_exchange_weak 允许伪失败(值相等时也可能返回 false),性能更高 循环重试的无锁场景(无锁栈 / 队列)

那么问题来了,什么是伪失败?

在 ARM 等使用LL/SC(加载链接 / 存储条件) 架构的 CPU 上,CAS 通过 LDREX/STREX 指令对实现。即使内存值和期望值相等,LL/SC 也可能因为中断、其他线程的内存访问等原因失败,这就是伪失败:

ARM 把原子操作拆成两条独立指令,硬件只做一件事:给地址打一个「独占标记」,但是独占标记极其脆弱,只要发生任何干扰,硬件会直接清除独占标记,不关心内存值有没有变:

  • LDREX:读取内存值 → 硬件给该内存地址打上「本核独占标记」

    • 标记存在 CPU 内部的监控单元(Exclusive Monitor) 里
    • 仅记录:「我正在独占这个地址」
  • STREX:尝试写入 → 硬件检查独占标记是否还在

    • 标记在 → 写入成功,清除标记
    • 标记不在 → 写入失败(伪失败就发生在这里)

x86 把 CAS 做成单条不可分割的硬件原子指令,硬件直接锁总线 / 缓存,全程不中断、不放弃。

执行流程(硬件原生实现,软件不可拆分)

用 CAS 实现原子累加

cpp 复制代码
#include <atomic>
#include <iostream>

int main() {
    std::atomic<int> a(10);

    int expected = a.load()+1;
    // 循环CAS,直到累加成功(处理伪失败和并发修改)
    while (!a.compare_exchange_weak(expected, expected + 1)) {
        // 失败时,expected已经被自动更新为a的当前值,直接重试即可
    }

    std::cout << "累加后的值:" << a.load() << std::endl; // 11
    return 0;
}

类型专属操作:整型算术、指针偏移

std::atomic 针对整型和指针类型做了专属特化,提供了原子的算术、位运算操作,无需手动写 CAS 循环。

针对所有整型 std::atomic,支持以下原子操作,所有操作都返回修改前的旧值:

操作函数 核心作用 重载运算符
fetch_add(T n, memory_order order = seq_cst) 原子加 n,返回旧值 +=、++(前置 / 后置)
fetch_sub(T n, memory_order order = seq_cst) 原子减 n,返回旧值 -=、--(前置 / 后置)
fetch_and(T n, memory_order order = seq_cst) 原子按位与 n,返回旧值 &=
fetch_or(T n, memory_order order = seq_cst) 原子按位或 n,返回旧值 =
fetch_xor(T n, memory_order order = seq_cst) 原子按位异或 n,返回旧值 ^=
cpp 复制代码
#include <atomic>
#include <iostream>

int main() {
    std::atomic<int> a(10);

    // 原子加5,返回旧值10
    int old_val = a.fetch_add(5);
    std::cout << "旧值:" << old_val << ",新值:" << a.load() << std::endl; // 10, 15

    // 原子自增,返回新值16
    int new_val = ++a;
    std::cout << "自增后:" << new_val << std::endl; // 16

    // 原子按位与0b1111,返回旧值16
    old_val = a.fetch_and(0b1111);
    std::cout << "按位与后:" << a.load() << std::endl; // 0
    return 0;
}

原子指针专属操作

std::atomic<T*> 支持原子的地址偏移操作,偏移量会自动按 sizeof(T) 计算,和普通指针的算术规则一致。

支持的操作:

  • fetch_add(ptrdiff_t n, memory_order):原子地将指针向前偏移 n * sizeof(T) 字节,返回旧地址;
  • fetch_sub(ptrdiff_t n, memory_order):原子地将指针向后偏移 n * sizeof(T) 字节,返回旧地址;
  • 重载运算符:++(前置 / 后置)、--(前置 / 后置)、+=、-=。
cpp 复制代码
#include <atomic>
#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    std::atomic<int*> atomic_arr(arr); // 指向数组首地址

    // 原子+1,偏移sizeof(int)=4字节,指向arr[1]
    atomic_arr++;
    std::cout << *atomic_arr.load() << std::endl; // 2

    // 原子+2,偏移2*4=8字节,指向arr[3]
    atomic_arr += 2;
    std::cout << *atomic_arr.load() << std::endl; // 4

    // 原子-1,返回偏移前的旧地址
    int* old_ptr = atomic_arr.fetch_sub(1);
    std::cout << "旧地址值:" << *old_ptr << ",新地址值:" << *atomic_arr.load() << std::endl; // 4, 3
    return 0;
}

五、内存序(Memory Order)

内存序是原子操作最核心、最难理解的部分,它控制着编译器和 CPU 的指令重排,以及多线程间的内存可见性。如果内存序使用错误,会出现极难调试的并发 bug。

编译器和 CPU 为了提升性能,会在不改变单线程执行结果的前提下,调整指令的执行顺序,这就是指令重排。

单线程下,重排完全无害,但多线程下,重排会导致其他线程看到的操作顺序和代码书写顺序完全不同,出现诡异的 bug。

经典重排问题示例:

cpp 复制代码
int data = 0;
bool flag = false;

// 线程1
void writer() {
    data = 42;   // 写数据
    flag = true; // 写标志
}

// 线程2
void reader() {
    if (flag) {
        // 可能输出0!
        std::cout << data << std::endl;
    }
}

问题根源:编译器或 CPU 可能把 flag = true 重排到 data = 42 之前,线程 2 看到 flag=true 时,data 还没被赋值,最终输出 0。

而原子操作的内存序,就是用来禁止不合理的重排,保证多线程间的操作顺序和可见性。

C++ 定义了 6 种内存序枚举值,分为 3 大类,从最严格到最宽松:

分类 枚举值 核心作用
全序一致性 std::memory_order_seq_cst 最严格,全局唯一全序,默认值,最安全
获取 - 释放语义 std::memory_order_acquire 读操作专用,获取语义,禁止后续操作重排到读之前
获取 - 释放语义 std::memory_order_release 写操作专用,释放语义,禁止前序操作重排到写之后
获取 - 释放语义 std::memory_order_acq_rel 读 - 改 - 写操作专用,同时具备 acquire 和 release 语义
获取 - 释放语义 std::memory_order_consume 指针依赖的获取语义,不推荐使用,编译器普遍按 acquire 处理
宽松语义 std::memory_order_relaxed 最宽松,仅保证原子性,无重排和可见性保证

(1)std::memory_order_seq_cst

所有原子操作的默认内存序,也是最严格、最安全的内存序,其保证所有线程看到的所有 seq_cst 原子操作,都有一个全局唯一的执行顺序,和代码书写顺序完全一致,本线程中,seq_cst 操作之前的所有读写,都不会被重排到该操作之后;之后的所有读写,都不会被重排到该操作之前,一个线程的 seq_cst 写,对所有其他线程的 seq_cst 读立即可见。

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> data(0);
std::atomic<bool> flag(false);

void writer() {
    data.store(42, std::memory_order_seq_cst);
    flag.store(true, std::memory_order_seq_cst);
}

void reader() {
    while (!flag.load(std::memory_order_seq_cst)) {}
    // 一定输出42,不会出现重排问题
    std::cout << data.load(std::memory_order_seq_cst) << std::endl;
}

int main() {
    std::thread t1(writer);
    std::thread t2(reader);
    t1.join();
    t2.join();
    return 0;
}

(2)获取 - 释放语义
acquire/release 是工业界最常用的内存序,性能远高于 seq_cst,同时能满足绝大多数同步场景的需求。其核心在于线程 A 对原子变量 X 执行 release 写,线程 B 对原子变量 X 执行 acquire 读,且读到了线程 A 写入的值;

那么:线程 A 中,release 写之前的所有读写操作,对线程 B 中 acquire 读之后的所有读写操作,都是完全可见的。

  • acquire(读操作):本线程中,acquire 读之后的所有读写操作,绝对不会被重排到该读之前;
  • release(写操作):本线程中,release 写之前的所有读写操作,绝对不会被重排到该写之后;
  • acq_rel(读 - 改 - 写操作):同时具备 acquire 和 release 语义,读的部分是 acquire,写的部分是 release。
cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> data(0);
std::atomic<bool> flag(false);

void writer() {
    data.store(42, std::memory_order_relaxed); // 写在release之前,会被保证可见
    flag.store(true, std::memory_order_release); // release写,作为同步点
}

void reader() {
    // acquire读,作为同步点,读到true就会看到data=42
    while (!flag.load(std::memory_order_acquire)) {}
    std::cout << data.load(std::memory_order_relaxed) << std::endl; // 一定输出42
}

int main() {
    std::thread t1(writer);
    std::thread t2(reader);
    t1.join();
    t2.join();
    return 0;
}

典型应用场景:双检锁单例模式、线程池任务队列、无锁数据结构的节点同步。

(3)宽松语义:relaxed

std::memory_order_relaxed 是最宽松的内存序,仅保证操作的原子性,不提供任何重排控制和可见性保证,性能开销最小。

适用场景:仅需要保证操作是原子的,不需要多线程间的同步,比如原子计数器、统计量累加(只需要最终结果正确,不需要和其他操作同步)。

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> request_count(0);

void handle_request() {
    // 仅需要原子累加,无需同步,用relaxed性能最高
    request_count.fetch_add(1, std::memory_order_relaxed);
}

int main() {
    std::thread threads[10];
    for (auto& t : threads) {
        t = std::thread([](){
            for (int i = 0; i < 10000; ++i) handle_request();
        });
    }
    for (auto& t : threads) t.join();

    // 最终结果一定是10*10000=100000
    std::cout << "总请求数:" << request_count.load() << std::endl;
    return 0;
}

特殊原子类型与 C++ 标准扩展

std::atomic_flag

std::atomic_flag 是 C++ 标准中唯一保证在所有平台都是无锁的原子类型,它只有两种状态:设置(true)和清除(false),是实现自旋锁、轻量级锁的核心。

操作函数 核心作用
bool test_and_set(memory_order order = seq_cst) 原子地将 flag 设置为 true,返回设置之前的旧状态
void clear(memory_order order = seq_cst) 原子地将 flag 设置为 false

值得注意的是,std::atomic_flag 必须用 ATOMIC_FLAG_INIT 宏初始化,初始状态为清除(false)。

自选锁实现示例

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

class SpinLock {
private:
    // 必须用宏初始化,初始为false
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        // 循环test_and_set,直到拿到锁(返回false表示之前未被设置)
        while (flag.test_and_set(std::memory_order_acquire)) {
            // x86平台可加pause指令,降低CPU占用
            // _mm_pause();
        }
    }

    void unlock() {
        // 释放锁,用release语义保证临界区操作可见
        flag.clear(std::memory_order_release);
    }
};

// 测试
SpinLock lock;
int counter = 0;

void add() {
    for (int i = 0; i < 100000; ++i) {
        lock.lock();
        counter++;
        lock.unlock();
    }
}

int main() {
    std::thread t1(add);
    std::thread t2(add);
    t1.join();
    t2.join();
    std::cout << "最终结果:" << counter << std::endl; // 200000
    return 0;
}

C++20 原子特性重大扩展

std::atomic_ref

std::atomic_ref 是对普通变量的原子封装,可以给一个已有的非原子变量提供原子操作能力,相当于给普通变量套了一个原子的 "壳",适用于需要偶尔对普通变量做原子操作的场景。

所有对该变量的并发访问,都必须通过 std::atomic_ref,否则会触发数据竞争。

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

// 普通全局变量
int counter = 0;

void add() {
    for (int i = 0; i < 100000; ++i) {
        // 给普通变量创建原子引用,执行原子自增
        std::atomic_ref<int> ref(counter);
        ref++;
    }
}

int main() {
    std::thread t1(add);
    std::thread t2(add);
    t1.join();
    t2.join();
    std::cout << "最终结果:" << counter << std::endl; // 200000
    return 0;
}

std::atomic<std::shared_ptr>

原子智能指针,C++20 之前,std::shared_ptr 的引用计数是原子的,但 shared_ptr 对象本身的读写不是原子的,多线程同时读写同一个 shared_ptr 会触发数据竞争。

C++20 起,std::atomic 支持对 std::shared_ptr/std::weak_ptr 的封装,保证智能指针本身的读写、替换操作是原子的。

原子智能指针的操作不一定是无锁的,取决于平台和编译器实现。

原子浮点数:std::atomic<float/double>

C++20 起支持对浮点数的原子封装,提供 fetch_add、fetch_sub 等原子算术操作,解决了多线程浮点累加的线程安全问题。

C++20 为所有 std::atomic 类型新增了 wait、notify_one、notify_all 方法,实现线程间的阻塞等待唤醒,替代之前的轮询自旋,大幅降低 CPU 占用。

  • wait(T old_val):阻塞当前线程,直到原子变量的值不等于 old_val,被唤醒后会重新检查;
  • notify_one():唤醒一个正在等待该原子变量的线程;
  • notify_all():唤醒所有正在等待该原子变量的线程。
cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<bool> ready(false);

void worker_thread() {
    std::cout << "工作线程等待就绪信号..." << std::endl;
    // 阻塞等待,直到ready的值不等于false
    ready.wait(false);
    std::cout << "收到就绪信号,开始执行任务" << std::endl;
}

int main() {
    std::thread t(worker_thread);

    // 模拟准备工作
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << "主线程发送就绪信号" << std::endl;
    ready.store(true);
    // 唤醒等待的线程
    ready.notify_one();

    t.join();
    return 0;
}

底层原理与平台区别

无锁判断:is_lock_free ()

std::atomic 提供了两个接口判断是否为无锁实现:

复制代码
bool is_lock_free() const noexcept:运行时判断当前对象的原子操作是否无锁;
static constexpr bool is_always_lock_free:C++17 起,编译期判断该类型的原子操作是否永远无锁。

无锁的核心条件:类型 T 的大小必须是 CPU 原生支持的原子操作宽度(32 位平台 4 字节,64 位平台 8 字节,部分平台支持 16 字节双字 CAS),且内存对齐。

x86 平台的原子实现

x86 架构是强内存模型(TSO,Total Store Order),天生保证了大部分有序性,原子操作的实现非常直接:

  • 对齐的基础类型 load/store:本身就是原子的,不需要额外指令,x86 保证对齐的字 / 双字 / 四字读写是原子的;
  • 读 - 改 - 写操作(add、inc、CAS):通过 lock 前缀实现,lock 前缀会锁住 CPU 缓存行(或总线),保证操作的原子性,同时相当于一个全内存屏障,保证顺序和可见性;
  • seq_cst 内存序:通过 lock 前缀或 mfence 内存屏障实现,保证全局全序。

ARM 平台的原子实现

ARM 架构是弱内存模型,天生不保证指令有序性,原子操作通过 LL/SC(Load-Linked/Store-Conditional) 指令对实现:

  • LDREX R0, [R1]:加载 R1 地址的值到 R0,同时标记该缓存行为独占访问;
  • 执行修改操作;
  • STREX R2, R0, [R1]:如果该缓存行的独占标记还在,就把 R0 的值写入 R1 地址,R2 设为 0(成功);否则 R2 设为 1(失败),不写入。

这种模式下,CAS 的伪失败就是因为 LL 和 SC 之间,其他线程访问了该地址,导致独占标记失效,STREX 失败。内存序通过 DMB/DSB/ISB 内存屏障指令实现。

相关推荐
Aurorar0rua2 小时前
CS50 x 2024 Notes C - 08
c语言·开发语言·学习方法
froginwe112 小时前
SQL GROUP BY 详解
开发语言
A charmer2 小时前
第一章:基础语法破冰|从 C++ 无缝切换 OC 语法
c++·objective-c
wangl_922 小时前
C#性能优化完全指南 - 从原理到实践
开发语言·性能优化·c#·.net·.netcore·visual studio
xrgs_shz2 小时前
基于轻量化浅层卷积神经网络的手写数字识别
算法·matlab·cnn
xyq20242 小时前
Redis 哈希(Hash)
开发语言
fffzd2 小时前
C++入门(一)
开发语言·c++·命名空间·输入输出·缺省参数
小妖同学学AI3 小时前
架构图即代码:GitHub星标41.9k的Diagrams,用Python解放你的画图生产力
开发语言·python·github
计算机安禾3 小时前
【计算机网络】第10篇:距离矢量路由算法——Bellman-Ford方程与RIP协议的特性分析
计算机网络·算法