python基础知识学习——多线程编程

本文详细介绍了多线程编程的基本概念、线程与进程的区别、全局解释器锁(GIL)的作用及工作原理,并深入探讨了Python中thread和threading模块的使用方法,包括线程同步机制、信号量、队列等高级特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.多线程编程

  • 在多线程编程出现之前,计算机程序的执行是由单个步骤序列组成的,该序列在主机的CPU中按照同步顺序执行的。
  • 而多线程编程就是将编程任务被划分成多个执行流,其中每个执行流都有一个指定要完成的任务。根据应用的不同,这些子任务可能需要计算出中间结果,然后合并成最终的输出结果。
  • 引入进程的目的:操作系统为何要引入进程这个概念,这要从多批道处理系统说起。为了提高CPU利用率,多批道处理系统一次性载入多个作业到内存中让程序并发执行,但这会造成一系列的问题。
  • 引入线程的目的:为了更好地利用CPU的资源,如果只有一个线程,我们有多个任务的时候必须等着上一个任务完成才能进行。多线程则不用等待可在主线程执行的同时执行其他任务。 在单核CPU系统中,每个线程运行一会儿,然后让步给其他线程(再次排队等待更多的CPU时间)

1>线程与进程的区别:

  • 进程是一个执行中的程序。每个程序都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。进程也可以派生新的进程来执行其他任务。
  • 线程是共享拥有相同的上下文,在同一个主进程或“主进程”中并行运行的一些“迷你进程”。
  • 一个进程中的各个线程与主线程共享同一片数据空间。
  • (计算机程序只是存储在磁盘上的可执行的二进制文件,只有将它们加载到内存中并被操作系统调用,才具有生命周期)

2>并行与并发:

如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。
如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。
关键的一点是 两个或多个任务能否在同一时刻进行执行。

2.全局解释器锁(GIL)

  • python在设计的时候,出于安全的考虑,在主循环中同时只能有一个控制线程在执行,就像单核CPU系统的多进程一样。换句话说,python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。
  • 全局解释器锁就是用来保证同时只能有一个线程执行。

GIL执行步骤:

  1. 设置GIL
  2. 切换一个线程运行
  3. 执行下面两个操作中的一个:
    a. 指定数量的字节码指令
    b. 线程主动让出控制权(可以调用time.sleep(0)来完成)
  4. 把线程设置回睡眠状态(切换出线程)
  5. 解锁GIL
  6. 重复上述步骤

3.线程模块:

  • Python提供了多个模块来支持多线程编程,包括thread、threading和Queue模块等等。我们写的程序可以使用thread和threading模块来创建和管理线程;而使用Queue模块,用户可以创建一个队列数据模块,用于多线程之间进行共享。

threading模块的对象:

对象描述
Thread通过threading.Thread()可派生一个执行线程的对象
Lock(锁原语对象,也称同步锁)为了避免多线程同时进入临界资源区获取和操作共有资源时,会出现资源的争夺而出现混乱。
RLock(递归锁)使单一线程可以再次获得已持有的锁,可以解决死锁问题
Event(同步对象)程序中的其一个线程需要通过判断某个线程的状态来确定自己下一步的操作
Semaphore为线程共享的优先资源提供一个“计数器”,如果没有可用资源时会被阻塞
BoundedSemaphore与Semaphore相似,但是它不允许超过初始值

4._thread模块

  • _thread模块的核心函数start_new_thread()。该函数的作用是派生一个新的线程,它的参数包括函数(对象)、函数的参数以及可选的关键字参数

  • _thread模块还提供了基本的同步数据结构,锁对象(LockType)

      示例1:使用thread模块
    
import _thread
import time

def loop1():
    print("start loop1 at:", time.ctime())
    time.sleep(2)
    print("stop loop1 at:", time.ctime())

def loop2():
    print("start loop2 at:", time.ctime())
    time.sleep(4)
    print("stop loop2 at:", time.ctime())

