glib库异步队列和线程池代码分析

linux的glib库使用 广泛应用于桌面环境、嵌入式系统、GNOME等项目中。它提供了完整的面向对象编程模型(GObject)、信号/槽机制、内存管理等高级功能。优点提供了完整的面向对象支持(如继承、封装、多态)。包含了信号/槽机制,适合事件驱动编程。支持跨平台和嵌入式系统,具有较好的可移植性。性能较好,适合资源受限的嵌入式系统(尤其是在高端嵌入式设备中)。 阅读详情
本文章主要讲了两部分内容:一是分析了异步队列的原理和实现,二是分析线程池的原理和实现。
在多线程程序的运行中,如果经常地创建和销毁执行过程相似而所用数据不同的线程,系统的效率,系统资源的利用率将会受到极大的影响。对于这一问题可用类似glib库中的线程池的解决办法。
  
我 们可以这样想像线程池的处理,当有新的数据要交给线程处理时,主程序/主线程 就从线程池中找到一个未被使用的线程处理这新来的数据,如果线程池中没有找到可用的空闲线程,就新创建一个线程来处理这个数据,并在处理完后不销毁它而是 把这个线程放到线程池中,以备后用。线程池的这个原理和内存管理中slab机制有异曲同工之妙!我想无论是线程池的这种处理方式还是slab机制,其本质 思想还是一致的。
近来做的项目,在框架中用到了多线程的异步队列,实现形式和glib中的异步队列极其相似,而glib线程池中的代码也用到了异步队列(名字用同步队列更合适),因此就先分析一下异步队列。
异 步队列的概念是这样的:所有的数据组织成队列,供多线程并发访问,而这些并发控制全部在异步队列里面实现,对外面只提供读写接口;当队列中的数据为空时, 如果是读线程访问异步队列,那么这一读线程就等待,直到有数据为止;写线程向队列放数据时,如果有线程在等待数据就唤醒等待线程。
异步队列主要代码剖析:
异步队列的数据结构如下:
struct _GAsyncQueue
{
GMutex *mutex; //互斥变量
GCond *cond; //等待条件
GQueue *queue; //数据队列
guint waiting_threads; //等待的读线程个数
gint32 ref_count;
};
g_async_queue_push_unlocked (GAsyncQueue* queue, gpointer data)
{
………//这些点代表一些省略的代码
//把数据放入队列
g_queue_push_head (queue->queue, data);
//现在队列已经有数据了,判断是否有读线程在等待数据,
//如果有就发送信号唤醒读线程
if (queue->waiting_threads > 0)
    g_cond_signal (queue->cond);
}
g_async_queue_push (GAsyncQueue* queue, gpointer data)
{
……..
g_mutex_lock (queue->mutex); //在访问临界区前先获得互斥变量
g_async_queue_push_unlocked (queue, data); //执行写数据操作
g_mutex_unlock (queue->mutex); //释放互斥变量,以使其它线程可以进入临界区
}
从以上的接口可看出,”…._ unlocked” 这样的接口就是异步队列这个对象已获得互斥变量的接口,glib中线程处理相关接口都有类似的命名规则,在接下来的代码分析中,如没有特别的需要就只看”…._ unlocked” 这样的接口。
// 读线程从异步队列中获取数据的接口
// try参数和时间参数在多线程同步/内核多进程的实现中是很常见的东西了,在这里就不再作特殊的解释了。
g_async_queue_pop_intern_unlocked (GAsyncQueue *queue,
                               gboolean     try,
                               GTimeVal    *end_time)
{
gpointer retval;
//判断是否有数据在队列中,如果没有就要执行if语句相应的睡眠等待,直到被写进程唤醒
if (!g_queue_peek_tail_link (queue->queue))
    {
      if (try)//如果try为真,则永远不睡眠
       return NULL;
     
    // 接下来是要让线程进行睡眠等待了,在等待之前先确保等待条件已创建
      if (!queue->cond)
       queue->cond = g_cond_new ();
      if (!end_time) // 等待无时间限制
        {
          queue->waiting_threads++; // 等待线程数加一
      // 这里为什么用循环?因为这是多线程的环境,有可能有多个读线程在等待
      // 当前线程被唤醒时,有可能数据队列中的数据又被别的线程读走了,所以
      // 当前线程就得继续睡眠等待
      // 注意:睡眠等待时会暂时放弃互斥锁,被唤醒时会重新获取互斥锁
       while (!g_queue_peek_tail_link (queue->queue))
            g_cond_wait (queue->cond, queue->mutex);
          queue->waiting_threads--; // 等待线程数减一
        }
      else
        {
          queue->waiting_threads++;
          while (!g_queue_peek_tail_link (queue->queue))
           if (!g_cond_timed_wait (queue->cond, queue->mutex, end_time))
              break;
          queue->waiting_threads--;
          if (!g_queue_peek_tail_link (queue->queue))
           return NULL;
        }
    }
retval = g_queue_pop_tail (queue->queue);
g_assert (retval);
return retval;
}
/* 返回数据队列的长度,也即数据队列中的数据个数.
* 如果是负值表明是等待数据的线程个数,正数表示数据队列的数据个数
* g_async_queue_length == 0 表示是有 'n' 个数据和' n' 个等待线程在数据队列
* 这种特殊情况可能是在对数据队列加锁或调度时发生
*/
g_async_queue_length_unlocked (GAsyncQueue* queue)
{
g_return_val_if_fail (queue, 0);
g_return_val_if_fail (g_atomic_int_get (&queue->ref_count) > 0, 0);
return queue->queue->length - queue->waiting_threads;
}
  
