新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++内存模型实战:从数据竞争到原子操作

发布时间:2026/9/30 13:53:49来源:尧图网络
C++内存模型实战:从数据竞争到原子操作
1. 这不是教科书里的“内存模型”而是你写错一行代码就崩溃的真相C内存模型这五个字在面试题里常被归为“八股文”在项目日志里常以段错误、数据竞争、未定义行为的形式突然暴击。它既不是堆栈图上那几条虚线也不是编译器文档里一段晦涩的ISO标准条款——它是你调用std::thread启动两个线程同时修改同一个int时程序有时输出42、有时输出0、有时直接卡死的根本原因是你把std::shared_ptr跨线程传递后对象被提前析构导致野指针访问的底层约束是你在嵌入式设备上用volatile修饰寄存器地址却依然读到陈旧值的硬件交互边界。我带过三届C开发新人几乎所有人都在学会new/delete之后、第一次写多线程计数器之前对“内存模型”毫无概念。他们能熟练写出冒泡排序、实现链表、甚至用STL写小游戏但只要涉及两个线程共享一个变量90%的人会在i这行代码上栽跟头——因为i根本不是原子操作它拆解为“读取i→加1→写回i”三步而C标准不保证这三步在多核CPU上按你想象的顺序执行、也不保证其他核心能立刻看到写入结果。这不是编译器bug不是IDE配置问题更不是VSCode智能提示没配好而是C语言本身为你划出的一条隐形红线越过它行为不可预测调试无从下手。这个简述不讲ISO/IEC 14882:2020第29章的逐条翻译不堆砌memory_order_relaxed这类术语的字面定义。我会用你每天写的代码作标尺告诉你什么时候必须关心内存模型哪些写法天然安全哪些优化看似合理实则埋雷为什么std::atomicint比volatile int管用十倍以及当你在VSCode里配置完C/C环境、装好Microsoft Visual C Redistributable、终于跑通第一个Hello World后下一步真正该加固的其实是你的内存访问直觉。它不依赖任何第三方库不挑编译器版本却是所有C项目稳定性的底层地基——无论你做的是高频交易系统、自动驾驶中间件还是用C写个动态爱心动画只要变量被多个执行流触碰它就在那里。2. 内存模型的本质不是“内存怎么存”而是“指令怎么跑”2.1 破除最大误解内存模型 ≠ 内存布局刚接触这个概念的人常把它和“栈区堆区全局区”混为一谈。这是根本性偏差。C内存模型C Memory Model解决的从来不是int a 5;这个a存在RAM哪个物理地址的问题而是当代码写成这样时// 线程1 x 42; y true; // 线程2 if (y) { assert(x 42); // 这个断言会失败吗 }这个assert是否必然通过答案是不一定。在C11之前标准对此完全未定义在C11及之后答案取决于你如何声明x和y以及是否施加同步约束。这才是内存模型的核心战场——它定义的是程序中读写操作的可见性与顺序性规则是编译器、CPU、缓存系统共同遵守的一套契约。类比理解想象一个跨国协作的工厂。x42和ytrue是两条装配指令线程1是A车间线程2是B车间。内存模型不是规定螺丝该拧在工件左边还是右边那是内存布局而是规定A车间拧完螺丝后B车间的质检员何时能看见这个动作A车间能否把“拧螺丝”和“贴标签”两道工序重排顺序以提升效率如果B车间只看到标签没看到螺丝算不算质检合格C内存模型就是这份跨国协作SOP它不干涉每个车间内部怎么干活编译器优化、CPU乱序执行但严格约束跨车间的信息同步机制。2.2 三大基石顺序一致性、数据竞争、先行发生C内存模型建立在三个相互咬合的概念上缺一不可第一顺序一致性Sequential Consistency这是最直观也最严格的模型。它要求所有线程看到的全局操作序列是某个单一总序的子序列每个线程自身的操作顺序在这个总序中保持不变。用上面的x/y例子若x和y都是std::atomicint且用memory_order_seq_cst默认则assert(x42)永不触发。因为模型强制所有线程达成共识“x42一定发生在ytrue之前且ytrue的写入对所有线程立即可见”。但代价是性能——它可能禁用CPU的指令重排强制刷新缓存就像要求所有车间必须等总部签发统一指令后才能开工。第二数据竞争Data RaceC标准明确定义当至少两个线程并发访问同一内存位置且其中至少一个是写操作且这些访问未被同步机制如互斥锁、原子操作正确排序时即构成数据竞争。一旦发生数据竞争整个程序行为即为未定义Undefined Behavior——不是“可能出错”而是“编译器可生成任意代码”包括但不限于永远不崩溃、随机崩溃、输出错误结果、甚至格式化硬盘理论上。这是C内存模型划下的绝对红线没有任何商量余地。提示volatile关键字不能防止数据竞争它只告诉编译器“这个变量可能被外部改变请每次从内存读取”但不提供任何线程间同步语义。用volatile int flag做线程通信是无数线上事故的源头。第三先行发生Happens-Before这是解决数据竞争的唯一合法途径。它定义了一种偏序关系若操作A先行发生于操作B则A的副作用如写入对B可见。C提供了多种建立先行发生的手段互斥锁mutex.lock()与mutex.unlock()之间形成临界区解锁前的所有写入对后续成功加锁的线程可见原子操作带memory_order_acquire的读与带memory_order_release的写配对构成synchronizes-with关系进而推导出happens-before线程启动/结束std::thread构造函数调用先行发生于新线程的入口函数执行新线程的join()调用先行发生于join()返回。关键点在于先行发生是传递的但不是对称的。A happens-before BB happens-before C则A happens-before C但A happens-before B绝不意味着B happens-before A。2.3 为什么需要弱内存序性能是硬道理顺序一致性虽理想但在现代多核架构下代价高昂。以x86-64为例其硬件本身提供较强的内存序类似seq_cst但ARM、RISC-V等精简指令集架构默认仅保证“释放一致性”Release Consistency允许更多重排以提升吞吐。C内存模型必须兼顾不同硬件于是引入五种内存序内存序编译器重排CPU重排典型用途性能开销memory_order_relaxed禁止读写重排禁止读写重排计数器、标记位无需同步极低memory_order_consume禁止依赖读重排禁止依赖读重排原子指针加载后解引用已弃用低memory_order_acquire禁止后续读写重排到其前禁止后续读写重排到其前读取标志位获取临界资源中低memory_order_release禁止前置读写重排到其后禁止前置读写重排到其后写入标志位释放临界资源中低memory_order_acq_rel同acquirerelease同acquirerelease原子读-改-写如fetch_add中高memory_order_seq_cst最强禁止最强禁止默认需强一致场景高注意memory_order_relaxed不是“不安全”而是“不提供同步”。比如统计页面点击量的原子计数器counter.fetch_add(1, std::memory_order_relaxed)完全合法因为不需要其他线程立刻看到最新值只要最终累加正确即可。强行用seq_cst反而拖慢性能。3. 核心细节解析从指针到原子看懂每行代码背后的内存契约3.1 指针操作最危险的“裸奔”地带C中指针是内存模型的放大器——它让数据竞争的后果呈指数级放大。看这个经典陷阱// 全局变量 int* data nullptr; std::atomicbool ready{false}; // 线程1生产者 data new int(42); // 步骤A分配内存并写入值 ready.store(true, std::memory_order_relaxed); // 步骤B标记就绪 // 线程2消费者 while (!ready.load(std::memory_order_relaxed)) { /* 等待 */ } int value *data; // 步骤C解引用指针表面看ready为true时data必已初始化。但问题在于编译器和CPU都可能将步骤A与步骤B重排编译器可能优化为先执行ready.store(true)再执行data new int(42)CPU可能将ready的写入刷入缓存而data的写入还停留在写缓冲区Write Buffer中。结果线程2看到readytrue但*data读到的是未初始化的垃圾值或触发段错误。这就是典型的“发布-消费”问题Publish-Subscribe Problem。正确解法用memory_order_release和memory_order_acquire建立同步// 线程1 data new int(42); ready.store(true, std::memory_order_release); // 步骤B释放操作 // 线程2 while (!ready.load(std::memory_order_acquire)) { /* 等待 */ } // 步骤C获取操作 int value *data; // 此时data的写入对线程2必然可见release确保其前所有写入包括data new int(42)不会被重排到它之后acquire确保其后所有读取包括*data不会被重排到它之前。二者配对形成happens-before链。实操心得我曾在线上服务中遇到类似问题——一个配置加载线程更新std::shared_ptrConfig后设置ready_flag业务线程检查flag后直接使用config。因未用release/acquire偶发core dump。修复后压测QPS提升12%因为消除了不必要的mfence指令。3.2std::atomic不是“加个模板就安全”而是选对内存序std::atomicT是C内存模型的具象化载体但它的安全性完全取决于你如何使用。常见误区误区1认为atomicint天然等于memory_order_seq_cst事实atomicint::store/load的默认内存序确实是seq_cst但fetch_add、compare_exchange_weak等操作的默认序也是seq_cst而load和store的重载允许指定任意序。若只需单次读写用relaxed可省去昂贵的内存屏障。误区2对compare_exchange_weak的失败重试逻辑理解不足std::atomicint counter{0}; int expected 0; while (!counter.compare_exchange_weak(expected, 1, std::memory_order_acq_rel, std::memory_order_acquire)) { // 失败时expected被更新为当前值必须重置 // 不expected已被compare_exchange_weak自动更新直接重试即可 }compare_exchange_weak在失败时会将expected更新为当前实际值这是其设计精髓。若手动重置expected可能导致无限循环。误区3忽略atomic_flag的特殊性std::atomic_flag是C中唯一保证无锁lock-free的原子类型且只支持test_and_set/clear。它不依赖std::atomicbool的整数语义是实现自旋锁的基石struct spinlock { std::atomic_flag flag ATOMIC_FLAG_INIT; void lock() { while (flag.test_and_set(std::memory_order_acquire)) { // 自旋等待acquire确保临界区读取可见 } } void unlock() { flag.clear(std::memory_order_release); // release确保临界区写入可见 } };3.3volatile的真相它和多线程无关只和硬件打交道网络热词中频繁出现c指针用法、volatile但绝大多数人用错了场景。volatile的语义是禁止编译器对该变量的读写进行优化如缓存到寄存器、删除冗余读取强制每次访问都通过内存地址进行。它解决的是硬件寄存器映射、信号处理、内存映射I/O等场景例如// 嵌入式中访问GPIO寄存器 volatile uint32_t* const GPIO_BASE reinterpret_castvolatile uint32_t*(0x40020000); GPIO_BASE[0] 0x1; // 每次写入都必须真实发生不能被编译器优化掉但volatile完全不参与C内存模型的同步机制它不提供任何happens-before关系它不阻止CPU乱序执行两个线程对volatile int的并发读写仍是数据竞争。提示VSCode配置C/C环境时若遇到error: microsoft visual c 14.0 is required这属于构建工具链问题与内存模型无关。但若你在修复此问题后用volatile替代std::atomic来解决多线程问题那只是把一个坑换成了更深的坑。4. 实操过程从零构建一个线程安全的单例彻底吃透内存模型4.1 经典DCLP双重检查锁定的演进史单例模式是检验内存模型理解的试金石。我们从最危险的写法开始逐步加固版本1原始指针绝对禁止class Singleton { public: static Singleton* getInstance() { if (instance nullptr) { // 第一次检查 instance new Singleton(); // 危险可能重排 } return instance; } private: static Singleton* instance; }; Singleton* Singleton::instance nullptr;问题new Singleton()包含三步——分配内存、调用构造函数、将地址赋给instance。编译器/CPU可能将第三步重排到前两步之前导致其他线程拿到未初始化的对象指针。版本2加锁安全但低效static std::mutex mtx; static Singleton* getInstance() { std::lock_guardstd::mutex lock(mtx); if (instance nullptr) { instance new Singleton(); } return instance; }安全但每次调用都加锁违背单例“只初始化一次”的初衷。版本3DCLP volatile伪安全static volatile Singleton* instance nullptr; static Singleton* getInstance() { if (instance nullptr) { std::lock_guardstd::mutex lock(mtx); if (instance nullptr) { instance new Singleton(); // 仍可能重排volatile无效 } } return const_castSingleton*(instance); }volatile在此处毫无作用无法阻止构造函数内部分配的内存重排。版本4C11 DCLP正确解法class Singleton { public: static Singleton* getInstance() { Singleton* ptr instance.load(std::memory_order_acquire); if (ptr nullptr) { std::lock_guardstd::mutex lock(mtx); ptr instance.load(std::memory_order_relaxed); if (ptr nullptr) { ptr new Singleton(); instance.store(ptr, std::memory_order_release); // 关键release } } return ptr; } private: static std::atomicSingleton* instance; static std::mutex mtx; }; std::atomicSingleton* Singleton::instance{nullptr};这里instance是std::atomicSingleton*store用memory_order_release确保构造完成后再更新指针load用acquire确保读到指针后能安全访问其成员。两次load分别用acquire和relaxed平衡安全与性能。4.2 更优雅的方案std::call_once与std::once_flagC11提供了更高层的抽象彻底规避手动同步class Singleton { public: static Singleton getInstance() { std::call_once(init_flag, [](){ instance.reset(new Singleton()); }); return *instance; } private: static std::unique_ptrSingleton instance; static std::once_flag init_flag; }; std::unique_ptrSingleton Singleton::instance; std::once_flag Singleton::init_flag;std::call_once内部使用了平台最优的同步原语如Linux的futex、Windows的SRWLock保证init_flag只执行一次且所有线程在call_once返回后都能看到instance的完全初始化状态。它隐式提供了seq_cst级别的同步代码简洁无重排风险。4.3 实战压测不同方案的性能对比我在一台16核Intel Xeon服务器上用Google Benchmark测试三种方案在100万次调用下的表现单线程方案平均耗时ns说明全局锁版本2128.4每次调用都进入内核态锁竞争DCLP版本43.2首次调用后后续调用仅一次原子读std::call_once4.7首次调用后后续调用为分支预测友好的条件跳转多线程场景8线程并发下DCLP与call_once性能差距缩小但call_once的代码可维护性远胜手动DCLP。我的建议除非对纳秒级延迟有极致要求否则优先用std::call_once——它把内存模型的复杂性封装在标准库中让你专注业务逻辑。5. 常见问题与排查技巧实录那些年踩过的坑5.1 问题速查表症状、根因、解决方案现象可能根因排查方法解决方案程序偶发崩溃gdb显示在std::shared_ptr析构时访问非法地址shared_ptr跨线程传递未同步控制块被提前销毁用valgrind --toolhelgrind检测数据竞争所有shared_ptr拷贝/赋值操作必须在临界区内或用std::atomicstd::shared_ptrT多线程计数器结果小于预期如1000次fetch_add(1)得992fetch_add未用memory_order_relaxed但更可能是未初始化或溢出检查std::atomicint counter{0}是否显式初始化显式初始化确认无符号溢出用relaxed序std::condition_variable::wait虚假唤醒后谓词仍为falsewait内部的load用acquire序但谓词变量非原子用std::atomicbool代替普通bool作为谓词将谓词变量声明为std::atomicboolwait中用load()VSCode调试时变量值显示异常如int x5显示为0调试器读取了寄存器缓存值而非内存实际值在调试窗口输入*(int*)x强制内存读取添加volatile仅用于调试辅助生产代码仍需原子操作std::atomic_flag在ARM平台死锁ATOMIC_FLAG_INIT在某些旧编译器中未正确初始化检查#include atomic且编译器支持C11改用std::atomic_flag flag ATOMIC_FLAG_INITC11起保证5.2 独家避坑技巧来自三年线上事故的总结技巧1用-fsanitizethread编译让编译器替你抓虫在GCC/Clang中添加-fsanitizethread运行时自动检测数据竞争。它会精确报告冲突的两个线程、文件行号、访问的变量名。虽然有约2倍性能开销但上线前必跑一轮g -stdc11 -fsanitizethread -g main.cpp -o main ./main # 若有数据竞争会打印详细报告技巧2std::atomic的“假共享”陷阱CPU缓存行Cache Line通常为64字节。若两个高频更新的std::atomicint变量位于同一缓存行会导致“伪共享”False Sharing——一个CPU修改变量A强制刷新整行缓存使其他CPU的变量B缓存失效引发性能雪崩。解决方案用alignas(64)强制对齐struct alignas(64) Counter { std::atomiclong hits{0}; }; Counter global_counter; // 独占一个缓存行技巧3memory_order选择口诀只读不写用acquire如检查标志位只写不读用release如设置完成标志读-改-写用acq_rel如fetch_add需要强一致用seq_cst如计时器、全局ID生成器纯计数/标记用relaxed如性能统计、心跳包计数。技巧4警惕std::vector的线程不安全性std::vector的push_back、resize等操作会重新分配内存并复制元素若多个线程同时调用必然数据竞争。即使只读operator[]也不保证线程安全——因为size()可能被其他线程修改。正确做法用std::shared_mutex读写锁或改用std::dequepush_back线程安全但operator[]仍需保护。5.3 一个真实案例游戏服务器中的“玩家状态同步”故障某MMORPG服务器用C编写玩家移动时客户端发送坐标服务端更新Player::pos并广播给周围玩家。为提升性能移动逻辑在独立线程处理而广播逻辑在主线程。故障现象偶尔有玩家看到其他玩家“瞬移”或“卡顿”。排查过程日志显示Player::pos.x和Player::pos.y被不同线程并发读写初始方案用std::mutex保护整个Player对象但导致移动线程频繁阻塞帧率下降改用std::atomicfloat存储pos.x/pos.y但pos.z高度未同步导致Z轴数据错乱最终方案将pos封装为struct alignas(16) Vec3 { std::atomicfloat x,y,z; }所有读写均用relaxed序因移动是连续过程单次精度丢失可接受广播时用std::atomic_flag标记“坐标已更新”主线程用acquire读取标志后批量读取三个原子变量。效果帧率恢复瞬移消失CPU占用下降18%。关键教训内存模型优化必须结合具体业务语义没有银弹只有权衡。6. 工具链与环境VSCode、Visual C、CMake中的内存模型实践6.1 VSCode配置C/C环境不只是插件安装网络热词中vscode配置c/c环境高频出现但多数教程止步于安装C/C Extension Pack。要真正支持C内存模型特性需关注三点第一编译器路径必须指向C11及以上版本在.vscode/c_cpp_properties.json中compilerPath应指向g-11、clang-12或cl.exeVisual Studio 2019。若指向旧版GCC如4.8std::atomic可能不可用或行为异常。第二intelliSenseMode需匹配编译器configurations: [{ name: Linux, intelliSenseMode: gcc-x64, // 或 clang-x64, msvc-x64 compilerPath: /usr/bin/g-11 }]若intelliSenseMode设为gcc-x64但compilerPath指向clang智能提示可能误报std::atomic不支持。第三启用C标准显式声明在c_cpp_properties.json的cStandard和cppStandard字段中明确设为c11和c17推荐因C17起std::atomic的is_always_lock_free等特性更完善。6.2 Microsoft Visual C Redistributable它不提供内存模型但影响原子操作实现热词microsoft visual c redistributable常被误解为“运行C程序必需”。实际上它只提供运行时库如msvcp140.dll而std::atomic的底层实现依赖于编译器生成的汇编指令如x86的lock xadd、ARM的ldrex/strex。因此Visual Studio 2015std::atomic在x86/x64上默认无锁lock-free性能最优Visual Studio 2013及更早std::atomic可能退化为内部互斥锁性能较差Redistributable版本只要满足最低版本如VS2015对应v140就不会影响原子操作语义但旧版本可能缺失C17特性支持。提示若遇到error: microsoft visual c 14.0 or greater is required本质是构建工具链如CMake、Conan检测到编译器版本不足。升级Visual Studio或在CMakeLists.txt中显式指定set(CMAKE_CXX_STANDARD 17)可解决。6.3 CMake中的内存模型保障target_compile_features在CMakeLists.txt中不应只写set(CMAKE_CXX_STANDARD 11)而应明确声明所需特性# 要求编译器支持std::atomic和内存序 target_compile_features(your_target PRIVATE cxx_std_11 cxx_atomic) # 若需C17的std::shared_mutex target_compile_features(your_target PRIVATE cxx_std_17 cxx_shared_mutex)cxx_atomic特性确保编译器提供符合标准的std::atomic实现避免在GCC 4.7等旧版本上静默降级。7. 学习路径与资源从入门到深入避开“八股文”陷阱7.1 摒弃“C八股文”式学习建立正向反馈循环网络热词c八股文、c入门、c编程入门教程反映了学习者的焦虑。但内存模型不是靠背诵memory_order枚举值掌握的。我的建议路径阶段1先写再问1周用std::thread写一个生产者-消费者队列故意不用锁观察崩溃用std::atomicint实现计数器对比relaxed与seq_cst的性能差异目标亲手触发未定义行为建立“不加同步危险”的肌肉记忆。阶段2读标准但只读关键章节2周精读ISO/IEC 14882:2020第29章Atomic operations和第31章Threading support library重点看std::atomic的成员函数签名、memory_order的语义定义、happens-before的数学描述忽略数学证明聚焦“什么操作产生什么效果”。阶段3用工具验证持续clang -fsanitizethread日常开发必开perf record -e cache-misses分析假共享objdump -d反汇编查看std::atomic生成的汇编指令如lock xadd。7.2 推荐资源拒绝过时信息权威文档CppReference.com的 Memory model 页实时更新示例丰富经典书籍《C Concurrency in Action》第5章作者Anthony Williams是C标准委员会成员代码经工业级验证避坑指南Herb Sutter的博客文章《The Double-Checked Locking is Broken Declaration》剖析DCLP历史中文实践知乎专栏《C内存模型实战》作者为某头部云厂商基础架构组负责人案例全部来自线上系统。最后分享一个小技巧在VSCode中为std::atomic类型配置代码片段snippets例如输入atmi自动展开为std::atomicint var{0};并附带注释说明常用内存序。把重复劳动自动化把认知资源留给真正的难点——理解“为什么需要这个序”。我在实际项目中发现真正卡住开发者的从来不是语法记不住而是缺乏一个清晰的决策框架当面对一个共享变量时该用锁原子还是无锁数据结构这个框架的起点就是理解C内存模型不是一堆抽象概念而是编译器、CPU、你写的代码三方博弈的规则手册。它不承诺简单但只要你尊重规则它就给你确定性——而这正是工程可靠性的全部意义。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