print("starting at :",time.ctime())
_thread.start_new_thread(loop1,())    #start_new_thread()必须包含两个参数。传入的第一个参数为函数名,不加括号。
_thread.start_new_thread(loop2,())    #因为要执行的函数不需要参数,也需要传递一个空元组。
time.sleep(6)
print("stopping at :",time.ctime())
---->输出结果所示:
starting at : Fri Jul 19 14:48:05 2019
start loop1 at: Fri Jul 19 14:48:05 2019
start loop2 at: Fri Jul 19 14:48:05 2019
stop loop1 at: Fri Jul 19 14:48:07 2019
stop loop2 at: Fri Jul 19 14:48:09 2019
stopping at : Fri Jul 19 14:48:11 2019
  • 使用线程能够提高程序执行的效率,将原有的12秒缩短到了6秒。

  • 注意点:

    1. start_new_thread()必须包含两个参数。传入的第一个参数为函数名,不加括号。
    2. 因为要执行的函数不需要参数,也需要传递一个空元组
  • 在我们应用的过程中,我们应该尽量避免使用_thread,而尽量使用threading。因为_thread对进程何时退出没有做控制。当主线程结束时,所有的其他线程也会强制结束,不会发出警告或者进行适当的清理。可以理解为,_thread派生的线程都是守护线程。而threading模块可以确保在所有的重要的子线程在进程退出前结束。

      示例2:
    
import _thread
import time

def loop1():
    print("start loop1 at:", time.ctime())
    time.sleep(2)
    print("stop loop1 at:", time.ctime())

def loop2():
    print("start loop2 at:", time.ctime())
    time.sleep(4)
    print("stop loop2 at:", time.ctime())

print("starting at :",time.ctime())
_thread.start_new_thread(loop1,())  
_thread.start_new_thread(loop2,())    
print("stopping at :",time.ctime())
---->输出结果所示:
starting at : Fri Jul 19 14:47:23 2019
stopping at : Fri Jul 19 14:47:23 2019
  • 我们可以发现,主线程派生出来的子线程并没有被执行,主进程结束后,子进程也被强制退出了。

5.threading模块

接下来我们就重点介绍threading模块了,threading模块除了Thread类以外还包括许多非常好用的同步机制,如第3点线程模块列出来的表格,那是比较常使用的类对象。

threading模块的其他函数

函数描述
active_count()当前活动的Thread对象个数
current_thread()返回当前的Thread对象
enumerate()返回当前活动的Thread对象列表
settrace(func)为所有的线程设置一个trace函数
setprofile(func)为所有的线程设置一个profile函数
stack_size(size = 0)返回新创建线程的栈大小;或为后续创建的线程设定栈的大小为size

1.守护线程

  • 正如上一节说的,_thread模块不具有守护线程这个概念,当主进程退出时,所有子进程都会强制退出,当然在我们程序设计的过程中不会希望出现这一点,子线程是否退出,应该是由我们自己控制的。

  • 守护线程就是如果主线程准备退出时,不需要等待某些子线程的完成,就可以为这些子线程设置守护线程标记,如果标记为真时,表示该线程不重要,可以不许等待该线程的执行

  • 一个新的子线程会继承父线程的守护标记,主线程会在所有非守护线程退出后才会退出。

  • 设置守护线程执行的赋值语句: thread.setDaemon(True), 必须在线程start()之前调用

      示例3:守护线程
      任务需求:将子线程2设置为守护线程,主线程不需要等待子线程2
    
import threading
from time import ctime,sleep


def ListenMusic(name):

        print ("Begin listening to %s. %s" %(name,ctime()))
        sleep(3)
        print("end listening %s"%ctime())


def RecordBlog(title):

        print ("Begin recording the %s! %s" %(title,ctime()))
        sleep(5)
        print('end recording %s'%ctime())

threads = []

t1 = threading.Thread(target=ListenMusic,args=('水手',))
t2 = threading.Thread(target=RecordBlog,args=('python线程',))

threads.append(t1)
threads.append(t2)

if __name__ == '__main__':
    t2.setDaemon(True)

    for t in threads:
        #t.setDaemon(True) #注意:一定在start之前设置
        t.start()
        print(t.getName())#返回线程名
       