有 了前面的异步队列基础就可以分析线程池是怎么实现的了。在glib库中的线程池的实现和使用有两种方式:1. 单个线程池对象不共享方式;2. 多个线程池对象共享线程方式,也即把各个具体的线程池对象创建的把任务做完了的线程统一放在全局线程池中进行统一管理,各个具体的线程池对象要使用线程 时,可以先向全局线程池中取线程,如果全局线程池没有线程了具体的线程池对象就可自行创建线程。
       线程池的数据结构有两部分,一部分是在头文件中,另一部分在C文件中。这是C语言中常用的信息隐藏方法之一,把要暴露给用户的数据放在头文件中,而要隐藏的数据则放在C文件中。下面是线程池头文件中的数据结构:
typedef struct _GThreadPool     GThreadPool;
struct _GThreadPool
{
   // 具体处理数据的函数
   // 它的第一个参数为g_thread_pool_push进去的数据,也即要执行的任务
GFunc func;
gpointer user_data; // func的第二个参数
// 通过这个成员控制线程池对象创建的线程是否在全局线程池中共享,
// TRUE为不共享,FALSE为共享
gboolean exclusive;
};
C文件中线程池的数据结构:
typedef struct _GRealThreadPool GRealThreadPool;
struct _GRealThreadPool
{
GThreadPool pool; // 头文件已定义
GAsyncQueue* queue; // 异步数据队列
GCond* cond;
gint max_threads; // 线程池对象持有的线程数上限
gint num_threads;// 池程池对象当前持有的线程数
gboolean running;
gboolean immediate;
gboolean waiting;
GCompareDataFunc sort_func;
gpointer sort_user_data;
};
我们可以先来分析单个线程对象不共享的主要实现。在分析它的实现之前,可以先看看一个流程图
从上图可见当主线程有数据交给线程池处理时,只要调用异步队列相关的push接口,线程池中的任何一个线程都可以为这服务。根据以上的流程图看看单个线程对象不共享方式的主要实现代码,它的调用从创建线程池对象开始:
g_thread_pool_new--->g_thread_pool_start_thread--->g_thread_create(g_thread_pool_thread_proxy,pool,FALSE,&local_error)--->>g_thread_pool_wait_for_new_task(pool)----> g_async_queue_pop_unlocked (pool->queue);
// max_threads为 -1 时表示线程池中的线程数无限制并且线程由动态生成
// max_threads为正整数时,线程池就会预先创建max_threads个线程
g_thread_pool_new (GFunc             func,
                 gpointer         user_data,
                 gint             max_threads,
                 gboolean         exclusive,
                 GError         **error)
{
GRealThreadPool *retval;
    ……………. //这些点代表一些省略的代码
retval = g_new (GRealThreadPool, 1);
retval->pool.func = func;
retval->pool.user_data = user_data;
retval->pool.exclusive = exclusive;
retval->queue = g_async_queue_new (); // 创建异步队列
retval->cond = NULL;
retval->max_threads = max_threads;
retval->num_threads = 0;
retval->running = TRUE;
    …………….
if (retval->pool.exclusive)
{
      g_async_queue_lock (retval->queue);
      while (retval->num_threads < retval->max_threads)
          {
             GError *local_error = NULL;
             g_thread_pool_start_thread (retval, &local_error);//起动新的线程
             …………….
       }
      g_async_queue_unlock (retval->queue);
}
return (GThreadPool*) retval;
}
g_thread_pool_start_thread (GRealThreadPool *pool,
                         GError          **error)
{
gboolean success = FALSE;
if (pool->num_threads >= pool->max_threads && pool->max_threads != -1)
    /* Enough threads are already running */
    return;
…………….
if (!success)
{
      GError *local_error = NULL;
      /* No thread was found, we have to start a new one */
      // 真正创建一个新的线程
      g_thread_create (g_thread_pool_thread_proxy, pool, FALSE, &local_error);
      ……………….
}
pool->num_threads++;
}
g_thread_pool_thread_proxy (gpointer data)
{
GRealThreadPool *pool;
pool = data;
……………..
g_async_queue_lock (pool->queue);
while (TRUE)
{
      gpointer task;
      // 线程等待任务,也即等待数据,线程在等待就是处在线程池中的空闲线程
      task = g_thread_pool_wait_for_new_task (pool);
      // 如果线程被唤醒收到并数据就用此线程执行任务,否则继续循环等待
      // 注意:当任务做完时,继续循环又会调用上面的g_thread_pool_wait_for_new_task
      // 而进入等待状态,
if (task)
       {
             if (pool->running || !pool->immediate)
              {
                /* A task was received and the thread pool is active, so
              * execute the function.
              */
                g_async_queue_unlock (pool->queue);
                pool->pool.func (task, pool->pool.user_data);
                g_async_queue_lock (pool->queue);
           }
       }
      else
       {
            ………………
      }
}
return NULL;
}
g_thread_pool_wait_for_new_task (GRealThreadPool *pool)
{
gpointer task = NULL;
if (pool->running || (!pool->immediate &&
                     g_async_queue_length_unlocked (pool->queue) > 0))
{
      /* This thread pool is still active. */
      if (pool->num_threads > pool->max_threads && pool->max_threads != -1)
       {
           …………..
       }
     else if (pool->pool.exclusive)
       {
           /* Exclusive threads stay attached to the pool. */
        // 调用异步队列的pop接口进入等待状态,到此一个线程的创建过程就完成了
           task = g_async_queue_pop_unlocked (pool->queue);
       }
      else
       {
           ………….
       }
}
else
{
     …………
}
return task;
}

