仓颉语言线程安全保证机制:从原理到实战的深度解析

仓颉语言线程安全保证机制:从原理到实战的深度解析

在鸿蒙生态开发中,多线程/协程并发是提升系统吞吐量的核心手段,但并发操作必然伴随“线程安全”挑战——当多个执行单元同时操作共享资源时,若缺乏有效保护,极易出现数据竞争、死锁、内存可见性等问题。作为鸿蒙生态的主力编程语言,仓颉并未简单沿用传统语言的线程安全方案,而是结合自身协程模型与鸿蒙内核特性,设计了一套“分层防护、原生支持、易用高效”的线程安全保证机制。本文将从原理层面拆解仓颉的线程安全核心技术,并通过实战案例验证其在实际开发中的可落地性,为鸿蒙开发者提供系统化的线程安全解决方案。

一、仓颉线程安全的核心挑战:为何需要专属保证机制?

在分析仓颉的解决方案前,需先明确其面临的独特线程安全挑战——这些挑战源于仓颉“协程+线程”混合并发模型,与传统语言存在显著差异:

  1. N:M映射下的资源竞争加剧:仓颉采用N个协程映射到M个内核线程的模型,虽然协程切换在用户态完成,但多个协程可能在同一内核线程上串行执行,也可能在不同内核线程上并行执行。这种“串行与并行交织”的特性,使得共享资源的访问场景更复杂——例如,一个全局计数器可能同时被10个协程访问,其中5个在同一线程串行执行,另外5个在不同线程并行执行,传统“线程级锁”无法完全覆盖协程场景。
  2. 用户态与内核态资源的一致性维护:仓颉协程的栈空间、上下文等资源在用户态管理,而内核线程的调度、内存页等资源由鸿蒙内核管理。当协程跨线程迁移(如调度器将阻塞后唤醒的协程分配到新内核线程)时,若共享资源未做特殊处理,可能出现“用户态可见性”与“内核态可见性”不一致的问题——例如,协程A在Thread1中修改了共享变量count,但CPU缓存未刷新到主存,协程B被调度到Thread2后读取count,可能获取到旧值。
  3. 低开销与高安全性的平衡:传统线程安全方案(如Java的synchronized、C++的std::mutex)往往伴随内核态锁的开销,而仓颉的设计目标是“支持百万级协程并发”,若每个线程安全操作都引入微秒级开销,将严重影响整体性能。因此,仓颉需要在“保证安全”与“控制开销”之间找到平衡点,避免因线程安全保护导致并发性能退化。

二、仓颉线程安全保证的三层防护体系

针对上述挑战,仓颉构建了“用户态轻量锁→内存可见性保障→内核级锁兜底”的三层防护体系,不同层级对应不同并发场景,兼顾安全性与性能。

(一)第一层:用户态轻量锁——协程级安全防护

针对“同一内核线程内多个协程的串行竞争”场景,仓颉设计了协程级自旋锁(Coroutine Spinlock) ,完全在用户态实现,避免内核态切换开销,是仓颉线程安全的基础防护手段。

1. 核心原理

协程级自旋锁的本质是“基于原子变量的忙等锁”:

  • 锁状态标识:使用一个32位原子变量lock_flag表示锁状态,0表示未加锁,1表示已加锁;
  • 加锁逻辑:协程尝试加锁时,通过CAS(Compare And Swap)原子操作将lock_flag0改为1。若操作成功,说明加锁成功,协程可继续执行;若操作失败(锁已被其他协程持有),则进入“忙等”状态——循环执行轻量级指令(如pause),直到锁被释放;
  • 解锁逻辑:协程执行完临界区代码后,通过原子操作将lock_flag设为0,释放锁。此时,正在忙等的协程可通过下一次CAS操作获取锁。
2. 关键优势:适配协程调度特性

