go语言的锁
本篇文章主要讲锁,主要会涉及go的sync.Mutex和sync.RWMutex。
一.锁的概念和发展
1.1 锁的概念
所谓的加锁和解锁其实就是指一个数据是否被占用了,通过Mutex内的一个状态来表示。
例如,取 0 表示未加锁,1 表示已加锁;
- 上锁:把 0 改为 1;
- 解锁:把 1 置为 0.
- 上锁时,假若已经是 1,则上锁失败,需要等锁的主人解锁,将状态改为 0,才可以被其他锁锁上。
这就是一个锁的基本骨架,锁主要就是加锁和解锁两个状态。
并且这里要注意一个点,就是这两个操作具有原子性,不可以被拆解。
1.2 由自旋等阻塞的升级过程
一个优先的工具需要具备探测并适应环境,从而采取不同对策因地制宜的能力.
针对 goroutine 加锁时发现锁已被抢占的这种情形,此时摆在面前的策略有如下两种:
- 阻塞/唤醒:将当前 goroutine 阻塞挂起,直到锁被释放后,以回调的方式将阻塞 goroutine 重新唤醒,进行锁争夺;
- 自旋 + CAS:基于自旋结合 CAS 的方式,重复校验锁的状态并尝试获取锁,始终把主动权握在手中.
阻塞和唤醒大家肯定都知道,这里主要说一下自旋和CAS
如果看过gmp的同学对自旋肯定不陌生,所谓的自旋其实就是轮询,什么意思?这里举一个例子:
比如所谓的主动轮询,其实就是指如果加锁失败之后,它会停歇一会,然后再次询问,我可以加锁嘛?如果可以,就取到这个锁,要是不可以,就进行下一次的轮询。
和阻塞/唤醒不同,他是需要等待通知,说这把锁释放了,然后后续的goroutine才可以拿到这把锁。
CAS是什么?
CAS全称为Compare-And-Swap,是一种原子操作,用于多线程编程中实现无锁同步。
上述的方案各有各的优缺点,都有其对应的适用场景,接下来来看一下
锁竞争方案 | 优势 | 劣势 | 适用场景 |
阻塞/唤醒 | 精准打击,不浪费 CPU 时间片 | 需要挂起协程,进行上下文切换,操作较重 | 并发竞争激烈的场景 |
自旋+CAS | 无需阻塞协程,短期来看操作较轻 | 长时间争而不得,会浪费 CPU 时间片 | 并发竞争强度低的场景 |
这里对这两种锁思想做一个介绍吧:
阻塞/唤醒:这种形式被称为是悲观锁,当G获取锁失败而阻塞时,会被挂起,标记为waiting的状态,主动让出Processor,直接让M和G结合,而P去执行其他的G(保证不会浪费这个P)锁被释放之后,才会唤醒G
自旋+CAS:这种形式被称为是乐观锁,主动权掌握在自己的手中(也就是不释放processor),会不断主动轮询尝试获取这个锁
而sync.Mutex结合了上述的两种方案,指定了一个锁升级的过程,让我们来看看吧
进行了一个怎么样的锁升级?
其实就是设计了一个状态的转化,由乐观转换为悲观,为什么要这样设计呢?
先来说说具体的方法:
- 首先保持乐观,goroutine 采用自旋 + CAS 的策略争夺锁;
- 尝试持续受挫达到一定条件后,判定当前过于激烈,则由自旋转为 阻塞/挂起模式.
这样做的原因是可以具备探测和适应环境,因地制宜采取不同的策略,首先采用乐观的状态,如果几次自旋无果,就认为现在是并发激烈的情况,就会转化为悲观的状态。
1.3 饥饿模式
上一小节的升级策略主要是面向性能,而本小节引入的饥饿模式,则是对公平性问题的探讨。
下面首先拎清两个概念:
- 饥饿:顾名思义,是因为非公平机制的原因,导致 Mutex 阻塞队列中存在 goroutine 长时间取不到锁,从而陷入饥荒状态;
- 饥饿模式:当 Mutex 阻塞队列中存在处于饥饿态的 goroutine 时,会进入模式,将抢锁流程由非公平机制转为公平机制.
Mutex运作下的两种模式
- 正常模式/非饥饿模式:这是 sync.Mutex 默认采用的模式. 当有 goroutine 从阻塞队列被唤醒时,会和此时先进入抢锁流程的 goroutine 进行锁资源的争夺,假如抢锁失败,会重新回到阻塞队列头部.
这里虽然有一个阻塞队列,当锁资源被释放,按理说阻塞队列的队首的G或获取这个锁资源,这其实是很公平了,但是实际上他只是看似公平,因为还有没进阻塞队列的G,还记得什么时候进阻塞队列嘛?对,就是当自旋结束才会进,这样一来就很清晰了,队首的G会和自旋的G抢占这个锁,如果说队首的G排了半天队,结果被这个初出茅庐的自旋G抢了锁资源,这还叫公平嘛?结果显而易见,肯定是不公平的,于是为了解决这个问题,就有了饥饿模式。
(值得一提的是,此时被唤醒的老 goroutine 相比新 goroutine 是处于劣势地位,因为新 goroutine 已经在占用 CPU 时间片,且新 goroutine 可能存在多个,从而形成多对一的人数优势,因此形势对老 goroutine 不利.)
- 饥饿模式:这是 sync.Mutex 为拯救陷入饥荒的老 goroutine 而启用的特殊机制,饥饿模式下,锁的所有权按照阻塞队列的顺序进行依次传递. 新 goroutine 进行流程时不得抢锁,而是进入队列尾部排队.
这样就可以避免自旋的锁抢占锁资源了
两种模式的转化
- 默认为正常模式;
- 正常模式 -> 饥饿模式:当阻塞队列存在 goroutine 等锁超过 1ms 而不得,则进入饥饿模式;
- 饥饿模式 -> 正常模式:当阻塞队列已清空,或取得锁的 goroutine 等锁时间已低于 1ms 时,则回到正常模式.
小结:正常模式灵活机动,性能较好;饥饿模式严格死板,但能捍卫公平的底线. 因此,两种模式的切换体现了 sync.Mutex 为适应环境变化,在公平与性能之间做出的调整与权衡. 回头观望,这一项因地制宜、随机应变的能力正是许多优秀工具所共有的特质.
二.sync.Mutex
在这之前呢,做一个简单的补充,在sync下,提供了一个接口,提供了一个实现属于自己的锁的方法哦
type Locker interface {Lock()Unlock()
}
2.1 核心数据结构
type Mutex struct {state int32sema uint32
}
- state:锁中最核心的状态字段,不同 bit 位分别存储了 mutexLocked(是否上锁)、mutexWoken(是否有 goroutine 从阻塞队列中被唤醒)、mutexStarving(是否处于饥饿模式)的信息,具体在 2.2 节详细展开;
- sema:用于阻塞和唤醒 goroutine 的信号量.
const (mutexLocked = 1 << iota // mutex is lockedmutexWokenmutexStarvingmutexWaiterShift = iotastarvationThresholdNs = 1e6
)
- mutexLocked = 1:state 最右侧的一个 bit 位标志是否上锁,0-未上锁,1-已上锁;
- mutexWoken = 2:state 右数第二个 bit 位标志是否有 goroutine 从阻塞中被唤醒,0-没有,1-有;
- mutexStarving = 4:state 右数第三个 bit 位标志 Mutex 是否处于饥饿模式,0-非饥饿,1-饥饿;
- mutexWaiterShift = 3:右侧存在 3 个 bit 位标识特殊信息,分别为上述的 mutexLocked、mutexWoken、mutexStarving;
- starvationThresholdNs = 1 ms:sync.Mutex 进入饥饿模式的等待时间阈值.
2.2 state字段
低 3 位分别标识 mutexLocked(是否上锁)、mutexWoken(是否有协程在抢锁)、mutexStarving(是否处于饥饿模式),高 29 位的值聚合为一个范围为 0~2^29-1 的整数,表示在阻塞队列中等待的协程个数.
2.3 加锁Mutex.Lock() (了解即可)
在之前说过一个锁要实现加锁和解锁的操作,接下来就来看看加锁的操作
func (m *Mutex) Lock() {// Fast path: 尝试直接通过 CAS 抢占锁if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// Slow path: 处理锁竞争或锁已被持有的情况m.lockSlow()
}
使用 原子操作 CompareAndSwapInt32
检查锁状态:如果锁的 state
为 0
(未锁定),则将其设为 mutexLocked
(1),表示锁被当前 Goroutine 持有。
否则就进入lockslow
来看下这个lockslow
func (m *Mutex) lockSlow() {var waitStartTime int64starving := falseawoke := falseiter := 0old := m.state
• waitStartTime:标识当前 goroutine 在抢锁过程中的等待时长,单位:ns;
• starving:标识当前是否处于饥饿模式;
• awoke:标识当前是否已有协程在等锁;
• iter:标识当前 goroutine 参与自旋的次数;
• old:临时存储锁的 state 值.for {// 进入该 if 分支,说明抢锁失败,处于饥饿模式,但仍满足自旋条件if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// 进入该 if 分支,说明当前锁阻塞队列有协程,但还未被唤醒,因此需要将 // mutexWoken 标识置为 1,避免再有其他协程被唤醒和自己抢锁if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin()iter++old = m.statecontinue}// ......}if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}
- 走进 for 循环;
- 假如满足三个条件:I 锁已被占用、 II 锁为正常模式、III 满足自旋条件(runtime_canSpin 方法),则进入自旋后处理环节;
- 在自旋后处理中,假如当前锁有尚未唤醒的阻塞协程,则通过 CAS 操作将 state 的 mutexWoken 标识置为 1,将局部变量 awoke 置为 true;
- 调用 runtime_doSpin 告知调度器 P 当前处于自旋模式;
- 更新自旋次数 iter 和锁状态值 old;
- 通过 continue 语句进入下一轮尝试.
上面的部分可以自旋的情况,当一定次数的自旋之后,会改变状态,调整字段,然后进入悲观状态,我们来看看,简单过一遍吧,结合ai的解读
func (m *Mutex) lockSlow() {// ......for {// ......new := old// 若非饥饿模式,尝试直接获取锁if old&mutexStarving == 0 {new |= mutexLocked}// 若锁已被持有或处于饥饿模式,增加等待者数量if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// 若锁已被持有或处于饥饿模式,增加等待者数量if starving && old&mutexLocked != 0 {new |= mutexStarving}// 清除唤醒标志(若当前协程已被唤醒)if awoke {.if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}if atomic.CompareAndSwapInt32(&m.state, old, new) {// 成功获取锁(仅在非饥饿模式且锁未被持有时可能)if old&(mutexLocked|mutexStarving) == 0 {break }// 加入等待队列(LIFO 或 FIFO,取决于是否已等待过)queueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// 阻塞等待信号量唤醒runtime_SemacquireMutex(&m.sema, queueLifo, 1)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.stateif old&mutexStarving != 0 {// 调整状态:减少等待者数量,可能退出饥饿模式if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {// 退出饥饿模式delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state// CAS 失败,重新加载状态}}if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}
2.4 Unlock (了解即可)
func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {m.unlockSlow(new)}
}
func (m *Mutex) unlockSlow(new int32) {if (new+mutexLocked)&mutexLocked == 0 {fatal("sync: unlock of unlocked mutex")}if new&mutexStarving == 0 {old := newfor {// If there are no waiters or a goroutine has already// been woken or grabbed the lock, no need to wake anyone.// In starvation mode ownership is directly handed off from unlocking// goroutine to the next waiter. We are not part of this chain,// since we did not observe mutexStarving when we unlocked the mutex above.// So get off the way.if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// Grab the right to wake someone.new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// Starving mode: handoff mutex ownership to the next waiter, and yield// our time slice so that the next waiter can start to run immediately.// Note: mutexLocked is not set, the waiter will set it after wakeup.// But mutex is still considered locked if mutexStarving is set,// so new coming goroutines won't acquire it.runtime_Semrelease(&m.sema, true, 1)}
}
这里就不再多介绍了,可以下去自己看一下源码,借助ai,其实只要知道大致的思想,在去编写的时候,在看即可了。
三.sync.RWMutex
- 从逻辑上,可以把 RWMutex 理解为一把读锁加一把写锁;
- 写锁具有严格的排他性,当其被占用,其他试图取写锁或者读锁的 goroutine 均阻塞;
- 读锁具有有限的共享性,当其被占用,试图取写锁的 goroutine 会阻塞,试图取读锁的 goroutine 可与当前 goroutine 共享读锁;
- 综上可见,RWMutex 适用于读多写少的场景,最理想化的情况,当所有操作均使用读锁,则可实现去无化;最悲观的情况,倘若所有操作均使用写锁,则 RWMutex 退化为普通的 Mutex
3.1 核心数据结构
type RWMutex struct {w Mutex // held if there are pending writerswriterSem uint32 // semaphore for writers to wait for completing readersreaderSem uint32 // semaphore for readers to wait for completing writersreaderCount atomic.Int32 // number of pending readersreaderWait atomic.Int32 // number of departing readers
}
- rwmutexMaxReaders:共享读锁的 goroutine 数量上限,值为 2^29;
- w:RWMutex 内置的一把普通互斥锁 sync.Mutex;
- writerSem:关联写锁阻塞队列的信号量;
- readerSem:关联读锁阻塞队列的信号量;
- readerCount:正常情况下等于介入读锁流程的 goroutine 数量;当 goroutine 接入写锁流程时,该值为实际介入读锁流程的 goroutine 数量减 rwmutexMaxReaders.
- readerWait:记录在当前 goroutine 获取写锁前,还需要等待多少个 goroutine 释放读锁.
源码的走读就不再写了,后续在学分布式锁的时候在完善。
相关文章:

go语言的锁
本篇文章主要讲锁,主要会涉及go的sync.Mutex和sync.RWMutex。 一.锁的概念和发展 1.1 锁的概念 所谓的加锁和解锁其实就是指一个数据是否被占用了,通过Mutex内的一个状态来表示。 例如,取 0 表示未加锁,1 表示已加锁ÿ…...
C++11完美转发
在 C11 之前,泛型函数在传递参数时无法保证参数的原始类型(左值或右值)导致额外的拷贝或移动操作,完美转发是一种高效传递技术,能够保持参数的原始特性,避免额外的性能开销 完美转发是指在泛型编程中以参数…...

VUE解决页面请求接口大规模并发的问题(请求队列)
方案1: 请求队列 // RequestQueue.js export default class RequestQueue {constructor(maxConcurrent) {this.maxConcurrent maxConcurrent; // 最大并发请求数this.currentConcurrent 0; // 当前并发请求数this.queue []; // 请求队列this.requestId 0; // …...

IDEA安装迁移IDEA配置数据位置
需求 因为C盘有清空风险,需要把IDEA(2025)安装位置以及配置数据都挪到D盘。 安装 到官网下载安装包 安装,这里可以改下安装位置 这几个选项随意,然后一直下一步就好 完成后重启或不重启都随意 迁移数据 初次安…...

Blazor-表单提交的艺术:如何优雅地实现 (下)
在上一章节中我们使用HTML的方式介绍了如何在Blazor框架下进行表单的提交,而在Blazor框架中也为我们内置了<EditForm>组件来代替原始的HTML,<form>,下面我们将对<EditForm>的用法进行讲解,并将两种表单方式进行对比&#x…...

五子棋网络对战游戏的设计与实现设计与实现【源码+文档】
五子棋网络对战游戏的设计与实现 摘 要 在现代社会中,及其它无线设备越来越多的走进普通老百姓的工作和生活。随着3G技术的普及与应用,基于Java开发的软件在上的使用非常的广泛,增值服务的内容也是越来越多,对丰富人们的生活内容、提供快…...

Vue基础(14)_列表过滤、列表排序
Array.prototype.filter()【ES5】 filter() 方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。 语法: filter(callbackFn) filter(callbackFn, thisArg) 参数: callbackFn(回调函数):为数组中的每个元…...

Spring Boot项目中JSON解析库的深度解析与应用实践
在现代Web开发中,JSON(JavaScript Object Notation)作为轻量级的数据交换格式,已成为前后端通信的核心桥梁。Spring Boot作为Java生态中最流行的微服务框架,提供了对多种JSON库的无缝集成支持。本文将深入探讨Spring B…...

我用Amazon Q写了一个Docker客户端,并上架了懒猫微服商店
自从接触了Amazon Q,我陆陆续续写了不少小软件,其中这个项目是一个典型的例子,自己平时来使用,也分享给一些 NAS 爱好者来用。 故事还要用上次折腾黑群晖说起,本意想把 NAS 和打印机共享二合一的,所以把闲着…...

Django CMS 的 Demo
以下是关于 Django CMS 的 Demo 示例及相关资源的整理 安装与运行 Django CMS 示例 使用 djangocms-installer 快速创建 Django CMS 项目: pip install django_cms djangocms -p . mysite安装记录 pip install django-cms Looking in indexes: https://pypi.tun…...

在 UE5 蓝图中配置Actor类型的Asset以作为位置和旋转设置目标
目标 UE5的蓝图的事件图表里面,有一个模块(节点)如图,这是一个设置Actor的location和rotation量的模块,其中需要接收一个Target作为输入,这个Target应该就是一个在map中具备location和rotation信息的实例化…...
Android 之 kotlin 语言学习笔记四(Android KTX)
一、Android KTX 简介 Android KTX 是包含在 Android Jetpack 及其他 Android 库中的一组 Kotlin 扩展程序。KTX 扩展程序可以为 Jetpack、Android 平台及其他 API 提供简洁的惯用 Kotlin 代码。为此,这些扩展程序利用了多种 Kotlin 语言功能,其中包括&…...

适用于vue3的大屏数据展示组件库DataV(踩坑版)
踩坑版 如果按照官网(https://datav-vue3.jiaminghi.com/)的vue3安装有问题 官网是将dataview/datav-vue3 安装为本地依赖 npm install dataview/datav-vue31、跑起来报错(报错信息忘记保留了) 有人说找到node_modules, 安装成功后会有这个…...
mysql实现分页查询
文章目录 mysql实现分页查询1. 使用LIMIT和OFFSET2. 使用计算OFFSET的函数(适用于动态分页)3. 使用MySQL的变量(适用于存储过程) 获取所有用户数据并分页 mysql实现分页查询 在MySQL中实现分页查询,通常我们会使用LIM…...
Flink checkpoint
对齐检查点 (Aligned Checkpoint) Flink 的分布式快照机制受到 Chandy-Lamport 算法的启发。 其核心元素是数据流中的屏障(Barrier)。 Barrier 注入 :JobManager 中的 Checkpoint Coordinator 指示 Source 任务开始 Checkpoint。Source 任务…...
【java】在springboot中实现证书双向验证
证书生成 public static void main(String[] args) throws Exception {// 生成密钥对KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);KeyPair keyPair keyPairGenerator.generateKeyPair();// 获取私…...
CppCon 2015 学习:Functional Design Explained
这两个 C 程序 不完全相同。它们的差异在于对 std::cout 的使用和代码格式。 程序 1: #include <iostream> int main(int argc, char** argv) {std::cout << "Hello World\n"; }解释:这个程序是 正确的。std::cout 是 C 标准库中…...

基于3D对象体积与直径特征的筛选
1,目的 筛选出目标3D对象。 效果如下: 2,原理 使用3D对象的体积与直径特征进行筛选。 3,代码解析 3.1,预处理2.5D深度图。 * 参考案例库:select_object_model_3d.hdev * ****************************…...
GIT - 如何从某个分支的 commit创建一个新的分支?
如果上一个Release 分支被污染了,想要还原这个分支最原始的样子,有什么办法或者说该怎么办呢?简单来说,就是如何从某个指定的 commit 创建一个新的 Git 分支? 操作非常简单! 命令格式 git branch <ne…...
Claude vs ChatGPT vs Gemini:功能对比、使用体验、适合人群
随着AI应用全面进入生产力场景,市面上的主流AI对话工具也进入“三国杀”时代: Claude(Anthropic):新锐崛起,语言逻辑惊艳,Opus 模型被称为 GPT-4 杀手ChatGPT(OpenAI)&a…...
线程基础编程
早期的计算机只能执行一个任务,一旦任务完成,计算机就会等待下一个任务。这种模型效率低下,无 法充分利用计算机的性能。 随着计算机技术的发展,操作系统开始支持多进程模型,即同时执行多个任务。每个任务被称为一个进…...

DJango项目
一.项目创建 在想要将项目创键的目录下,输入cmd (进入命令提示符)在cmd中输入:Django-admin startproject 项目名称 (创建项目)cd 项目名称 (进入项目)Django-admin startapp 程序名称 (创建程序)python manage.py runserver 8080 (运行程序)将弹出的网址复制到浏览器中…...
深入了解JavaScript当中如何确定值的类型
JavaScript是一种弱类型语言,当你给一个变量赋了一个值,该值是什么类型的,那么该变量就是什么类型的,并且你还可以给一个变量赋多种类型的值,也不会报错,这就是JavaScript的内部机制所决定的,那…...

excel数据对比找不同:6种方法核对两列数据差异
工作中,有时需要核对两列数据的差异,用于对比、复核等。数据较少的情况下差异肉眼可见,数据量较大时用什么方法比较好呢?从个人习惯出发,我整理了6种方法供参考。 6种方法核对两列数据差异: 1、Ctrl G定位…...

基于智能代理人工智能(Agentic AI)对冲基金模拟系统:模范巴菲特、凯西·伍德的投资策略
股票市场涉及众多统计数据和模式。股票交易基于研究和数据驱动的决策。人工智能的使用可以实现流程自动化,让投资者在研究上花费更少的时间,同时提高准确性。这使他们能够更加专注于监督实际交易和服务客户。 顶尖对冲基金经理发挥着至关重要的作用&…...

MySQL数据库基础(二)———数据表管理
前言 上篇文章介绍了MySQL数据库以即数据库的管理 这篇文章将给大家讲解数据表的管理 一、数据表常见操作 数据表常见操作的指令 进入数据库use数据库; 查看当前所有表:show tables; 创建表结构 1.创建表操作 1.1创建表 create table 表名(列名 …...
如何在Lyra中创建一个新的Game Feature Plugin和Experience游戏体验
目录 -1.前言0.预备知识1.创建一个新的Game Feature Plugin插件2.创建Lyra Pawn Data Asset3. 创建Lyra Experience Definition4. 创建自定义关卡5. 设置资产管理器Asset Manager引用6. 创建Lyra User Facing Experience Definition7. 在编辑器中运行测试后记-1.前言 由于转职…...

RDMA简介5之RoCE v2队列
在RoCE v2协议中,RoCE v2队列是数据传输的最底层控制机制,其由工作队列(WQ)和完成队列(CQ)共同组成。其中工作队列采用双向通道设计,包含用于存储即将发送数据的发送队列(SQ…...

SAFe/LeSS/DAD等框架的核心适用场景如何选择?
在敏捷开发的规模化实践中,SAFe(Scaled Agile Framework)、LeSS(Large Scale Scrum)和DAD(Disciplined Agile Delivery)是三大主流框架。它们分别以不同的哲学和方法论应对复杂性、协作与交付的…...
鸿蒙应用开发之uni-app x实践
鸿蒙应用开发之uni-app x实践 前言 最近在开发鸿蒙应用时,发现uni-app x从4.61版本开始支持纯血鸿蒙(Harmony next),可以直接编译成ArkTS原生应用。这里记录一下开发过程中的一些经验和踩过的坑。 一、环境搭建 1.1 开发工具 …...