---->输出结果所示:
Begin listening to 水手. Fri Jul 19 15:05:14 2019
Thread-1
Begin recording the python线程! Fri Jul 19 15:05:14 2019
Thread-2
end listening Fri Jul 19 15:05:17 2019

2.同步锁

  • 为了维护线程安全,线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入同步锁。

  • 同步锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。

  • 同步锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。但这种锁因为功能比较简单,如果使用不恰当将会产生死锁

      示例4:为加同步锁
      任务需求:定义一个sum为100,递减100次,每次递减1
    
#加入I/O拥塞后,即加入time.sleep()
import threading
import time

#调用函数,每次自减1的操作
def sub():
    global num
    # num -= 1
    temp = num
    time.sleep(0.0010)
    num = temp -1

num  = 100
l = []  #该列表存储的是100个线程

for i in range(100):
    t = threading.Thread(target=sub)
    t.start()
    l.append(t)

for t in l:
    t.join()
print(num)
---->输出结果所示:
88
  • 当我们没加入线程的时候,得到的就是我们意料之内的答案0,而加入线程后,答案就不是唯一的。其实是这样的 ,所有的线程共享一份是数据,num=100,然后这100个线程中,会有其中一个线程申请到了GIL Lock,那么它就回到CPU中执行操作。每个线程的操作都是这三步走,将num赋值给temp,睡眠0.001秒,再将temp-1的结果赋值给num。如果CPU来不及操作第三步的话,就会被回收GIL,让步给其他线程。那么下一个线程此时的num拿到的值就是100了。

  • 所以说 如果time.sleep()的时间足够长的话,每一个线程拿到的num都会是1,那么执行的结果必然会是100。

  • 因为所有线程拿的都是同一份数据,那么我们就应该对设一份数据进行控制,每当一个线程进来后,都要限制其他线程的进入,这就用到了同步锁了。

      示例5:同步锁
      任务需求:定义一个sum为100,递减100次,每次递减1
    
import threading
import time

def sub():
    time.sleep(1)
    global num
    locks.acquire()		#没当一个线程进来了就锁住,只有该线程执行完了释放,其他线程才能获取该锁
    temp = num
    time.sleep(0.01)
    num = temp -1
    locks.release()

num  = 100
l = []
locks = threading.Lock()

for i in range(100):
    t = threading.Thread(target=sub)
    t.start()
    l.append(t)

for t in l:
    t.join()
print(num)

  • 从结果看上去,我们好像从并行的执行变成了串行执行。其实这并没有的,只有函数运行到同步锁和被释放锁这一段时,才是串行运行。如果我们在设锁之间加入一条time.sleep(1),如果时串行执行的话,那么将会在101秒后才结束函数的运行,然而实际上只花了2秒时间,说明还是并行运行程序的。

3.递归锁

  • 死锁的概念:
    死锁是指一个资源被多次调用,而多次调用方都未能释放该资源就会造成一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

  • 递归锁:
    递归锁就是使单一线程可以再次获得以持有的锁。

      示例6:递归锁的引入
    
import threading
import time
class MyThreading(threading.Thread):
    def run(self):
        self.actionA()
        # time.sleep(0.5)
        self.actionB()

    def actionA(self):
        A.acquire()
        print(self.name,"actionA gotA",time.ctime())
        time.sleep(2)
        B.acquire()
        print(self.name,"actionA gotB",time.ctime())
        time.sleep(1)
        B.release()
        A.release()

    def actionB(self):
        B.acquire()
        print(self.name, "actionB gotB", time.ctime())
        time.sleep(2)
        A.acquire()
        print(self.name, "actionB gotA", time.ctime())
        time.sleep(1)
        A.release()
        B.release()


if __name__ == '__main__':

    A = threading.Lock()
    B = threading.Lock()
    l = []

    for t in range(3):
        t = MyThreading()
        t.start()
        l.append(t)

    for i in l:
        i.join()

    print("All done",time.ctime())