与传统自旋锁相比,仓颉的协程级自旋锁做了两点关键优化,适配N:M调度模型:

  • 忙等时主动让出CPU:若协程忙等超过100次循环(约10纳秒),会主动调用coroutine_yield()接口,将自身切换为“就绪状态”,让调度器分配其他协程执行,避免单个协程长时间占用CPU(传统自旋锁在多核场景下可能导致CPU空转);
  • 锁与协程绑定:通过协程ID(每个协程唯一的32位标识)记录锁的持有者,若持有锁的协程因IO操作被阻塞,调度器会自动释放该协程持有的所有轻量锁,避免“协程阻塞导致锁泄漏”(这是传统自旋锁在协程场景下的常见问题)。
3. 适用场景

协程级自旋锁适用于“临界区代码执行时间短(如毫秒级以下)、同一线程内协程竞争频繁”的场景,例如:

  • 全局计数器的自增操作(如count++);
  • 协程间共享的小型缓存(如LRU缓存的节点插入);
  • 轻量级消息队列的入队/出队操作。
4. 实战代码示例
// 定义协程级自旋锁
var counter_lock: CoroutineSpinlock = CoroutineSpinlock()
var global_counter: Int = 0

// 协程函数:安全地自增全局计数器
async func safe_increment() {
    // 加锁:若锁被占用,忙等或主动让出CPU
    counter_lock.lock()
    defer {
        // 解锁:函数退出时自动释放锁,避免遗漏
        counter_lock.unlock()
    }
    
    // 临界区:安全操作共享变量
    global_counter += 1
    print("当前计数器值:\(global_counter)")
}

// 并发测试:创建1000个协程同时调用safe_increment
func test_coroutine_lock() {
    let group = AsyncGroup()
    for _ in 0..<1000 {
        group.spawn(safe_increment())
    }
    group.wait()
    // 最终输出:当前计数器值:1000(无数据竞争,结果正确)
}

(二)第二层:内存可见性保障——跨线程数据一致性

当协程在不同内核线程间迁移时,即使没有显式的数据竞争,也可能因CPU缓存、指令重排序导致“内存可见性”问题。仓颉通过内存屏障(Memory Barrier)原子类型(Atomic Type) ,从语言层面保障跨线程的内存可见性。

1. 内存可见性问题的根源

在多核CPU架构中,每个CPU都有独立的缓存(L1/L2/L3),当协程修改共享变量时,修改会先写入CPU缓存,再由缓存控制器异步刷新到主存。若其他线程的协程读取该变量时,读取的是旧的缓存值,就会出现“可见性问题”。例如:

// 线程1的协程A执行
var flag: Bool = false
var data: String = ""

async func write_data() {
    data = "鸿蒙开发者"  // 步骤1:修改data
    flag = true        // 步骤2:设置flag为true
}

// 线程2的协程B执行
async func read_data() {
    while !flag {      // 步骤3:循环检查flag
        // 空等
    }
    print(data)        // 步骤4:读取data,可能输出空字符串?
}

由于CPU可能对步骤1和步骤2进行指令重排序,或步骤2的修改未刷新到主存,协程B可能在步骤3中检测到flag=true,但步骤4中读取到的data仍是空字符串——这就是典型的内存可见性问题。

2. 仓颉的解决方案:原生内存屏障与原子类型

为解决该问题,仓颉从两个层面提供保障:

  • 显式内存屏障:提供memory_fence()函数,强制CPU执行缓存刷新与指令排序。例如,在write_data()中插入内存屏障:

    async func write_data() {
        data = "鸿蒙开发者"
        memory_fence()  // 强制刷新缓存,确保data的修改可见
        flag = true
    }
    

    memory_fence()会阻止CPU对屏障前后的指令进行重排序,并将缓存中的修改同步到主存,确保后续线程能读取到最新值。

  • 原子类型(Atomic):对于需要频繁读写的共享变量(如计数器、状态标识),仓颉提供Atomic<T>泛型类型,其所有操作(读取、修改、比较交换)都内置内存屏障,无需开发者手动插入。例如:

    // 定义原子类型的计数器,内置内存可见性保障
    var atomic_counter: Atomic<Int> = Atomic(initialValue: 0)
    
    async func atomic_increment() {
        // 原子自增:操作本身包含内存屏障,跨线程可见
        atomic_counter.increment()
        // 原子读取:确保读取到最新值
        print("原子计数器值:\(atomic_counter.load())")
    }
    

    Atomic<T>的底层实现依赖鸿蒙内核的hwi_atomic接口,确保在不同CPU架构(如ARM、x86)下都能提供一致的内存可见性保障,避免开发者因底层架构差异引入安全隐患。

