Python最全多线程编程异步专题-精心典藏

本文详细介绍了Python中的多线程编程,包括进程与多进程、线程与多线程的概念、创建与使用,以及进程锁和进程间的通信。讨论了进程池的运用,如multiprocessing模块的Pool,以及异步编程的基础,如async/await关键字和asyncio模块。此外,还提到了gevent库作为异步编程的一种实现方式。
摘要由CSDN通过智能技术生成

前言:

精心典藏之作
能帮助到你是我的荣幸,点赞加收藏呀!

什么是多线程编程?

多线程编程是一种编程方式,它允许在单个程序中同时执行多个线程(或者称为子进程)。这些线程可以并发地执行,意味着它们可以在相同的时间段内独立地运行。

多线程编程可以帮助程序员实现一些特定的目标,例如:

提高程序的性能:将一个任务分解成多个子任务,每个子任务都由一个线程来执行,可以加快程序的执行速度。
改善程序的用户体验:通过在一个单独的线程中执行长时间运行的任务(例如读取大型文件或执行长时间计算),可以避免程序在执行这些任务时出现卡顿或无响应的情况。
简化程序设计:将一个复杂的程序分解成多个较小的模块,并将每个模块分配给不同的线程,可以使程序设计更加模块化和可读性更好。
然而,多线程编程也有一些挑战,例如线程同步问题和死锁问题等。因此,在进行多线程编程时,需要小心谨慎地考虑并发访问共享数据的方式。

进程与多进程

然后一个程序都要先有进程
进程:

  • 程序执行的载体
  • 从计算机中获得一定的cpu和内存(不同的程序获得大小不同)
    进程是指正在运行的程序实例。每个进程都有自己的内存空间和系统资源,并且独立于其他进程运行。在操作系统中,进程是系统资源的基本单位,每个进程都由操作系统分配一个唯一的进程标识符,称为进程ID。

多进程:

  • 由一个主进程创建出多个子进程(主进程就可以相当我们的系统,打开的软件在同时运行就相当于我们的子进程)
  • 子进程他们互不干扰,并行运行,提升运行速度
    多进程是指在操作系统中同时运行多个进程。多进程可以提高系统的效率,因为在处理多个任务时,可以同时利用多个CPU核心,以及其他系统资源。多进程也可以提高系统的稳定性,因为如果一个进程出现问题,其他进程不会受到影响。

线程与多线程

进程下面才是线程,进程是获得资源,线程才是执行主体
进程吃的内存和cpu太多了?,所以用线程?:对,应为使用线程更节省资源
线程:

  • 进程的一部分,运行的主体
  • 共享进程的cpu和内存

线程是指在进程内部执行的轻量级任务,它是进程的一部分,共享进程的地址空间和系统资源。每个线程有自己的程序计数器、栈和局部变量,但共享进程的内存、文件句柄、打开的网络连接等资源。

多线程:

  • 多线程里面的子进程是可以相互共享数据的
  • 比如浏览器是主进程,那么在浏览器打开的多个窗口就是子进程
  • 又比如说游戏的exe是主进程,而游戏音乐就是其中之一的子进程
    多线程是指在同一个进程内运行多个线程。多线程可以提高程序的效率和响应能力,因为线程之间可以共享进程的资源和数据,从而减少了资源的重复使用和数据的传输。此外,多线程也可以简化程序的设计,因为可以将程序拆分成多个独立的线程,每个线程负责不同的任务。
    如下图所示,也并不是真正的同时执行
    在这里插入图片描述
    与多进程相比,多线程具有以下优点:
    轻量级:线程的创建和切换比进程更快,因为线程共享进程的资源和地址空间。
    资源共享:线程之间可以共享进程的资源和数据,因此更适合于需要共享数据的任务。
    更容易实现同步:线程之间可以共享内存,因此更容易实现同步和互斥。

多进程的创建

进程创建模块multiprocessing

模块函数
在这里插入图片描述

Process 和 start

将普通和多进程运行一遍就知道区别了
普遍运行:

import os
import time
def work_a(n):
    for i in range(n):
        print(i, 'a', os.getpid())
        time.sleep(1)

def work_b(n):
    for i in range(n):
        print(i, 'b', os.getpid())
        time.sleep(1)