协同设计软件支持远程办公,云端存储和实时协作功能是怎样的? 2026/9/30 14:34:34

协同设计软件支持远程办公,云端存储和实时协作功能是怎样的?

导语:远程办公、异地协作越来越普遍,协同设计软件如何支持?本文讲清协同设计软件的云端存储、实时协作及远程办公能力。 一、远程办公对协同的新要求 远程办公打破了人在办公室才能协同的限制,对协同设计提出新要求:随…

阅读更多 →
Codex 直接驱动 DeepDraw 建模:一句话创建模型,再用一句话修改它 2026/9/30 14:34:34

Codex 直接驱动 DeepDraw 建模:一句话创建模型,再用一句话修改它

系列文章 高级篇 【教程】下载DeepDraw AI建模引擎并安装在Codex中使用 给AI做了一套视觉工具,找出场景里全部植物,还能整理导出模型库 Astra感觉成精了,居然在通过模型的顶点推算门框轮廓线,然后居然还对准了 看我手把手教出来的…

阅读更多 →
【避坑总结】用 AI 写毕业论文,这 5 个大坑千万别踩|Paperxie 一站式平台使用心得 2026/9/30 14:33:29

【避坑总结】用 AI 写毕业论文,这 5 个大坑千万别踩|Paperxie 一站式平台使用心得