3. 实战价值:简化跨线程开发

在鸿蒙分布式设备开发中,内存可见性保障尤为重要。例如,在“手机-平板协同办公”场景中,手机端协程修改共享文档的编辑状态,平板端协程需要实时读取该状态。通过仓颉的Atomic<T>类型,开发者无需关注底层缓存刷新逻辑,只需使用原子操作即可确保状态同步,大幅降低跨设备并发开发的复杂度。

(三)第三层:内核级锁兜底——重量级资源保护

对于“临界区代码执行时间长(如秒级)、跨线程竞争频繁”的场景(如文件读写、数据库连接池),协程级轻量锁的忙等机制会导致CPU资源浪费,此时需要内核级锁(Kernel Mutex) 兜底——这类锁由鸿蒙内核实现,支持线程阻塞与唤醒,避免CPU空转。

1. 核心原理:与鸿蒙内核深度协同

仓颉的内核级锁并非独立实现,而是直接封装鸿蒙内核的mutex接口,实现“语言层与内核层的安全协同”:

  • 加锁逻辑:协程调用KernelMutex.lock()时,若锁未被持有,直接获取锁;若锁已被持有,协程会通过hwi_mutex_lock接口向鸿蒙内核发起阻塞请求,内核将该协程对应的内核线程挂起,放入锁的等待队列,释放CPU资源;
  • 解锁逻辑:协程调用KernelMutex.unlock()时,通过hwi_mutex_unlock接口通知内核,内核从等待队列中唤醒一个线程,使其对应的协程能够获取锁;
  • 死锁检测:仓颉的内核级锁内置死锁检测机制,通过记录锁的持有链(如协程A持有锁1,等待锁2;协程B持有锁2,等待锁1),若检测到循环等待,会立即抛出DeadlockException,避免系统卡死。
2. 与协程调度的适配优化

传统内核级锁在协程场景下的最大问题是“协程阻塞导致线程阻塞”——若持有锁的协程因IO操作被调度器切换,内核线程会一直持有锁,导致其他协程阻塞。仓颉通过“锁与协程的绑定映射”解决该问题:

  • 当持有内核级锁的协程被调度器挂起(如执行await操作),调度器会自动将锁的“持有权”从当前协程转移到内核线程,确保其他协程尝试加锁时,内核能正确识别锁的持有状态;
  • 当协程恢复执行时,调度器会将锁的持有权重新转移回协程,避免锁的所有权混乱。
3. 适用场景与实战案例

内核级锁适用于“重量级共享资源保护”,例如:

  • 共享文件的读写(如日志文件追加写入,临界区执行时间可能达毫秒级);
  • 数据库连接池的连接获取与释放(每个连接的操作时间可能达秒级);
  • 分布式锁的抢占(如基于Redis的分布式锁,需要网络IO操作)。

以“鸿蒙智能家居系统的日志写入”为例,多个设备的协程需要向同一个日志文件写入数据,使用内核级锁可确保日志内容不混乱:

// 定义内核级锁,保护日志文件写入
var log_mutex: KernelMutex = KernelMutex()
let log_file: File = File(path: "/data/smarthome.log")

// 协程函数:安全写入日志
async func safe_write_log(device_id: String, content: String) throws {
    // 加锁:若锁被占用,协程对应的线程会被内核挂起,不占用CPU
    log_mutex.lock()
    defer {
        log_mutex.unlock()
    }
    
    // 临界区:写入日志(执行时间可能达10毫秒)
    let timestamp = DateTime.now().format("yyyy-MM-dd HH:mm:ss")
    let log_entry = "[\(timestamp)] [\(device_id)]: \(content)\n"
    try log_file.append(content: log_entry)
}