if __name__ == '__main__':
    work_a(5)
    work_b(5)

Process语法如下
multiprocessing.Process(target=func,args=*args)

import os
import time
import multiprocessing
def work_a(n):
    for i in range(n):
        print(i, 'a', os.getpid())
        time.sleep(1)

def work_b(n):
    for i in range(n):
        print(i, 'b', os.getpid())
        time.sleep(1)
if __name__ == '__main__':
    a_p = multiprocessing.Process(target=work_a,args=[5])  # args里用list传参就行了,不要只写个参数,或者用元组(5,)应为只有一个参数不要忘记“,”号
    a_p.start()
    b_p = multiprocessing.Process(target=work_b,args=[5])
    b_p.start()
# 进程多的话可以这样运行
# if __name__ == '__main__':
#     a_p = multiprocessing.Process(target=work_a,args=[5])  # args里用list传参就行了,不要只写个参数
#     b_p = multiprocessing.Process(target=work_b,args=[5])
#     for i in (a_p,b_p):
#    	 i.start()

join

具体来说,当一个进程通过创建子进程来执行一些任务时,父进程需要等待join了的子进程执行完毕后再继续执行自己的任务,
否则可能会导致不可预料的结果。
说白了就是阻塞主进程的运行。
比如我运行 a_p.join 那么主进程要等a_p这个进程运行完才会继续运行
看程序就知道了
没有join的程序

import os
import time
import multiprocessing
def work_a(n):
    for i in range(n):
        print(i, 'a', os.getpid())
        time.sleep(1)
if __name__ == '__main__':
   a_p = multiprocessing.Process(target=work_a,args=[5])
   a_p.start()
   print("Hello World")
'''
Hello World
0 a 3732
1 a 3732
2 a 3732
3 a 3732
4 a 3732
'''

join了的程序

import os
import time
import multiprocessing
def work_a(n):
    for i in range(n):
        print(i, 'a', os.getpid())
        time.sleep(1)
if __name__ == '__main__':
   a_p = multiprocessing.Process(target=work_a,args=[5])
   a_p.start()
   print("Hello World")
'''
0 a 21904
1 a 21904
2 a 21904
3 a 21904
4 a 21904
Hello World
'''

看完程序很明了啊!

kill

没什么讲的证明意思
kill() 方法用于向子进程发送终止信号,强制终止子进程的运行。一般来说,应当避免使用 kill() 方法,因为它可能会导致子进程无法正确地释放资源,甚至出现数据丢失等问题。在需要终止子进程时,可以通过向子进程发送信号的方式,让子进程自己处理退出逻辑,从而避免这种问题。

is_alive

字面意思
is_alive() 方法用于查询子进程的运行状态,返回值为布尔类型。如果子进程仍在运行中,则返回 True;否则返回 False。可以通过定期调用 is_alive() 方法来监控子进程的运行状态,从而及时处理子进程异常退出的情况。

多进程的问题

  • 通过进程运行的函数我们无法获得其函数的返回值
  • 多个进程修改文件可能出现错误
  • 多个进程共享资源的情况,因此可能会出现数据竞争、死锁等问题,导致程序不稳定。

进程池

什么是进程池?

进程池是一种进程管理方式,它可以预先创建一定数量的子进程,并将它们放入一个池中。当需要执行任务时,就从池中取出一个可用的子进程来执行任务。当任务执行完毕后,该子进程不会立即退出,而是返回到池中,等待下一次任务。

进程池来干什么?

进程池的主要作用是优化进程的创建和销毁。在创建进程时,会涉及到一些开销,如系统资源的分配和初始化等,这些开销会影响程序的性能。而使用进程池,则可以事先创建好一定数量的进程,避免了频繁的创建和销毁进程的开销,从而提高了程序的执行效率

什么时候用进程池?

进程池适用于需要频繁创建和销毁进程的情况,如多个任务需要并行执行、任务数量较大、每个任务的执行时间较短等。通常,在执行这类任务时,使用进程池能够有效地提高程序的性能,并且使得代码的实现更加简洁。

创建与使用进程池

multiprocessing.函数名()
在这里插入图片描述

Pool()

创建进程池

import multiprocessing
pool = multiprocessing.Pool(5) # 创建的进程池里有五个进程,就是能同时跑五个,剩下进来的要等空位

