常用lock-free编程

Hazard Pointer 改进设计——hazard version Hazard Version 1 lock free算法的内存回收问题 hazard pointer算是比较通用也比较实用的内存回收机制,但是也有缺点,这里先介绍hazard pointer的做法,然后介绍一种改进方案。 2 hazard pointer 按hazardpointer的做法,一个节点要区分以下两种状态 retired:这个节点被删掉。在retire之后开始的操作 阅读详情

RCU(COW)

RCU 的关键思想有两个:1)COW,复制后更新;2)延迟回收内存。典型的RCU更新时序如下:

复制:将需要更新的数据复制到新内存地址;
更新:更新复制数据,这时候操作的新的内存地址;
替换:使用新内存地址指针替换旧数据内存地址指针(内存地址替换操作是原子的),此后原有读者访问旧数据,新的读者将访问新数据;
等待: 所有访问旧数据的读者进入静默期(QS),说明之前的reader都离开了临界区, 这个时候访问旧数据完成;
回收:当所有的CPU都至少经历过一次QS后, 意味着宽限期的结束,因此就进行回收处理工作;

基本概念

读临界区
RCU读者执行的区域,每一个临界区开始于rcu_read_lock(),结束于rcu_read_unlock(),读临界区中使用rcu_dereference()获取一个受RCU保护的指针
写临界区
写侧使用rcu_assign_pointer来对受RCU保护的指针赋新值, 推迟销毁被RCU保护的旧值
静默期
静默态(quiescent state): 当一个线程没有运行在读侧临界区时,其就处在静默状态。
宽限期
当所有的CPU都至少经历过一次QS后,宽限期将结束并触发回收工作。在不支持抢占的RCU实现中,检测到CPU有context切换,就能表明CPU离开了临界区,因此读者在持有rcu_read_lock()的时候,不能发生进程上下文切换

相关API

rcu_read_lock()  //标记读者临界区的开始
 rcu_read_unlock()  //标记读者临界区的结束
synchronize_rcu() / call_rcu() //等待Grace period结束后进行资源回收
rcu_assign_pointer()  //Updater使用这个宏对受RCU保护的指针进行赋值
rcu_dereference()  //Reader使用这个宏来获取受RCU保护的指针

COW+Reclaimer
在这里插入图片描述

Reader
使用rcu_read_lock和rcu_read_unlock来界定读者的临界区,访问受RCU保护的数据时,需要始终在该临界区域内访问;
在访问受保护的数据之前,需要使用rcu_dereference来获取RCU-protected指针;当使用不可抢占的RCU时,rcu_read_lock/rcu_read_unlock之间不能使用可以睡眠的代码;

Updater
多个Updater更新数据时,需要使用互斥机制进行保护;
Updater使用rcu_assign_pointer来移除旧的指针指向,指向更新后的临界资源;
Updater使用synchronize_rcu或call_rcu来启动Reclaimer,对旧的临界资源进行回收,其中synchronize_rcu表示同步等待回收,call_rcu表示异步回收;

Reclaimer
Reclaimer回收的是旧的临界资源;
为了确保没有读者正在访问要回收的临界资源,Reclaimer需要等待所有的读者退出临界区,这个等待的时间叫做宽限期(Grace Period);

具体例子

在这里插入图片描述

上图中,每一个读者、更新者表示一个独立的线程,总共4个读线程,一个写线程。

RCU更新操作分为两个阶段:移除阶段和回收阶段。两个阶段通过宽限期隔开。更新者在移除元素后,通过synchronize_rcu()原语,初始化一个宽限期,并等待宽限期结束后,回收移除的元素。

  1. 移除阶段:RCU更新通过rcu_assign_pointer()等函数移除或插入元素。现代CPU的指针操作都是原子的, rcu_assign_pointer()原语在大多数系统上编译为一个简单的指针赋值操作。移除的元素仅可被移除阶段(以灰色显示)前的读者访问。
  2. 回收阶段:一个宽限期后, 宽限期开始前的原有读者都完成读操作,因此,此阶段可安全释放由删除阶段删除的元素。

缺点