---->输出结果所示:
Thread-1 actionA gotA Fri Jul 19 15:39:13 2019
Thread-1 actionA gotB Fri Jul 19 15:39:15 2019
Thread-1 actionB gotB Fri Jul 19 15:39:16 2019
Thread-2 actionA gotA Fri Jul 19 15:39:16 2019
  • 在上述例子中,我们进行逐行解释:
    • 自定义线程子类,当派生线程时,自动调用actionA和actionB方法,A和B为后面定义的锁变量名,

    • 9-27行: actionA的功能是线程获取一个互斥锁A
      #打印获得该锁的时间,睡眠2秒,线程A再获得互斥锁B,打印时间,睡眠1秒后,释放B锁和A锁。此时线程

    • 30-44行:定义1个互斥锁A和互斥锁B,派生5个线程,执行run方法。
      #首先Thread-1首先执行actionA获得A锁,B锁,程序正常运行。当actionA运行结束后,A锁被释放了,此时其他4个线程都在竞争执行actionA,于此同时线程1执行actionB,线程1在24秒时刻等待锁A的释放,线程2在同一时刻等待锁B的释放,这就造成了双方都未能释放资源,互相等待。
      #通过现象我们可以发现,程序执行到中间时,调用方都未能释放资源,造成了互相等待的对象。

      示例7:递归锁的运用
      
import threading
import time
class MyThreading(threading.Thread):
    def run(self):
        self.actionA()
        # time.sleep(0.5)
        self.actionB()

    def actionA(self):
        r_lock.acquire()
        print(self.name,"actionA gotr_lock",time.ctime())
        time.sleep(2)
        r_lock.acquire()
        print(self.name,"actionA gotr_lock",time.ctime())
        time.sleep(1)
        r_lock.release()
        r_lock.release()

    def actionB(self):
        r_lock.acquire()
        print(self.name, "actionB gotr_lock", time.ctime())
        time.sleep(2)
        r_lock.acquire()
        print(self.name, "actionB gotr_lock", time.ctime())
        time.sleep(1)
        r_lock.release()
        r_lock.release()



if __name__ == '__main__':

    r_lock = threading.RLock()
    l = []

    for t in range(3):
        t = MyThreading()
        t.start()
        l.append(t)

    for i in l:
        i.join()

    print("All done",time.ctime())

---->输出结果所示:
Thread-1 actionA gotr_lock Fri Jul 19 15:46:12 2019
Thread-1 actionA gotr_lock Fri Jul 19 15:46:14 2019
Thread-2 actionA gotr_lock Fri Jul 19 15:46:15 2019
Thread-2 actionA gotr_lock Fri Jul 19 15:46:17 2019
Thread-3 actionA gotr_lock Fri Jul 19 15:46:18 2019
Thread-3 actionA gotr_lock Fri Jul 19 15:46:20 2019
Thread-1 actionB gotr_lock Fri Jul 19 15:46:21 2019
Thread-1 actionB gotr_lock Fri Jul 19 15:46:23 2019
Thread-2 actionB gotr_lock Fri Jul 19 15:46:24 2019
Thread-2 actionB gotr_lock Fri Jul 19 15:46:26 2019
Thread-3 actionB gotr_lock Fri Jul 19 15:46:27 2019
Thread-3 actionB gotr_lock Fri Jul 19 15:46:29 2019
All done Fri Jul 19 15:46:30 2019

  • 使用递归锁能够解决死锁的问题,因为RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,counter的初始值为0,每次调用acquire会使counter+1,release会使counter-1,如果counter大于1时,除了自身实例线程,其余线程都没资格使用该递归锁。直到一个线程所有的acquire都被release,其他的线程才能获得资源。所以使用RLock代替Lock,则不会发生死锁。

4.Event对象(同步对象)

  • 同步对象,即程序中的其一个线程需要通过判断某个线程的状态来确定自己下一步的操作

  • 通过使用threading.Event来创建一个Event对象。我们把这个Event传递到线程对象中;主要用于主线程控制其他线程的执行,事件主要提供了三个方法:wait、clear、set

  • 事件处理的机制:
    全局定义了一个Flag;
    如果Flag值为False(clear:将Flag设置为False),则执行event.wait方法时阻塞;
    如果Flag值为True(set:将Flag设置为True),则执行event.wait方法时不阻塞。
    注:
    event对象默认为False,即遇到event对象在等待就阻塞线程的执行。

      示例8:Event的运用
    