apply_async()是一种异步

将任务放入进程池

import multiprocessing
def work(n,b):
	for i in range(n):
		print("n")
	for i in range(b):
		print("b")
if __name__ == '__main__':
	pool = multiprocessing.Pool(5)
	pool.apply_async(func=work,args=(1,2)) # args是iterable类型

会发现什么都没有发生,为什么呢?
前面说过有主进程,主进程结束所有的一切子进程都会结束
如果我们在最后一行写一个time.sleep(10)会发现开始输出
执行多个任务放入进程池,并输出

if __name__ == '__main__':
	pool = multiprocessing.Pool(5)
	for i in range(30):
		pool.apply_async(func=work,args=(i,i))
	time.sleep(50)

join() 和 close()

前面我们虽然运行成了,但我们不可能总是sleep,说明我们要用阻塞join来阻止主函数来完成
在调用 pool.join() 方法之前,需要先调用 pool.close() 方法,否则会导致程序卡住。
原因:
调用 close() 方法时,会向进程池中的所有进程发送一个特殊的“sentinel”任务,这个任务告诉每个进程,它们不需要再接收新的任务了。当所有进程都完成自己的任务后,它们会尝试获取“sentinel”任务,但此时进程池已经关闭,没有新的任务了,因此所有进程都会顺利退出。
如果我们没有调用 close() 方法,那么进程池会一直保持打开状态,而进程池中的进程也会一直等待新的任务。这样就会导致程序卡住,无法正常退出。因此,在使用进程池时,一定要记得先调用 close() 方法,再调用 join() 方法。

if __name__ == '__main__':
	pool = multiprocessing.Pool(5)
	for i in range(30):
		pool.apply_async(func=work,args=(i,i))
	pool.close() # 关闭进程池,只能上面30个进程运行到结束,其他进程不能进入
	# 阻塞主进程运行,防止子进程还没有结束就关闭。
	# 进程池是一直运行的,如果先阻塞进程池,那么程序就永远不会结束,导致程序卡住
	pool.join()  				

记得前面的标题吗?apply_async()是一种异步,前面说进程不能获取返回值,而异步可以获取返回值
看下面如何获得返回值
pool.apply_async(func,args) 执行下来是一个对象AsyncResult
<multiprocessing.pool.ApplyResult object at 0x000001B5F56A5990>
这个对象有个get()函数能得到返回值

import multiprocessing
def work(n, b):
    for i in range(n):
        print("n")
    for i in range(b):
        print("b")
    return (n, b)

if __name__ == '__main__':
    pool = multiprocessing.Pool(5)
    re = []
    for i in range(30):
        result = pool.apply_async(func=work, args=(i, i))
        re.append(result) # 将所有AsyncResult的对象放入re中
    for i in re:
        print(i) # 这输出的是对象
        print(i.get()) # 通过该对象的get() 函数输出的是返回值(n,b)

仔细看这个程序,我们没有所有什么time.sleep() 或者close()和join(),但程序仍然可以很好的运行
使用 AsyncResult.get() 方法获取每个 AsyncResult 对象的结果,这个方法会自动阻塞等待进程完成并返回结果。因为在这个例子中程序中没有使用 close() 和 join() 方法,所以程序不需要等待进程池中的所有进程执行完成后再关闭和加入进程池, multiprocessing.Pool() 函数默认会自动完成这些操作。

进程锁

什么是进程锁

进程锁是一种同步机制,用于控制多个进程之间对共享资源的访问。它的作用是保证多个进程可以安全地访问共享资源,避免出现竞争条件和数据不一致的情况。

在多进程编程中,如果多个进程同时访问共享资源,可能会导致数据不一致、死锁等问题。因此,在访问共享资源之前,需要对共享资源进行加锁操作,以确保同一时刻只有一个进程可以访问共享资源,其他进程必须等待锁释放后才能继续访问。

怎么使用进程锁

  • 多个进程需要访问共享资源时,为了避免竞争条件和数据不一致的情况,需要对共享资源进行加锁。
  • 多个进程需要访问同一文件、同一数据库等需要互斥访问的资源时,需要使用进程锁来保证数据的完整性和正确性。
  • 多个进程需要协调完成某项任务,需要使用进程锁来确保各个进程按照预期顺序执行。