// 多设备并发写入测试
async func test_kernel_lock() {
    let group = AsyncGroup()
    // 模拟3个设备的协程同时写入日志
    group.spawn(safe_write_log(device_id: "light_001", content: "开启灯光"))
    group.spawn(safe_write_log(device_id: "ac_001", content: "设置温度26℃"))
    group.spawn(safe_write_log(device_id: "curtain_001", content: "关闭窗帘"))
    await group.wait()
    // 日志文件内容(顺序可能不同,但无重叠):
    // [2024-05-20 14:30:00] [light_001]: 开启灯光
    // [2024-05-20 14:30:00] [ac_001]: 设置温度26℃
    // [2024-05-20 14:30:00] [curtain_001]: 关闭窗帘
}

三、实战进阶:仓颉线程安全的最佳实践

掌握仓颉的三层防护体系后,还需结合实际场景选择合适的方案,避免“过度防护”或“防护不足”。以下是三个核心最佳实践:

(一)按“临界区耗时”选择锁类型

不同锁类型的性能差异主要源于“等待策略”,需根据临界区执行时间选择:

  • <1微秒:优先使用Atomic<T>原子类型,无需加锁,性能最优;
  • 1微秒~1毫秒:使用协程级自旋锁,避免内核切换开销;
  • >1毫秒:使用内核级锁,避免CPU空转浪费资源。

例如,“用户登录状态缓存”的更新操作:若缓存是内存中的哈希表,更新耗时约100纳秒,适合用Atomic<T>;若缓存更新需要同步到本地文件,耗时约5毫秒,适合用内核级锁。

(二)利用“结构化并发”减少共享资源

线程安全的根本解决方案是“减少共享资源”。仓颉的AsyncGroup结构化并发特性,可将并发任务封装为独立单元,避免全局共享变量:

// 不推荐:使用全局共享变量传递结果,需加锁
var global_result: [String] = []
var result_lock: CoroutineSpinlock = CoroutineSpinlock()

// 推荐:通过AsyncGroup的返回值传递结果,无共享资源
async func fetch_data(url: String) -> String {
    let response = await Http.request(url: url)
    return response.body
}

async func structured_concurrency_demo() {
    let group = AsyncGroup()
    // 并发请求,结果通过任务返回值获取,无需共享变量
    let task1 = group.spawn(fetch_data(url: "https://api1.harmonyos.com/data"))
    let task2 = group.spawn(fetch_data(url: "https://api2.harmonyos.com/data"))
    
    let data1 = await task1.get()
    let data2 = await task2.get()
    // 本地合并结果,无线程安全问题
    let merged_data = "\(data1)\n\(data2)"
}

通过结构化并发,将共享资源转化为“任务间的通信”,从源头减少线程安全问题。

(三)分布式场景下的线程安全扩展

在鸿蒙分布式场景中,线程安全需要跨设备保障。仓颉的DistributedLock(分布式锁)基于鸿蒙的分布式软总线技术实现,可在多设备间提供一致的锁服务:

// 分布式锁:跨设备保护共享资源(如家庭共享打印机)
var distributed_printer_lock: DistributedLock = DistributedLock(
    lockId: "home_printer_lock",  // 锁的唯一标识,跨设备可见
    timeout: 5000                 // 锁超时时间,避免设备离线导致锁泄漏
)

// 设备A的协程:获取打印机锁并执行打印
async func print_from_device_a(content: String) throws {
    // 尝试获取分布式锁,超时5秒未获取则抛出异常
    try distributed_printer_lock.lock(timeout: 5000)
    defer {
        // 无论打印成功与否,最终释放锁,确保其他设备可用
        distributed_printer_lock.unlock()
    }
    
    // 临界区:调用打印机API执行打印(跨设备通信操作)
    let printer = DistributedDevice.getDevice("printer_001")
    try printer.print(content: content, paperSize: .A4)
    print("设备A打印完成")
}

// 设备B的协程:竞争打印机锁
async func print_from_device_b(content: String) {
    do {
        try distributed_printer_lock.lock(timeout: 5000)
        defer {
            distributed_printer_lock.unlock()
        }
        let printer = DistributedDevice.getDevice("printer_001")
        try printer.print(content: content, paperSize: .A4)
        print("设备B打印完成")
    } catch {
        // 捕获锁超时异常,提示用户稍后重试
        print("设备B获取打印机锁失败:\(error),请5秒后重试")
    }
}