import threading
import time

class Work(threading.Thread):
    def run(self):
        event.wait()
        print("Works:啊啊啊啊。。。。。")
        time.sleep(2)
        event.clear()
        event.wait()
        print("Works:走,下班。")

class Boss(threading.Thread):
    def run(self):
        print("Boss:同志们,加班")
        # print(event.isSet())
        event.set()
        time.sleep(5)
        print("Boss:同志们,下班了")
        event.set()

if __name__ == '__main__':
    event = threading.Event()
    threads = []
    for i in range(3):
        threads.append(Work())  #派生5个Work类的线程,然后添加到列表中
    threads.append(Boss())
    for i in threads:
        i.start()
    for i in threads:
        i.join()

    print("All done")
---->输出结果所示:
Boss:同志们,加班
Works:啊啊啊啊。。。。。
Works:啊啊啊啊。。。。。
Works:啊啊啊啊。。。。。
Boss:同志们,下班了
Works:走,下班。
Works:走,下班。
Works:走,下班。
All done

  • 注意点:
  1. 调用event.set()之后,如果下次还需要使用wait方法时,必须对Flag进行清除,否者event.wait方法时不阻塞。
  2. event对象默认为False

5.信号量

  • 信号量的目的就是设定线程并发的数量,信号量的本质就是一个计数器,当资源消耗时递减,当资源释放时递增。我们可以认为信号量代表它们的资源可用还是不可用。

  • threading模块包括有两种信号量类:Semaphore和BoundedSemaphore。

  • BoundedSemaphore的一个额外功能就是防止其中信号量的释放次数多于获得次数的异常。

      示例9:信号量的运用
    
from atexit import register
from random import randrange
from threading import BoundedSemaphore, Lock, Thread
from time import sleep, ctime

lock = Lock()
MAX = 5
candytray = BoundedSemaphore(MAX)

def refill():
    lock.acquire()
    print("Refilling candy ...")
    try :
        candytray.release()
    except ValueError:
        print("full, skipping...")
    else:
        print("OK")
    lock.release()

def buy():
    lock.acquire()
    print("Buying candy...")
    if candytray.acquire(False):
        print("OK")
    else:
        print("empty, skipping")
    lock.release()

def producer(loops):
    for i in range(loops):
        refill()
        sleep(randrange(3))

def consumer(loops):
    for i in range(loops):
        buy()
        sleep(randrange(3))

def _main():
    print("starting at :", ctime())
    nloops = randrange(2, 6)
    print("The candy machine(full with %d bars)!" %MAX)
    t1 = Thread(target=consumer, args=(randrange(nloops,nloops+MAX+2),))
    t2 = Thread(target=producer, args=(nloops,))
    t1.start()
    t2.start()

@register
def _atexit():
    print("All done at ",ctime())

if __name__ == '__main__':
    _main()

在这个例子中,我们模拟了一个简化的糖果机,机器只有5个可用的槽存储糖果。如果槽满了,糖果就不能再加到机器;如果槽为空的,消费者就无法购买到糖果。我们用信号量来追踪资源,此处的资源就是糖果槽。

6.queue

队列是一种数据结构,数据存放方式类似于列表,但是取数据的方式不同于列表。

队列的数据有三种方式:

  1. 先进先出(FIFO),即哪个数据先存入,取数据的时候先取哪个数据,同生活中的排队买东西

  2. 先进后出(LIFO),同栈,即哪个数据最后存入的,取数据的时候先取,同生活中手枪的弹夹,子弹最后放入的先打出

  3. 优先级队列,即存入数据时候加入一个优先级,取数据的时候优先级最高的取出

     示例10:队列的引入
     任务需求:给出一个列表li = [1,2,3,4,5]依次将列表中的最后一个元素剔除
    