RCU不支持临界区阻塞,因为这个时候会发送CPU上下文切换.

RCU参考链接
rcu 机制简介
Linux RCU原理剖析(一)-初窥门径
Linux RCU原理剖析(二)-渐入佳境
RCU(Read Copy Update)十年计

Hazard Point(COW)

基本概念

所有读线程维持一个全局链表Hazard Pointer List,每个Hazard Pointer有一个成员flag标识其是否可用,一个指针ptr表示被保护的原始指针. 读的时候找到一个空闲节点(没有则新建一个)并将flag设置为true, ptr指向被保护的指针对象. 读完后flag设置为false,ptr置空. 通过CAS保证链表线程安全.

每个写线程维持一个thread-local的retire-list, 当retire-list达到一定阈值后触发内存回收,找出retire-list和Hazard Pointer List的补集然后将这些指针释放.

缺点

需要为每个共享变量维护一个线程的hazard pointer,对于有遍历需求的数据结构同时存在的hazard pointer很容易膨胀.

Hazard Point参考链接
Lock-free 编程:A Case Study(下)
并行编程中的内存回收Hazard Pointer

Hazard Version(COW)

基本概念

Hazard Version数据结构主要由三部分组成

  1. 全局的Current Version变量
  2. 每个线程局部一个Hazard Version变量
  3. 每个线程局部一个待回收的内存块链表Retire List

Hazard Version提供如下三个操作逻辑:

  1. 每个线程要开始访问“可能被其他线程释放”的内存块前,将当前Current Version的值保存在线程局部的Hazard Version中,对共享内存块的操作完成后,再清除线程局部的Hazard Version值;
  2. 要释放一个共享内存块时,原子的将Current Version加1后,将旧值保存在内存块中,然后将它挂在线程局部的RetireList上;
  3. 当待回收的内存块过多时,遍历所有线程的Hazard Version,以及全局的Current Version,获得最小值(MinVersion),遍历待回收的内存块,将Version小于Min Version的内存块回收掉(所有线程的Hazard Version都大于需要释放内存快的Hazard Version)。

缺点

管理多个线程Hazard Version的“滑动窗口”,太久不释放的Hazard Version,会阻塞内存回收。在使用时需要注意,对Hazard Version的require和release操作之间,不能有耗时过长的逻辑。

Hazard Version参考链接
[LockFree之美] 使用Hazard Version实现的无锁Stack与Queue

内存屏障

CPU缓存一致性(可见性)

缓存一致性通过MESI协议保证,指的是cpu的L1缓存(L1,L2独有,L3共享,再下面就是主存)。
MESI状态流转

Store Buffere—存储缓存

store buffer即存储缓存。位于内核和L1缓存之间。当处理器需要处理将计算结果写入在缓存中处于shared状态的数据时,需要通知其他内核将该缓存置为 Invalid(无效),引入store buffer后将不再需要处理器去等待其他内核的响应结果,只需要把修改的数据写到store buffer,通知其他内核,然后当前内核即可去执行其它指令。当收到其他内核的响应结果后,再把store buffer中的数据写回缓存,并修改状态为M。

Invalidate Queue—失效队列

处理器修改数据时,需要通知其它内核将该缓存中的数据置为Invalid(失效),我们将该数据放到了Store Buffere处理。那收到失效指令的这些内核会立即处理这种失效消息吗?答案是不会的,因为就算是一个内核缓存了该数据并不意味着马上要用,这些内核会将失效通知放到Invalidate Queue,然后快速返回Invalidate Acknowledge消息(意思就是尽量不耽误正在用这个数据的内核(Store Buffer那个核)正常工作)。后续收到失效通知的内核将会从该queue中逐个处理该命令。

指令重排(有序性)

只要没有依赖,代码中在后面的指令就可能跑到前面去,编译器和CPU都会这么做。不管指令如何重排,对单线程来说,结果必然是一致的, 即不会改变单线程程序的行为.