现在可以结合流程图分析线程池中创建一个线程的一个情景:从函数g_thread_pool_new的while循环调用了 g_thread_pool_start_thread函数,在函数中直接调用g_thread_create创建线程,被创建的线程调用函数 g_thread_pool_wait_for_new_task循环等待任务的到来,函数 g_thread_pool_wait_for_new_task调用g_async_queue_pop_unlocked (pool->queue)真正进入等待。如此可知,最终新创建的线程是调用异步队列的pop接口进入等待状态的,这样一个线程的创建就大功告成 了。而函数g_thread_pool_new的while循环结束时就创建了max_threads个等待线程,也即这个新建的线程池对象有了 max_threads个线程以备使用。

       创建线程池、线程池中的线程是为了使用它,在线程池中取线程,叫线程干活的过程就很简单多了,这个调用过程:g_thread_pool_push--à g_thread_pool_queue_push_unlocked--à g_async_queue_push_unlocked。可见最终调用的是异步数据队列的push接口,把要处理的数据插入队列后它就会唤醒等待异步队列数据的等待线程。

g_thread_pool_push (GThreadPool *pool,
                  gpointer      data,
                  GError      **error)
{
……………
//
if (g_async_queue_length_unlocked (real->queue) >= 0)
    /* No thread is waiting in the queue */
    g_thread_pool_start_thread (real, error);
g_thread_pool_queue_push_unlocked (real, data);
g_async_queue_unlock (real->queue);
}
g_thread_pool_queue_push_unlocked (GRealThreadPool *pool,
                               gpointer         data)
{
   ………….
    g_async_queue_push_unlocked (pool->queue, data);
}

    总结:单个线程池对象不共享方式在管理多线程时是以线程池对象中的异步队列为中心,新创建的线程或做完任务的线程并不释放,让它调用异步队列的pop接口进入等待状态,而在使用唤醒线程池中的线程就是调用异步队列的push接口。

    以上对于理解线程池的实现已经足够,多个线程池对象共享线程方式和具体线程池的销毁的技巧,在这里就不讨论了。