总之,当多个进程需要访问共享资源或者需要协同完成某项任务时,都需要使用进程锁来保证程序的正确性和稳定性。

进程锁的加锁与解锁

from multiprocessing import Manager
manage = Manager()
lock = manage.Lock()

在这里插入图片描述
锁可以直接放入函数的参数里面
如下面就是一个简单的锁:

import multiprocessing
import time
import os
from multiprocessing import Manager # 引入管理者

def work(i, lock):
    print("Hello")
    lock.acquire()  # 当有一个进程进入后就上锁,然后后面的进程都进不来了
    print(i, os.getpid())
    time.sleep(2)
    lock.release()  # 当进程走到这里,它又会开锁下一个进程又进来


if __name__ == '__main__':
    manage = Manager() # 实例化管理者
    lock = manage.Lock() # 给管理者实例化一把锁
    pool = multiprocessing.Pool(5)
    for i in range(10):
        pool.apply_async(func=work, args=(i,lock))
    pool.close()
    pool.join()

通过运行结果我们发现,5个大小的进程池,会先打印出5个Hello,然后后面都是一个一个的出现。
这看结果就说明很多问题了
最后在使用进程锁时,需要仔细考虑锁的使用方式和范围,并确保锁的正确性、重入性和并发性,避免死锁和优先级反转等问题。

进程间的通信

进程间通信(IPC)是指在不同进程之间传递信息的机制,其主要作用在于协调不同进程之间的操作,使它们能够相互合作完成一定的任务。
IPC 通常在以下情况下使用:

  • 进程协作:当一个应用程序需要启动多个进程来完成某个任务时,这些进程之间需要进行通信,以实现数据交换和共同完成任务。
  • 在并发编程中,多个线程或进程需要共享数据,为了避免数据的竞争和不一致性,需要使用 IPC 机制进行线程或进程之间的通信
  • 服务器和客户端应用程序:当一个服务器应用程序需要处理多个客户端请求时,需要使用 IPC 机制来进行服务器和客户端之间的通信,以实现数据交换和请求处理。
  • 分布式应用程序:当应用程序被设计成分布式的,即不同的进程运行在不同的计算机或服务器上时,需要使用 IPC 机制来进行进程之间的通信

常见的 IPC 机制包括:管道、共享内存、消息队列、信号量和套接字等

IPC-消息队列

队列的创建-multiprocessing

在这里插入图片描述

json略讲

JSON是一种轻量级的数据交换格式,常用于Web应用程序和客户端之间的数据传输。通过Python的JSON模块,可以将Python数据结构(如列表、字典等)转换为JSON格式字符串,也可以将JSON格式字符串转换为Python数据结构。
以下是JSON模块的一些常见用法:

将Python对象转换为JSON格式字符串:
import json
person = {    "name": "Alice",    "age": 30,    "city": "New York"}
json_str = json.dumps(person)  # 将Python对象转换为JSON格式字符串
print(json_str)
输出结果:
json{"name": "Alice", "age": 30, "city": "New York"}
将JSON格式字符串转换为Python对象:
import json
json_str = '{"name": "Alice", "age": 30, "city": "New York"}'
person = json.loads(json_str)  # 将JSON格式字符串转换为Python对象
print(person)
输出结果:
{'name': 'Alice', 'age': 30, 'city': 'New York'}

需要注意的是,JSON格式字符串中的键名必须使用双引号,而不能使用单引号或没有引号。如果JSON格式字符串不符合规范,则会导致转换失败并抛出异常。多不同类型的 Python 对象转换为 JSON 格式,包括以下几种:

  1. 字符串
  2. 数字(整数和浮点数)
  3. 布尔值(True 和 False)
  4. 列表(list)
  5. 元组(tuple)
  6. 字典(dict)
  7. None

需要注意的是,如果要将自定义的 Python 对象转换为 JSON 格式的字符串,那么该对象必须是可序列化的(即可以转换为 JSON 格式的数据类型)

在实际应用中,JSON模块常用于Web应用程序中从API中获取数据,并将其转换为Python对象进行处理.

信息传递实例-消息队列

创建一个类来

import json
import multiprocessing