// 代码片段6
a = 1;
b = 2;
c = a + b;
  • 编译器/CPU/VM 可以对a = 1;和b = 2;进行对换,而不能将c = a + b与前面两句对换.
  • 对指定地址的操作(读写)序列,CPU是会保证和程序顺序一致的(比如a是先写后读),并且CPU的读写对自己总是可见的(Store Forwarding).
  • CPU不能解析多线程的依赖关系,可能会乱序执行,比如如果有其它线程依赖于a先于b赋值这个事实,那么就必须要应用程序告诉CPU/编译器,a和b有依赖关系,不要重排。

atomic

合理的指令重排在单线程环境下不会造成逻辑错误, 但多线程不能解析依赖关系可能会造成逻辑错误。memory order本身与多线程无关,通过限制单一线程当中指令执行顺序以及对cache可见性来解决多线程环境下出现的这个问题。以atomic操作为边界,根据对其之前的内存访问命令、之后的内存访问命令能够在多大的范围内自由重排形成6种内存序。内存序封装了内存屏障功能,保证有序性和可见性的顺序,解决了指令重排和cpu缓存一致性问题;atomic同时通过cas解决原子性问题。
六种内存序实际上对应三种内存模型,具体如下:
在这里插入图片描述
apache/incubator-brpc
memory_order_relaxed
除了保证操作的原子性之外,没有限定前后指令的顺序
acquire-release
主要包括memory_order_acquire(类似于mutex的lock), memory_order_release(类似于mutex的unlock), memory_order_consume(依赖型acquire),memory_order_acq_rel(acquire + release语意)。

  • memory_order_release: 原子操作之前的读写不能往后乱序;并且之前的写操作,会被使用acquire/consume的线程观察到。这里要注意它和seq_cst不同的是只有相关的线程(使用acquire或consume模式加载同一个共享变量的线程)能观察到,而seq_cst是所有seq_cst模式的线程(无论是否加载同一个变量)都能观察到。
  • memory_order_acquire: 原子操作之后的读写不能往前乱序;它能看到release线程在调用load之前的那些写操作。
  • memory_order_consume:原子操作之后的的依赖变量读写不能往前乱序;它可以看到release线程在调用load之前那些依赖变量的写操作。依赖是指和该原子变量有依赖关系的变量。
  • release/acquire的效率要比seq_cst高,常用于spinlock和读写锁的实现。

memory_order_seq_cst
该原子操作前后的读写不能跨过该操作乱序,该原子操作之前的写操作都能被所有memory_order_seq_cst线程观察到且具有唯一试图。
表象上看就是每个处理器的执行顺序和代码中的顺序一样;所有memory_order_seq_cst模式处理器都只能看到一个单一的操作执行顺序,就像单线程一样。

顺序一致性:几个线程观察到一个事件的时机可能不同,但一旦观察到,事件内部的顺序肯定是一致的。

如何理解 C++11 的六种 memory order?

Memory Barrier

内存屏障保证有序性和可见性的有序性。即栅栏后的操作不会在栅栏之前发生,且栅栏前的操作结果对栅栏后的操作必然是可见的。
可见性保证
比如原子变量A被读线程和写线程共享,此时写线程写A。内存屏障会让写线程将Store Buffer中的新数据flush到L1 cacheline(S——M),稍后会writeback回内存中;同时让读线程消费Invalidate Queue中的无效通知使L1中对应的cacheline无效(S——I), 接下来读线程读对应原子变量时会发生cachemiss,写线程会将L1 cacheline中的数据writeback回内存,读线程从主存中获取最新值,获取完后读线程和写线程的cacheline变为S状态。
有序性保证
屏障前的操作肯定在屏障后的操作之前完成。

CAS与ABA问题

  1. CAS是通过硬件以及cpu指令实现简单对象的原子操作(包括多线程写)。当修改的数据在同一个cache-line时通过MESI协议和内存屏障来保证,当修改的数据不在同一个cache-line通过锁总线来保证。
  2. CAS本身没有ABA问题,ABA问题的出现是因为对CAS的使用方式
  3. 对于CAS产生的ABA问题,通常的解决方案是采用CAS的一个变种DCAS。DCAS,是对于每一个V增加一个引用的表示修改次数的标记符。对于每个V,如果引用修改了一次,这个计数器就加1。然后再这个变量需要update的时候,就同时检查变量的值和计数器的值。