GLib/Gstreamer代码浅析 阅读详情

相关推荐

GLib库介绍与linux下应用

GLib库介绍与linux下应用 GLib 是一个由 GNOME 项目组开发的高性能、跨平台 C 语言工具库,最初为 GTK 图形界面框架提供底层支持,后发展为独立的通用库。它通过封装常用数据结构和系统功能,显著提升了 C 语言开发的效率和可移植性。

热土程序园,利他愉己~ 1063

1_01_GLib库入门与实践_GLib库简介

GLib库是用C语言实现的一个通用的、可移植的实用程序库,由GNOME的GTK小组开发并维护,最早用在GTK+程序开发上,后来被越来越多的程序所引用。刚开始,GLib库叫GLib-1.0,形成于约1995到1997年间,彼时提供的功能还比较少,后来于2002年升级到GLib-2.0,提供的功能也越来越丰富。截止目前,GLib的最新版本已经是GLib-2.56.4(当前最新:GLib-2.71.0,目前仍在更新)。

field1003的博客 4616

GLib线程池全面解析:原理、应用与性能优化

GLib线程池核心解析:高效并发编程解决方案 GLib线程池通过GThreadPool结构提供了一套完整的并发编程框架,核心特性包括: 任务管理:基于异步队列实现任务提交(g_thread_pool_push)和智能调度 线程控制:支持独占/共享模式,动态创建线程并自动回收闲置资源 性能优化:内置负载均衡机制,通过工作窃取和动态扩展提升吞吐量 资源保护:可设置最大线程数,防止资源耗尽导致的系统崩溃 关键优势在于简化了传统的线程管理复杂性,开发者只需关注业务逻辑实现,同时获得高效的并发处理能力。适用于从I/O

数字人生 308

glib实践:原子操作

glib实践:原子操作

1111

标准恢复二极管整流器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告

报告页数: 150 图表数: 100 报告价格:¥16800 本文研究全球与中国市场标准恢复二极管整流器的发展现状及未来发展趋势,分别从生产和消费的角度分析标准恢复二极管整流器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。 主要生产商包括: Yangzhou Yangjie Vishay Intertechnology Power Semiconductors Inc Nexperia B

Fj69258的博客 5849

InfiniBand(IB)实战指南:从硬件配置到性能优化

本文是一份详尽的InfiniBand实战指南,涵盖从硬件选型、部署到驱动配置、性能优化的全流程。重点解析了InfiniBand作为AI训练与高性能计算(HPC)核心网络的优势,特别是其支持的RDMA技术如何实现微秒级低延迟。文章提供了硬件匹配、NCCL压测、内核参数调优及在PyTorch分布式训练中启用IB的实操步骤,旨在帮助工程师构建和优化高性能计算集群。

mqtt6iot的博客 581

glib库线程池代码分析

本文章主要讲了两部分内容:一是分析了异步队列的原理和实现,二是分析线程池的原理和实现。 在多线程程序的运行中,如果经常地创建和销毁执行过程相似而所用数据不同的线程,系统的效率,系统资源的利用率将会受到极大的影响。对于这一问题可用类似glib库中的线程池的解决办法。    我们可以这样想像线程池的处理,当有新的数据要交给线程处理时,主程序/主线程 就从线程池中找到一个未被使用的线程处理这