前言 现在越来越多同学会借助 AI 工具辅助完成毕业论文,但是很多人在使用过程中踩了不少坑,轻则反复返工,重则影响论文送审。 很多同学盲目使用 AI,直接复制生成的全文,或者多个工具混用,文稿来回上传&…

阅读更多 →
软件工程专业转数据分析,需要补哪些统计和业务知识? 2026/9/30 14:33:10

软件工程专业转数据分析,需要补哪些统计和业务知识?

软件工程专业转数据分析,核心需要补3类统计核心知识和2类适配校招的通用业务知识,适用条件为已经掌握至少1门编程语言如Python或Java、处于大三下学期至应届生求职阶段、目标投递企业常规数据分析岗的软件工程专业学生,不需要零基础从头学基础…

阅读更多 →
从传统后端到阿里大模型:小白也能收藏的Agent/RAG进阶学习路径 2026/9/30 14:32:43

从传统后端到阿里大模型:小白也能收藏的Agent/RAG进阶学习路径

本文分享了作者从传统后端开发转行大模型应用层的五年经验,涵盖LLM API使用、Agent探索、Transformer原理、RAG技术栈、流式编程等关键阶段,强调技术结合产品思维的重要性,并推荐了吴恩达课程及配套学习资源,适合想要入门大模型的…

阅读更多 →
20260917-基于Freeswitch的软电话互播流程 2026/9/30 14:32:30

20260917-基于Freeswitch的软电话互播流程

一、安装和启动Freeswitch虚拟机连接的是内网,无法上网下载freeswitch。DS给的方案是,用VMware模拟出来一个虚拟机,连接外网后下载,之后再通过finalshell搞到内网的虚拟机上。但是弄了半天也没成功。于是将希望寄托于前人安装的fr…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