CAS实现无锁链表

其他

上篇|说说无锁(Lock-Free)编程那些事
下篇|说说无锁(Lock-Free)编程那些事

c++ 高并发队列的实现 JAVA如何进行CAS 讲到java的队列时,讲到java中的CAS操作 回顾下java中的cas,主要采用compareAndSet方法,如AtomicReference中所使用的: AtomicRefrence.java /** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * @param expect 阅读详情

相关推荐

C++并发实战19:lock free编程

涉及到并行/并发计算时,通常都会想到加锁,加锁可以保护共享的数据,不过也会存在一些问题: 1. 由于临界区无法并发运行,进入临界区就需要等待,加锁使得效率的降低。多核CPU也不能发挥全部马力 2. 在复杂的情况下,很容易造成死锁,并发进程、线程之间无止境的互相等待。 3. 在中断/信号处理函数中不能加锁,给并发处理带来困难。 4. 加锁影响实时性,等待时间不确定 5. 优先级反转,优先级

liuxuejiang158的专栏 5823

ABA问题

这篇文章(http://oceanbase.org.cn/?p=82)的第6小节讲述了Hazard Version的实现原理,它的设计思想最早由OB团队的席华锋提出,本文不再赘述,本文主要分享Hazard Version的实现要点,以及使用它实现无锁Stack与Queue的方法,已经在多核系统上的性能测试,代码已在github共享。 ABA问题 如《共享变量的并发读写》一文

linuxheik的专栏 1083

linux编程的108种奇淫巧计-7(Lock-free实验)

发现csdn把我一篇博客搞丢了,好在从百度快照(http://cache.baidu.com/c?m=9d78d513d99d1cfe01fa950e1a16a1711824d4236b80c7150e96c414cf2f051a143ab6af60624e0b89833a2516ae3a41f7a0682f621420c0ca89de16cabbe57478ce3a762740da0a4c884

pennyliang的专栏 5849

linux内核源码分析之RCU

目录 RCU概念 核心函数 实例一 实例二 实例三 RCU概念 RCU(Read Copy-Update),读-复制更新,是一种同步机制。它是在2.5开发过程中添加到Linux内核的一种同步机制,该机制针对以读为主的情况进行了优化。 读临界区:每个临界区开始与rcu_read_lock(),结束于rcu_read_unlock()。这些指针函数实现了依赖顺序加载的概念。 写临界区:推迟销毁并维护多个版本的数据结构,有同步开销。 RCU核心思想: 1)复制后更新;2)延迟

1643

C++:Lock-free;atomic;CAS;cmpxchg

libstdc+±v3/libsupc++/atomic_lockfree_defines.h /** * Lock-free property. * * 0 indicates that the types are never lock-free. * 1 indicates that the types are sometimes lock-free. * 2 indicates that the types are always lock-free. */ #if __cplusplus

mzhan017的博客 1494

ringbuf-可直接访问内部数据的无锁SPSC FIFO环形缓冲区-Rust开发

ringbuf无锁单生产者单消费者(SPSC)FIFO环形缓冲区,可直接访问内部数据。 概述RingBuffer是最初的结构ringbuf无锁单生产者单消费者(SPSC)FIFO环形缓冲区,可直接访问内部数据。 概述RingBuffer是代表环形缓冲区本身的初始结构。 环形缓冲区可以分为生产者和消费者两对。 生产者和消费者用于相应地将元素添加到环形缓冲区或从环形缓冲区中删除元素。 它们可以在线程之间安全地传输。 生产者和使用者的操作是无锁的-它们成功成功或立即失败而没有

free技术详解 lock_简化概念下的 lock-free 编程

1 lock-free 是什么常有人说 lock-free 就是不使用 mutex / semaphores 之类的 无锁(lock-Less) 编程,这句话严格来说并不对。从本质上讲,lock-free 描述的是代码逻辑的一种『属性』。不使用锁的代码大部分情况下具备这个『属性』,所以有时候会混淆 lock-free 和 无锁 两个概念,前者是对代码性质的描述,后者是说代码如何实现。那么,什么是 ...