xb_0916的专栏 1382

【LPDDR4 - 学习二】LPDDR4 介绍 ——LPDDR4 芯片引脚说明解析

本文系统解析了LPDDR4内存芯片的引脚定义与特性,重点介绍了272球POPFBGA封装的双通道架构。内容涵盖电源管理(三级供电架构与上电时序)、控制信号(差分时钟与复用命令总线)、数据接口(16位双向数据总线与选通信号)以及校准复位等核心模块。通过表格详细列明了各引脚电压范围、功能定义及工程注意事项,并附有典型封装球位分布图。全文基于JEDEC标准,为移动设备内存设计提供全面的硬件参考,特别强调电源时序控制、信号完整性及抗干扰设计等关键点。

zap15622077817的博客 2641

GLib库入门与实践_目录

目录 GLib库入门与实践_目录 前言 0_00_GLib库入门与实践_前言 第一部分 1_00_GLib库入门与实践_概述序 1_01_GLib库入门与实践_GLib库简介 1_02_GLib库入门与实践_GLib库源代码目录结构简介 1_03_GLib库入门与实践_GLib库帮助文档简介 1_04_GLib库入门与实践_GLib库的编译 1_05_GLib库入门与实践_第一个基于GLib库的程序 第二部分 2_00_GLib库入门与实践_基础序 2_01_GLib库入门与实践_GLib库版本与兼容性 2

field1003的博客 3494

基于MATLAB/Simulink的四旋翼无人机PID控制与仿真(3)

《基于MATLAB/Simulink的四旋翼无人机PID控制与仿真》的第三篇,将为大家继续展示并完善控制与仿真模型。本文将在前文的基础上,完成控制环节增补、参数设置、测试等工作,在不考虑性能(低超调、快、准等)的前提下,保证系统对于一般输入(阶跃、斜坡)的响应稳定性。

jlx142857的博客 9321

PP-OCRv6 ONNX部署的官方和手搓方案

PP-OCRv6 基于全新设计的 PPLCNetV4 统一骨干网络,提供 tiny、small、medium 三档模型,分别面向端侧/IoT、移动端/桌面端、服务端场景。在内部多场景综合评估集上,PP-OCRv6_medium 相比 PP-OCRv5_server 识别精度提升 5.1%、检测精度提升 4.6%,同时 GPU 推理速度提升 2.37×;以仅 34.5M 参数的规模,精度超越 Qwen3-VL-235B、GPT-5.5 等大型视觉语言模型。

懒人的技术笔记 1895

Glib库使用 (1)

GLib-2.0的GLib部分提供了许多有用的数据类型、宏、类型转换、字符串实用程序、文件实用程序、主循环抽象等等,具体包含:字符串操作、文件操作、数据校验、编解码、字符集转换、随机数生成器、命令行解析器、xml解析器、正则表达式、单链表、双链表、 数组、指针数组、双端队列、哈希表、平衡二叉树、N维树、泛型、主循环、多线程、线程池、异步队列、内存分配、内存片段、错误系统、测试框架等等一系列功能。

A152419的博客 2635

sift、surf、orb 特征提取——三维重建

参考文章:[sift、surf、orb 特征提取及最优特征点匹配] 最近刚刚入门三维重建,在SFM算法的代码中注意到了feature_type( feature extraction method)这个参数,想细致了解一下这几种特征提取算法,在这里做一下笔记。 SIFT 简介 SIFT(Scale-Invariant Feature Transform)——尺度不变特征变换,是一种计算机视觉的特征提取算法,用来侦测与描述图像中的局部特征。 实质上,它是在不同的尺度空间上查找关键点(特征点),并计算出关键点的

bb_sy_w的博客 4362

GLib学习