import threading
import time
li = [1,2,3,4,5]
def pri():
    while li:
        a = li[-1]
        print(a)
        time.sleep(1)
        li.remove(a)
        # try:
        #     li.remove(a)
        # except ValueError as e:
        #     print("")

t1 = threading.Thread(target=pri, args=())
t1.start()
t2 = threading.Thread(target=pri, args=())
t2.start()
t2.join()
print(li)

  • 我们可以发现,如果用线程的方式来处理列表时,会发生错误(ValueError: list.remove(x): x not in list),因为当一个线程对列表的数据进行删除了之后,列表就不再存在这一个数据,

  • 那么此时第二个列表拿到的便是一个不存在的数据,所以会报错。从而说明了列表是不安全的数据结果,所有的线程都有权利访问数据。

      示例11:线程队列queue.Queue()
      任务需求:给出一个列表li = [1,2,3,4,5]依次将列表中的最后一个元素剔除
    
import queue

q = queue.Queue()
q.put(12)
q.put("hello")
q.put({"name":"baidu"})

while True:
    data = q.get()
    print(data)
    print("-----------")
---->输出结果所示:
12
-----------
hello
-----------
{'name': 'baidu'}
-----------

线程队列queue模块
queue常用类描述
Queue(naxsize = 0)创建一个先入先出队列。如果给定最大值,则在队列没有空间时阻塞,否者为无限队列
LifoQueue(naxsize = 0创建一个后入先出队列。如果给定最大值,则在队列没有空间时阻塞,否者为无限队列
PriorityQueue(naxsize = 0创建一个优先级队列。如果给定最大值,则在队列没有空间时阻塞,否者为无限队列
Queue对象方法描述
qsize()返回一个队列已有数据的大小
empty()如果队列为空,则返回True;否则,返回False
full()如果队列为满,则返回True;否则,返回False
put(item, block = True, timeout = None)将item放入队列。如果block为True(默认)且timeout为None,则在有可用空间之前阻塞;如果timeout为正值,则最多阻塞timeout秒;如果block为False,则抛出Empty异常。
putnowait()等价于put(item, False)
get(block = True, timeout = None)在队列中取得元素。如果给定的block(非0),则一直阻塞到有可用的元素为止。如果block为False且队列无数据,则抛出Empty异常
get_nowait()等价于get(False)
task_done()用于表示队列中的某个元素已执行完成,该信号会被join()接收
join()在队列中所有元素执行完毕并调用上面的task_done()信号之前,保持阻塞

7.生产者-消费者问题

  • 该生产者-消费者问题的实现使用了Queue对象,以及随机生产(或消费)的商品数量。生产者和消费者独立并且并发的执行线程。

      示例12:生产者-消费者模型
    
import time, random
import queue, threading

q = queue.Queue()

def Producer(name):
    count = 0
    while count<10:
        print("making......")
        time.sleep(random.randrange(4))
        q.put(count)
        print("produer %s has produced %s baozi..."%(name, count))
        count += 1

        print("ok")

def Consumer(name):
    count = 0
    while count <5:
        time.sleep(random.randrange(4))
        if not q.empty():
            data = q.get()
            time.sleep(4)
            print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' % (name, data))
        else:
            print("-----no baozi anymore----")
            count +=1

p1 = threading.Thread(target=Producer, args=('A君',))
c1 = threading.Thread(target=Consumer, args=('B君',))
c2 = threading.Thread(target=Consumer, args=('C君',))

p1.start()
c1.start()
c2.start()

  • 从本例中我们可以得出,对于一个要执行多个任务的程序,可以让每个任务使用单独的线程。相比于使用单线程执行所有的任务,这种设计方案更加的简洁。

6.总结

本篇简述了单线程进程是如何限制应用程序性能的,以及多线程程序的优点。总的来说,多线程是一个好东西。但是不是所有时候都适用的,由于Python的GIL限制,多线程更适合于I/O密集型应用,而不使用计算密集型应用。对于计算密集型的应用,我们应该为了实现更好的并发性,而选择使用多进程,以便让CPU的其他内核来执行。
下一篇博文,我们将简述进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值