weixin_42467445的博客 1054

别再只调电压了!深入理解数控电源的CC/CV模式与双闭环PID控制(以BUCK电路为例)

本文深入解析数控电源的CC/CV模式与双闭环PID控制技术,以BUCK电路为例,探讨如何通过双闭环控制优化负载突变响应和模式切换稳定性。文章详细介绍了电压环与电流环的协同工作原理、PID参数整定技巧及工程实践中的非线性补偿方法,帮助工程师提升电源设计的智能控制能力。

weixin_27017211的博客 261

free技术详解 lock_Lock-Free 编程

文章索引Lock-Free 编程是什么?当谈及 Lock-Free 编程时,我们常将其概念与 Mutex 或 Lock 联系在一起,描述要在编程中尽量少使用这些锁结构,降低线程间互相阻塞的机会,以提高应用程序的性能。类同的概念还有"Lockless" 和 "Non-Blocking" 等。实际上,这样的描述只涵盖了 Lock-Free 编程的一部分内容。本质上说,Lock-Free 编程仅描述了...

weixin_39765100的博客 463

麒麟银河操作系统V10部署ffmpeg(也能用于Linux系统)

部署ffmpeg用来处理视频的各种操作,如编码解码,格式转换,剪辑,处理实时视频流,音频流,加滤镜等

zhouwenxing666的博客 9035

Scrapy实现微博关键词爬虫(爬虫结果写入mongodb)

话不多说,上代码: spiders文件夹下的microBlogSpider.py里这样写: # -*- coding: utf-8 -*- import scrapy from scrapy import Spider, Request, FormRequest from scrapy.selector import Selector import datetime import random from blogSpider.items import microBlogItem, keyWordItem f

qq_39360565的博客 1779

lock free queue的实现

#ifndef __ARRAY_LOCK_FREE_QUEUE_IMPL_MULTIPLE_PRODUCER_H__ #define __ARRAY_LOCK_FREE_QUEUE_IMPL_MULTIPLE_PRODUCER_H__ #include "atomic_ops.h" #include <assert.h> // assert() #include <sched.h> // sched_yield() #include "logger_factory.h" #incl

wzq2009的专栏 627

【华为OD机试】真题E卷-一种字符串压缩表示的解压(C++)

有一种简易压缩算法:针对全部为小写英文字母组成的字符串,将其中连续超过两个相同字母的部分压缩为连续个数加该字母,其他部分保持原样不变。例如:字符串aaabbccccd 经过压缩变成字符串 3abb4cd请您编写解压函数,根据输入的字符串, 判断其是否为合法压缩过的字符串若输入合法则输出解压缩后的字符串 否则输出字符串!error来报告错误。

u014481728的博客 892

lock-free vs wait-free

文章目录为什么需要lock-free从底层去看lock-freeatomic指令的层级atomic操作wait-free 为什么需要lock-free 什么是lock-free?lock-free通常用于结构体中,此结构体经常被多个线程访问,假如处于blocking中,一个线程访问这个结构体那么其他的线程访问结构体都会被blocking住(ie mutex),假如正在访问这个结构体的线程(已经获得锁)因为某一些原因crash了,那么其他待访问这个结构的线程就被永远block住…这个时候我们就需要lock-f

qq_37026934的博客 1033

十五、【测试执行篇】异步与并发:使用 Celery 实现测试任务的后台执行与结果回调

本文介绍如何使用Celery实现测试任务的异步执行与结果回调,解决同步执行测试计划导致的请求阻塞问题。主要内容包括:Celery的基本概念与工作原理(任务队列、消息中间件、Worker进程),在Django项目中集成Celery的步骤,以及将同步测试逻辑改造为异步任务的方法。

blues_C的博客 707

生成自定义工具栏