Gstreamer 基础 【学习博客】 一、glib glib介绍 1.1 类型介绍 glib的类型定义在gtypes.h文件中,关键定义如下: 1.1.1 不规则类型 gboolean gpointer gconstpointer gchar guchar 1.1.2 整型 gint G_MININT G_MAXINT guint G_MAXUINT gshort G_MINSHORT G_MAXSHORT gushort G_MAXUSHORT glong G_MINLONG G_MAXLONG g

weixin_43360707的博客 1991

论文阅读《Eigen-GNN: A Graph Structure Preserving Plug-in for GNNs》

好久没看论文了,感觉好生疏 关于这篇文章的大致脉络,这篇文章已经介绍过了。

且视他人之疑目如盏盏鬼火,大胆地去走你的夜路 461

共聚焦显微镜的光源、工作原理与选型

共聚焦显微镜作为科研与工业领域中重要的微观成像工具,凭借光源的精准适配性、光学切片技术的三维成像能力,以及对不同工业场景的定制化适配潜力,为半导体检测、工业部件分析、智能制造升级提供了关键技术支撑。深入理解其光源特性、工作原理与选型逻辑,能帮助工程师与技术人员精准匹配设备与生产需求,既保障微观检测的精度与效率,又为产业优化、技术创新筑牢微观观测的基础。

PhotonixBay的博客 1537

【亲测免费】 深入探索GLib库:C语言开发者的利器

深入探索GLib库:C语言开发者的利器 【下载地址】GLib库入门与实践配套源码 GLib库入门与实践配套源码 项目地址: https://gitcode.com/open-source-toolkit/93966 ...

gitblog_09761的博客 1304

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

0_00_GLib库入门与实践_前言

GLib库是用C语言实现的一个通用的、可移植的实用程序库,由GNOME的GTK小组开发并维护,最早用在GTK+程序开发上,后来被越来越多的程序使用。刚开始,GLib库叫GLib-1.0,形成于约1995到1997年间,彼时提供的功能还比较少,后来于2002年升级到GLib-2.0,提供的功能也越来越丰富。

field1003的博客 2451

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

[线程]glib库线程池代码分析

转载自: http://blog.youkuaiyun.com/sanlongcai/archive/2007/07/22/1701699.aspx   本文章主要讲了两部分内容:一是分析了异步队列的原理和实现,二是分析线程池的原理和实现。 在多线程程序的运行中,如果经常地创建和销毁执行过程相似而所用数据不同的线程,系统的效率,系统资源的利用率将会受到极大的影响。对于这一问题可用类似gli...

Linux Job 334

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

[线程]glib库线程池代码分析续

转载自: http://blog.youkuaiyun.com/sanlongcai/archive/2007/08/18/1749949.aspx        Glib-2.12.9中的线程池可以把它看作一个类,进程可以通过这个类创建多个线程池对象。每个线程池对象所创建的线程可以以私有方式使用,也可以以共享方式使用。私有方式是指具体某个线程池对象所创建管理的线程只能是它自己使用,同一进程的其它...

Linux Job 273

1_03_GLib库入门与实践_GLib库帮助文档简介

GLib官方帮助文档可以从GNOME官网在线预览,也可以从源码目录获取。 源码目录下的glib-2.56.4/docs/reference/glib/html为全部的帮助文档, 此文档使用gtk-doc生成。里面有一个inde.html,是为索引页,通过索引页可以很方便地跳转到其他页面。

field1003的博客 1404

5_00_GLib库入门与实践_核心应用支持序

本部分是GLib的核心应用支持部分,包括十章内容。 5_01章介绍了错误系统GError、告警和断言。 5_02章介绍了GLib的一套日志输出接口。 5_03章介绍了线程、互斥锁、递归锁、读写锁、条件量、线程私有数据、线程一次性初始化及位锁相关的操作函数。 5_04章介绍了一个线程池。 5_05章介绍了带线程和锁的异步队列。 5_06章介绍了模块动态加载技术。 5_07章介绍了GLib的主循环。 5_08章介绍了一套通用的IO通道。 5_09章介绍了内存的申请及释放。 5_10章介绍了基于slab思想的内

field1003的博客 394
上一篇: linux环境变量
下一篇: 股票历史数据获取
sofia1217
博客等级 码龄17年 36粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值