DistributedLock的核心优势在于“分布式一致性保障”:它通过鸿蒙分布式软总线同步锁状态,确保同一时间只有一个设备的协程能获取锁,避免多设备并发操作共享硬件(如打印机、共享存储)导致的资源冲突。同时,内置的超时机制与“锁续租”功能(若打印任务未完成,协程可自动续租锁),解决了分布式场景下“设备离线导致锁永久占用”的痛点。

(四)死锁预防与诊断工具

即使采用分层防护体系,若开发者使用锁的逻辑不当(如锁的获取顺序不一致),仍可能出现死锁。仓颉从“预防”与“诊断”两方面提供工具,降低死锁风险:

1. 死锁预防:锁的有序获取

仓颉推荐“按锁ID升序获取多个锁”的编码规范,并提供LockOrderGuard工具类强制检查锁的获取顺序:

// 定义两个内核级锁,分别对应订单资源与库存资源
var order_lock: KernelMutex = KernelMutex(lockId: 1)  // 锁ID=1
var stock_lock: KernelMutex = KernelMutex(lockId: 2) // 锁ID=2

// 正确示例:按锁ID升序获取锁,避免死锁
async func process_order_correctly(orderId: String) {
    // LockOrderGuard自动检查锁的获取顺序,若违反则编译警告
    let guard = LockOrderGuard()
    // 先获取ID较小的order_lock
    guard.lock(order_lock)
    // 再获取ID较大的stock_lock
    guard.lock(stock_lock)
    
    defer {
        // 解锁顺序与加锁相反,避免资源泄漏
        stock_lock.unlock()
        order_lock.unlock()
    }
    
    // 业务逻辑:扣减库存、生成订单
    try deduct_stock(orderId: orderId)
    try create_order(orderId: orderId)
}

// 错误示例:违反锁ID升序规则,编译时触发警告
async func process_order_incorrectly(orderId: String) {
    let guard = LockOrderGuard()
    // 先获取ID较大的stock_lock,触发编译警告
    guard.lock(stock_lock) 
    guard.lock(order_lock) // 编译器提示:"锁获取顺序违反规范,可能导致死锁"
    // ... 业务逻辑
}

LockOrderGuard通过编译期静态检查,强制开发者遵循锁的有序获取规则,从源头减少死锁发生的可能。

2. 死锁诊断:运行时监控工具

若程序运行时仍出现死锁,仓颉提供ThreadSafetyMonitor工具类,可实时监控锁的持有状态与等待链,并生成可视化诊断报告:

// 启用线程安全监控
ThreadSafetyMonitor.enable(
    logPath: "/data/thread_safety.log",  // 诊断日志输出路径
    monitorInterval: 1000                // 监控间隔(毫秒)
)

// 模拟死锁场景(仅作示例,实际开发需避免)
async func deadlock_demo() {
    let lock1 = KernelMutex(lockId: 1)
    let lock2 = KernelMutex(lockId: 2)
    
    // 协程A:持有lock1,等待lock2
    group.spawn {
        lock1.lock()
        sleep(100) // 模拟业务耗时,给协程B获取lock2的时间
        lock2.lock() // 等待lock2,形成死锁
        // ...
    }
    
    // 协程B:持有lock2,等待lock1
    group.spawn {
        lock2.lock()
        sleep(100)
        lock1.lock() // 等待lock1,形成死锁
        // ...
    }
    
    group.wait()
}

当死锁发生时,ThreadSafetyMonitor会在日志中输出详细的死锁信息,包括:

  • 死锁涉及的协程ID、所属线程;
  • 每个协程持有的锁与等待的锁;
  • 死锁发生的时间与调用栈。
    开发者可根据日志快速定位死锁原因,例如上述案例中,日志会明确提示“协程A持有lock1等待lock2,协程B持有lock2等待lock1,形成循环等待”。

四、仓颉线程安全机制的技术优势与行业价值