我们生成了和VBA时代几乎一样的常用的工具栏。用WPF也可以完成以后的插件开发,界面控件编写基本靠自己绘画,还有用代码去控制,代码结构像HTML,做过网站的应该可以很快入手。其它后台C#代码除了对控件代码的控制外,和WinForm基本相同,但目前来看用WPF是生成自定义工具栏的较好的办法。当然,你也可以在工具栏命令中调用WinForm程序,对Coreldraw进行操作,这也应该是比较高效的办法,下次会讨论。资源的加入是本次调试的难点之一,我们的目标是让图片资源嵌入到程序中去,不必去电脑文件目录去调用;

力先生C#程序开发 1374

Lock-Free 编程

文章索引 Lock-Free 编程是什么? Lock-Free 编程技术 读改写原子操作(Atomic Read-Modify-Write Operations) Compare-And-Swap 循环(CAS Loops) ABA 问题(ABA Problem) 内存模型(Memory Model)对细粒度锁的影响 代码实践 实现普通的栈 Simpl...

aijianxie8808的博客 264

100天精通Python(爬虫篇)——第114天:爬虫基础模块之requests基础入门

一、request模块大总结 1. 下载安装 2. 常用属性或方法 3. response.text和response.content的区别: 4. 发送带headers参数请求 5. 发送带参数请求 6. 在headers参数中携带cookie 7. 超时参数timeout的使用 8. proxies代理参数的使用 9. 发送post请求...............

努力让自己发光,对的人才能迎着光而来 11万+

再说 lock-free 编程

lock-free 编程实在让人又爱又恨。博主以前曾经写过几篇关于 lock-free 编程的文章。比如关于无锁编程、并发数据结构:迷人的原子。如果想更加深入的了解和实践 lock-free 编程,可以参考CLR 2.0 Memory Model、并发数据结构:Stack。这篇文章并不打算继续阐述如何使用 lock-free 技术,而是谈一下它的负面影响。从而让大家对 lock-fre...

weixin_30825581的博客 200

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

lock-free介绍

无锁编程 / lock-free / 非阻塞同步 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。 实现非阻塞同步的方案称为“无锁编程算法”(Non-blocking algorithm)。 lock-free是目前最常见的无锁编程的实现级别(一共三种级别)。 为什么要 Non-blocking sync ? 使用lock实现线程同步有很多缺点: * 产生竞争时,.

chqj_163的专栏 684

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

test and set lock_简化概念下的 lock-free 编程

1 lock-free 是什么常有人说 lock-free 就是不使用 mutex / semaphores 之类的 无锁(lock-Less) 编程,这句话严格来说并不对。从本质上讲,lock-free 描述的是代码逻辑的一种『属性』。不使用锁的代码大部分情况下具备这个『属性』,所以有时候会混淆 lock-free 和 无锁 两个概念,前者是对代码性质的描述,后者是说代码如何实现。那么,什么是 ...

weixin_39900206的博客 602

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

lock-free编程系列

lock-free编程,讲究的是高效率,因此不能再使用锁,而是使用原子性操作。 而原子性操作的实现,可能有多种方式,比如,

FinixLei的专栏 (https://github.com/FinixLei) 598

lock free(无锁并发)是什么

一、非阻塞同步(Non-blocking Synchronization) 1. 无锁编程 / lock-free / 非阻塞同步 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。 实现非阻塞同步的方案称为“无锁编程算法”(Non-blocking algorithm)。 lock-free是目前最常见的无锁编程的实现级别(一共三种级别): wait-free l.

软件工程小施同学 的专栏 1892

非阻塞算法:关于lock-free和wait-free的一些思考

所谓lock-free和wait-free算法是指对于共享的数据并非对其加锁来控制访问,而是多个线程并行的访问。通过该算法可以达到对共享对象并发的读写而不会破坏对象本身。所谓lock-free是指对于线程不加锁,让系统执行所有的步骤。lock-free提到的不加锁是指不使用类似于互斥锁或者信号量之类的排他机制。因为一旦对线程加锁的话,当线程执行中断时,那么对于这个系统来说运行也中断了。所谓wait-free是指,不管其他线程执行什么操作,线程无论有什么操作都能在有限的步骤里面完成。所以对于算法来说达到loc

chqj_163的专栏 1292
上一篇: leetcode451. 根据字符出现频率排序
下一篇: 虚拟内存和物理内存
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值