class Work(object):
    def __init__(self, q):
        self.q = q

    def send(self, message):
        if not isinstance(message, str):  # 判断是不是字符串
            message = json.dumps(message)
        self.q.put(message)

    def receive(self):
        while 1:
            result = self.q.get()
            try:
                res = json.loads(result)
            except:
                res = result
            print('recv is %s' % res)

if __name__ == '__main__':
    q = multiprocessing.Queue() # 实例化信息队列
    work = Work(q)	# 实例化Work对象
    send = multiprocessing.Process(target=work.send, args=({'信息': '您好,很高兴遇见你'},))
    recv = multiprocessing.Process(target=work.receive)
    send.start()
    recv.start()
    send.join()		# 不能用recv.join() 应为receive是while True,不会停
    # recv.kill() # send运行完杀死recv就可以了
    recv.terminate()		 # 还可以使用recv.terminate()终结接收端

线程的创建—threading

在这里插入图片描述
在这里插入图片描述

线程的使用—import threading

简单的走一个线程

import threading
def work():
	print("Hello")
t = threading.Thread(target=work)
t.start()
t.join()  
# 我发现就算不加阻塞也可以正常的运行
# 应为这个线程是在主进程下的,主进程是要等下面的线程都跑完才会结束的

线程的问题

  • 通过线程执行的函数无法获得返回值
  • 多个线程修改文件可能造成数据错乱

线程池

在这里插入图片描述

线程池的创建—concurrent

在这里插入图片描述

import time
import os
import threading
from concurrent.futures import ThreadPoolExecutor # 线程池

def work(i):
    print(i, os.getpid())
    time.sleep(1)
    return 'result %s' % i

if __name__ == '__main__':
    print(os.getpid())
    t = ThreadPoolExecutor(2)
    result = []
    for i in range(20):
        t_result = t.submit(work, (i, )) # 就算不用变量接收也可以运行
        result.append(t_result) # 将该对象放入result里
    for res in result:
        print(res.result()) # 获取线程返回结果

线程锁

import time
import os
import threading
from concurrent.futures import ThreadPoolExecutor # 线程池

lock = threading.Lock() # 创建全局线程锁,和进程锁不同,不用放到函数里面

def work(i):
    lock.acquire()
    print(i, os.getpid())
    time.sleep(1)
    lock.release()
    return 'result %s' % i

if __name__ == '__main__':
    print(os.getpid())
    t = ThreadPoolExecutor(2)
    result = []
    for i in range(20):
        t_result = t.submit(work, (i, ))
        result.append(t_result)
    for res in result:
        print(res.result())

全局锁

Python线程与其他语言线程的区别

  • 全局锁:Python中有全局锁(Global Interpreter Lock,简称GIL),这意味着任何时候只有一个线程可以执行Python代码。这个锁是由解释器自动管理的,它可以确保Python的内部数据结构在并发访问时不会出错。因此,Python的线程不适合用于CPU密集型任务,但适用于I/O密集型任务。
  • 线程间通信:Python中的线程间通信可以通过共享内存、队列和管道来实现。共享内存通常不是一个好的选择,因为它容易出现竞争条件。队列和管道是线程安全的数据结构,因此它们是常用的线程间通信方式。

在这里插入图片描述

Gil的作用

  • 单一CPU工作
  • 保证线程安全
    怎么解除?换解释器pypy
    可以用多进程+多线程的方法来去除Gil的一些副作用

异步(asynico)—精心典藏

什么是异步?

Python中的异步编程模型是一种非常流行的编程方式,它的主要作用是提高程序的并发能力和性能,特别是在I/O密集型任务中。

在传统的同步编程模型中,当一个任务执行时,它会一直等待直到完成,然后才会返回结果。这种方式在I/O密集型任务中会浪费大量的时间,因为线程或进程在等待I/O操作完成时,是处于空闲状态的,而I/O操作的等待时间往往会很长。

相比之下,异步编程模型中的任务不会等待I/O操作完成,而是在等待的过程中可以继续执行其他任务,从而提高了程序的并发能力和性能。在异步编程中,当一个任务遇到一个I/O操作时,它会通知事件循环(Event Loop)来处理这个I/O操作,并且继续执行其他任务,直到这个I/O操作完成后,事件循环再通知任务继续执行。