(一)相比传统语言的核心优势

  1. 分层防护,兼顾性能与安全:传统语言(如Java、C++)的线程安全方案往往是“单一锁类型全覆盖”,要么性能不足(如Java的synchronized默认是重量级锁),要么安全性不够(如C++的自旋锁无协程适配)。而仓颉的三层防护体系,针对不同场景提供精准解决方案,例如协程级锁的开销仅为传统内核锁的1/10,原子类型的性能接近无锁操作,同时内核级锁与分布式锁又能保障重量级场景的安全。

  2. 深度适配鸿蒙生态:仓颉的线程安全机制并非孤立设计,而是与鸿蒙内核、分布式软总线深度协同——例如内核级锁封装鸿蒙hwi_mutex接口,分布式锁依赖鸿蒙分布式设备管理能力,确保在鸿蒙全场景设备(手机、平板、手表、智能家居)中都能提供一致的线程安全保障。这种“语言-系统”协同的设计,是Java、Python等跨平台语言无法实现的。

  3. 开发者友好的工具链支持:从编译期的LockOrderGuard静态检查,到运行时的ThreadSafetyMonitor诊断工具,仓颉为开发者提供了全链路的线程安全辅助工具,降低了并发编程的学习成本与调试难度。例如,传统语言中定位死锁可能需要借助jstack(Java)、gdb(C++)等复杂工具,而仓颉的监控工具可直接输出可视化日志,新手开发者也能快速上手。

(二)在鸿蒙生态中的行业价值

  1. 赋能高并发鸿蒙应用开发:随着鸿蒙生态向金融、电商、物联网等领域渗透,高并发场景(如电商秒杀、金融实时交易、物联网设备数据采集)的需求日益增长。仓颉的线程安全机制支持百万级协程并发,且线程安全操作的开销极低,可帮助开发者构建高性能的鸿蒙应用,例如某鸿蒙电商平台基于仓颉的协程级锁,将秒杀场景的TPS从5万提升至15万,同时保证数据零错误。

  2. 简化分布式设备协同开发:鸿蒙的核心特性是“分布式全场景”,但多设备协同的线程安全问题一直是开发难点。仓颉的DistributedLock与内存可见性保障,为跨设备并发提供了统一的解决方案,例如智能家居厂商可基于仓颉快速开发“多设备共享打印机”“多设备协同控制灯光”等功能,无需自行设计分布式锁协议,开发效率提升60%以上。

  3. 推动鸿蒙生态的技术标准化:作为鸿蒙生态的主力编程语言,仓颉的线程安全机制为鸿蒙应用的并发编程提供了标准化方案,避免了不同开发者因采用自定义锁方案导致的“技术碎片化”。例如,鸿蒙官方开发文档将仓颉的三层防护体系列为并发编程的推荐实践,确保不同团队开发的鸿蒙应用都能具备一致的线程安全水平,降低生态内的协作成本。

五、总结与未来展望

仓颉的线程安全保证机制,通过“用户态轻量锁→内存可见性保障→内核级锁兜底”的三层防护体系,解决了传统语言在协程并发、跨线程可见性、分布式安全等场景下的痛点,同时结合鸿蒙生态特性,提供了兼顾“高性能、高安全、易用性”的解决方案。从实战角度看,无论是单机高并发场景(如电商秒杀),还是分布式多设备场景(如智能家居协同),开发者都能基于仓颉的线程安全工具,快速构建稳定可靠的鸿蒙应用。

未来,仓颉的线程安全机制将向两个方向演进:

  1. 智能锁调度:结合AI技术实现锁的动态选择——例如,系统根据历史执行数据,自动判断临界区耗时,为开发者推荐最优锁类型(如原子类型、自旋锁、内核锁),进一步降低并发编程的决策成本;
  2. 分布式安全增强:整合鸿蒙的分布式可信身份认证技术,为DistributedLock添加身份校验功能,防止未授权设备抢占锁资源,提升分布式场景下的安全性。

对于鸿蒙开发者而言,掌握仓颉的线程安全机制不仅是解决当前并发问题的关键,更是未来在鸿蒙生态中构建高性能、高可靠应用的核心能力。随着仓颉语言的不断成熟与鸿蒙生态的持续扩张,这套线程安全方案将成为鸿蒙并发编程的“事实标准”,为鸿蒙生态的繁荣发展提供坚实的技术支撑。

请添加图片描述

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工藤学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值