在Python中,异步编程可以使用asyncio模块来实现,该模块提供了协程(coroutine)、事件循环(event loop)等功能。使用asyncio编写的异步程序可以更高效地利用CPU资源,提高程序的并发能力和性能,特别是在I/O密集型任务中,例如网络编程、Web应用程序等。

总的来说:
多线程是在同一个进程中并发执行多个线程,每个线程共享同一个进程的内存空间和系统资源,
而异步编程则是在同一个线程中,在同一时间点上处理多个任务。注意粗字区别,就是将全部协程放在一起使用

异步与多进程和多线程

  1. 异步也是一种线程,是一种轻量级线程:协程
  2. 可以获取异步函数的返回值
  3. 主进程需要异步才可以,所有的程序都要是异步
  4. 更适合文件读写使用,需要返回值

async与await关键字

async 定义异步函数

async def test():
	return 'a'

await 执行异步
await 关键字通常用于等待一个异步操作的完成,同时,它只能在 async 函数中使用。在使用 await 关键字等待异步操作完成时,事件循环可以切换到其他协程或任务中,从而达到非阻塞式的并发执行效果。

async def handle():
	result = await test()
"""
await test() # 这样执行是错误的,它只能在 async 函数中使用
"""

我们发现执行异步的函数也是异步的,但最上层的代码不是函数,我们不能用async来定义异步,不用async我们就不能用await来执行异步,所以我们引入asyncio模块

主程序怎么办?
if __name__ == '__main__':

那我们怎么实现所有的程序都要是异步
那就是asyncio模块 asyncio.run(main())

asyncio 的 gather 和 run

在这里插入图片描述
看我注释理解

"""
使用asyncio.run()运行一个协程,
它会自动创建一个事件循环并运行协程,直到协程结束或抛出异常。

使用asyncio.gather()并发运行多个协程,
它会同时启动多个协程,并等待它们全部完成。这个函数返回一个协程,我们可以使用await来等待这个协程的完成。
"""
import asyncio # 引入异步函数
import random
import os

# 下面是两个异步函数,要使用异步所有的程序都要是异步
async def a():
    for i in range(10):
        time_s  = random.random() * 5
        await asyncio.sleep(time_s)
        print(i, 'a', os.getpid(), time_s)
    return 'a function'

async def b():
    for i in range(10):
        time_s = random.random() * 2
        print(i, 'b', os.getpid(),time_s)
        await asyncio.sleep(time_s) # 不要使用time.sleep这是CPU级别的阻塞
    return 'b function'

# 在这里面调用asyncio.gather来运行多个异步函数,asyncio.gather本身就是异步函数,所以运行也要加await
async def main():
    await asyncio.gather(a(),b()) 
    # 这里可以获取返回值的 result = await asyncio.gather(a(),b()),
    # 就会将a和b函数的返回值就放入result列表里了

if __name__ == '__main__':
    asyncio.run(main()) # 用来运行主异步函数,主异步函数一般就是调用其余函数的
	
"""
将最后两行换成下面这个也是一样的
asyncio.run(main()) 这个就是来执行第一个异步函数的

asyncio.run(a()) 和 asyncio.run(b())自然也是可也的但又变成一个一个执行了,对于我们来说没有意义
所以我们要定义一个main函数用gather来运行多个函数,然后使用run(main())来完成,单线程同时执行多个操作的异步操作
"""

gevent 和 上面类似

先安装pip3 install gevent

spawn 和 joinall

在这里插入图片描述

import random
import gevent
import os

def gevent_a():
    for i in range(5):
        print(i, 'a gevent', os.getpid())
        gevent.sleep(random.random() * 2) # 不要使用CPU级别的
    return 'gevent a result'

def gevent_b():
    for i in range(5):
        print(i, 'b gevent', os.getpid())
        gevent.sleep(random.random() * 2)
    return 'b gevent result'

if __name__ == '__main__':
    g_a = gevent.spawn(gevent_a) # 创建协程函数
    g_b = gevent.spawn(gevent_b) 
    gevent_list = [g_a,g_b]	# 将协程函数放入列表
    result = gevent.joinall(gevent_list) #处理全部的协程对象
    print(result) # 运行出来发现是协程函数
    for i in result:
    	print(i.value) # i.value是属性,返回值就是按照gevent_list的顺序来的

现在入门的多线程的文章我也只能写在这了,没有墨水了,等我后面继续学习!!
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值