python学习笔记-进程线程

1.什么是进程(process)?

      程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。

在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

2.什么是线程(thread)?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

3.有了进程为什么还要线程?

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但我的亲,分时是指在不同进程间的分时呀, 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀。

再直白一点, 一个操作系统就像是一个工厂,工厂里面有很多个生产车间,不同的车间生产不同的产品,每个车间就相当于一个进程,且你的工厂又穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间都能同时生产,你的工厂的电工只能给不同的车间分时供电,但是轮到你的qq车间时,发现只有一个干活的工人,结果生产效率极低,为了解决这个问题,应该怎么办呢?。。。。没错,你肯定想到了,就是多加几个工人,让几个人工人并行工作,这每个工人,就是线程!

4.Python GIL(Global Interpreter Lock)

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行,擦。。。,那这还叫什么多线程呀?

  由于所有线程在cpu上都是分时间片执行的.举个例子来说

      首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

这篇文章透彻的剖析了GIL对python多线程的影响,强烈推荐看一下:http://www.dabeaz.com/python/UnderstandingGIL.pdf 

 


5.Python threading模块

线程有2种调用方式,如下:

直接调用:

 1 import threading
 2 import time
 3  
 4 def sayhi(num): #定义每个线程要运行的函数
 5  
 6     print("running on number:%s" %num)
 7  
 8     time.sleep(3)
 9  
10 if __name__ == '__main__':
11  
12     t1 = threading.Thread(target=sayhi,args=(1,)) #生成一个线程实例
13     t2 = threading.Thread(target=sayhi,args=(2,)) #生成另一个线程实例
14  
15     t1.start() #启动线程
16     t2.start() #启动另一个线程
17  
18     print(t1.getName()) #获取线程名
19     print(t2.getName())

继承式调用

 1 import threading
 2 import time
 3  
 4  
 5 class MyThread(threading.Thread):
 6     def __init__(self,num):
 7         threading.Thread.__init__(self)
 8         self.num = num
 9  
10     def run(self):#定义每个线程要运行的函数
11  
12         print("running on number:%s" %self.num)
13  
14         time.sleep(3)
15  
16 if __name__ == '__main__':
17  
18     t1 = MyThread(1)
19     t2 = MyThread(2)
20     t1.start()
21     t2.start()

6.Join & Daemon

一些线程执行后台任务,如发送keepalive数据包,或执行周期性垃圾收集,或其他。 这些仅在主程序运行时有用,一旦其他非守护进程线程退出,就可以关闭它们。

没有守护线程,你必须跟踪它们,并告诉他们退出,然后你的程序才能完全退出。 通过将它们设置为守护线程,您可以让它们运行并忘记它们,当您的程序退出时,任何守护线程都会自动终止。

示例:

 1 import time
 2 import threading
 3  
 4  
 5 def run(n):
 6  
 7     print('[%s]------running----\n' % n)
 8     time.sleep(2)
 9     print('--done--')
10  
11 def main():
12     for i in range(5):
13         t = threading.Thread(target=run,args=[i,])
14         t.start()
15         t.join(1)
16         print('starting thread', t.getName())
17  
18  
19 m = threading.Thread(target=main,args=[])
20 m.setDaemon(True) #将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务
21 m.start()
22 m.join(timeout=2)
23 print("---main thread done----")

注意:守护线程在关机时突然停止。 他们的资源(如打开的文件,数据库事务等)可能无法正确释放。 如果你想让你的线程正常停止,使他们非守护进程,并使用一个合适的信号机制,如一个事件。

7.线程锁(互斥锁Mutex)

一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?

未加锁版本 in python2

 1 import time
 2 import threading
 3  
 4 def addNum():
 5     global num #在每个线程中都获取这个全局变量
 6     print('--get num:',num )
 7     time.sleep(1)
 8     num  -=1 #对此公共变量进行-1操作
 9  
10 num = 100  #设定一个共享变量
11 thread_list = []
12 for i in range(100):
13     t = threading.Thread(target=addNum)
14     t.start()
15     thread_list.append(t)
16  
17 for t in thread_list: #等待所有线程执行完毕
18     t.join()
19 
20 print('final num:', num )

正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。 

*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁

加锁版本

 1 import time
 2 import threading
 3  
 4 def addNum():
 5     global num #在每个线程中都获取这个全局变量
 6     print('--get num:',num )
 7     time.sleep(1)
 8     lock.acquire() #修改数据前加锁
 9     num  -=1 #对此公共变量进行-1操作
10     lock.release() #修改后释放
11  
12 num = 100  #设定一个共享变量
13 thread_list = []
14 lock = threading.Lock() #生成全局锁
15 for i in range(100):
16     t = threading.Thread(target=addNum)
17     t.start()
18     thread_list.append(t)
19  
20 for t in thread_list: #等待所有线程执行完毕
21     t.join()
22  
23 print('final num:', num )

GIL VS Lock 

既然Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下

既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。

8.RLock(递归锁)

说白了就是在一个大锁中还要再包含子锁

 1 import threading,time
 2  
 3 def run1():
 4     print("grab the first part data")
 5     lock.acquire()
 6     global num
 7     num +=1
 8     lock.release()
 9     return num
10 def run2():
11     print("grab the second part data")
12     lock.acquire()
13     global  num2
14     num2+=1
15     lock.release()
16     return num2
17 def run3():
18     lock.acquire()
19     res = run1()
20     print('--------between run1 and run2-----')
21     res2 = run2()
22     lock.release()
23     print(res,res2)
24  
25  
26 if __name__ == '__main__':
27  
28     num,num2 = 0,0
29     lock = threading.RLock()
30     for i in range(10):
31         t = threading.Thread(target=run3)
32         t.start()
33  
34 while threading.active_count() != 1:
35     print(threading.active_count())
36 else:
37     print('----all threads done---')
38     print(num,num2)

9.Semaphore(信号量)

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

 1 import threading,time
 2  
 3 def run(n):
 4     semaphore.acquire()
 5     time.sleep(1)
 6     print("run the thread: %s\n" %n)
 7     semaphore.release()
 8  
 9 if __name__ == '__main__':
10  
11     num= 0
12     semaphore  = threading.BoundedSemaphore(5) #最多允许5个线程同时运行
13     for i in range(20):
14         t = threading.Thread(target=run,args=(i,))
15         t.start()
16  
17 while threading.active_count() != 1:
18     pass #print threading.active_count()
19 else:
20     print('----all threads done---')
21     print(num)
10.Timer

此类表示应在一定时间过去后才应运行的操作

与线程一样,通过调用它们的start()方法来启动计时器。 可以通过调用thecancel()方法停止计时器(在动作开始之前)。 定时器在执行其操作之前将等待的间隔可能与用户指定的间隔不完全相同。

1 def hello():
2     print("hello, world")
3  
4 t = Timer(30.0, hello)
5 t.start()  # after 30 seconds, "hello, world" will be printed

11.Events

事件是一个简单的同步对象;

该事件表示内部标志和线程
可以等待标志被设置,或者设置或清除标志本身。

event = threading.Event()

#一个客户线程可以等待设置的标志
event.wait()

#一个服务器线程可以设置或重置它
event.set()
event.clear()
如果设置了标志,wait方法不会做任何事情。
如果该标志被清除,等待将阻塞,直到它再次被设置。
任何数量的线程都可以等待相同的事件。

通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。

 1 import threading,time
 2 import random
 3 def light():
 4     if not event.isSet():
 5         event.set() #wait就不阻塞 #绿灯状态
 6     count = 0
 7     while True:
 8         if count < 10:
 9             print('\033[42;1m--green light on---\033[0m')
10         elif count <13:
11             print('\033[43;1m--yellow light on---\033[0m')
12         elif count <20:
13             if event.isSet():
14                 event.clear()
15             print('\033[41;1m--red light on---\033[0m')
16         else:
17             count = 0
18             event.set() #打开绿灯
19         time.sleep(1)
20         count +=1
21 def car(n):
22     while 1:
23         time.sleep(random.randrange(10))
24         if  event.isSet(): #绿灯
25             print("car [%s] is running.." % n)
26         else:
27             print("car [%s] is waiting for the red light.." %n)
28 if __name__ == '__main__':
29     event = threading.Event()
30     Light = threading.Thread(target=light)
31     Light.start()
32     for i in range(3):
33         t = threading.Thread(target=car,args=(i,))
34         t.start()

这里还有一个event使用的例子,员工进公司门要刷卡, 我们这里设置一个线程是“门”, 再设置几个线程为“员工”,员工看到门没打开,就刷卡,刷完卡,门开了,员工就可以通过。

 1 import threading
 2 import time
 3 import random
 4 
 5 def door():
 6     door_open_time_counter = 0
 7     while True:
 8         if door_swiping_event.is_set():
 9             print("\033[32;1mdoor opening....\033[0m")
10             door_open_time_counter +=1
11 
12         else:
13             print("\033[31;1mdoor closed...., swipe to open.\033[0m")
14             door_open_time_counter = 0 #清空计时器
15             door_swiping_event.wait()
16 
17 
18         if door_open_time_counter > 3:#门开了已经3s了,该关了
19             door_swiping_event.clear()
20 
21         time.sleep(0.5)
22 
23 
24 def staff(n):
25 
26     print("staff [%s] is comming..." % n )
27     while True:
28         if door_swiping_event.is_set():
29             print("\033[34;1mdoor is opened, passing.....\033[0m")
30             break
31         else:
32             print("staff [%s] sees door got closed, swipping the card....." % n)
33             print(door_swiping_event.set())
34             door_swiping_event.set()
35             print("after set ",door_swiping_event.set())
36         time.sleep(0.5)
37 door_swiping_event  = threading.Event() #设置事件
38 
39 
40 door_thread = threading.Thread(target=door)
41 door_thread.start()
42 
43 
44 
45 for i in range(5):
46     p = threading.Thread(target=staff,args=(i,))
47     time.sleep(random.randrange(3))
48     p.start()
刷卡例子

12.queue队列

队列在线程编程中尤其有用,因为信息必须在多个线程之间安全地交换。

class queue.Queue(maxsize = 0)#先入先出 (FIFO)
class queue.LifoQueue(maxsize = 0)#last in fisrt out
class queue.PriorityQueue(maxsize = 0)#存储数据时可设置优先级的队列
maxsize是一个整数,它设置了可以放入队列中的项目数量的上限。一旦达到此大小,插入就会阻塞,直到队列项被消耗。如果maxsize小于或等于零,队列大小是无限的。

首先检索最低值的条目(最低值的条目是由sorted(list(entries))[0]返回的条目。条目的典型模式是形式为 (priority_number, data)的元组。

异常queue.Empty
在非空的队列对象上调用非阻塞 get() (or get_nowait())时引发的异常。

异常队列
当非阻塞 put() (or put_nowait()) 在已满的Queue对象上调用时引发异常。

Queue.qsize()
Queue.empty()#return如果为空,则返回True
Queue.full() #return如果已满
Queue.put(item, block=True, timeout=None)
将项目放入队列。如果可选的args块为true并且超时为None(默认值),则在必要时阻塞,直到空闲插槽可用。如果timeout是一个正数,它会阻塞最多超时秒数,并且如果在该时间内没有可用空闲时间,则会引发完全异常。否则(块为假),如果空闲时隙立即可用,则将一个项目放在队列上,否则抛出完全异常(在这种情况下忽略超时)。

Queue.put_nowait(item)
相当于put(item,False)。

Queue.get(block=True, timeout=None)
从队列中删除并返回项目。如果可选的args块为true并且timeout为None(默认值),则在必要时阻止,直到项目可用。如果timeout是一个正数,它会阻塞最多超时秒数,并且如果在该时间内没有可用的项目,则会引发Empty异常。否则(块为假),返回一个项,如果一个立即可用,否则提出空异常(在这种情况下超时被忽略)。

Queue.get_nowait()
相当于get(False)。

提供了两种方法来支持跟踪入队任务是否已由守护程序消费者线程完全处理。

Queue.task_done()
指示以前入队的任务已完成。由队列消费者线程使用。对于用于获取任务的每个get(),对task_done()的后续调用将告诉队列任务上的处理已完成。

如果join()当前正在阻塞,它将在所有项目都被处理时恢复(意味着对于已经被put()到队列中的每个项目都接收到一个task_done()调用)。

如果调用的次数比在队列中放置的项目多,则引发ValueError。

Queue.join()block直到queue被消费完毕

 

13.生产者消费者模型

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

为什么要使用生产者和消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

 

下面来看一个最基本的生产者消费者模型的例子

 

 1 import threading
 2 import queue
 3  
 4 def producer():
 5     for i in range(10):
 6         q.put("骨头 %s" % i )
 7     print("开始等待所有的骨头被取走...")
 8     q.join()
 9     print("所有的骨头被取完了...")
10  
11 def consumer(n):
12     while q.qsize() >0:
13         print("%s 取到" %n  , q.get())
14         q.task_done() #告知这个任务执行完了
15 
16 q = queue.Queue()
17 
18 p = threading.Thread(target=producer,)
19 p.start()
20 
21 c1 = consumer("李闯")
 1 import time,random
 2 import queue,threading
 3 q = queue.Queue()
 4 def Producer(name):
 5   count = 0
 6   while count <20:
 7     time.sleep(random.randrange(3))
 8     q.put(count)
 9     print('Producer %s has produced %s baozi..' %(name, count))
10     count +=1
11 def Consumer(name):
12   count = 0
13   while count <20:
14     time.sleep(random.randrange(4))
15     if not q.empty():
16         data = q.get()
17         print(data)
18         print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data))
19     else:
20         print("-----no baozi anymore----")
21     count +=1
22 p1 = threading.Thread(target=Producer, args=('A',))
23 c1 = threading.Thread(target=Consumer, args=('B',))
24 p1.start()
25 c1.start()

14.多进程multiprocessing

multiprocessing是一个包,它支持使用类似于线程模块的API来生成进程。 多进程包提供本地和远程并发,通过使用子进程而不是线程有效地避开全局解释器锁。 因此,多处理模块允许编程人员充分利用给定机器上的多个处理器。 它能在Unix和Windows上运行。

 1 from multiprocessing import Process
 2 import time
 3 def f(name):
 4     time.sleep(2)
 5     print('hello', name)
 6  
 7 if __name__ == '__main__':
 8     p = Process(target=f, args=('bob',))
 9     p.start()
10     p.join()

要显示涉及的单个进程ID,下面是一个扩展示例:

 1 from multiprocessing import Process
 2 import os
 3  
 4 def info(title):
 5     print(title)
 6     print('module name:', __name__)
 7     print('parent process:', os.getppid())
 8     print('process id:', os.getpid())
 9     print("\n\n")
10  
11 def f(name):
12     info('\033[31;1mfunction f\033[0m')
13     print('hello', name)
14  
15 if __name__ == '__main__':
16     info('\033[32;1mmain process line\033[0m')
17     p = Process(target=f, args=('bob',))
18     p.start()
19     p.join()

15.进程间通讯

不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以用以下方法:

Queues

使用方法跟threading里的queue差不多

 1 from multiprocessing import Process, Queue
 2  
 3 def f(q):
 4     q.put([42, None, 'hello'])
 5  
 6 if __name__ == '__main__':
 7     q = Queue()
 8     p = Process(target=f, args=(q,))
 9     p.start()
10     print(q.get())    # prints "[42, None, 'hello']"
11     p.join()

Pipes

Pipe()函数返回通过管道连接的一对连接对象,默认情况下是duplex (two-way)。 例如:

 1 from multiprocessing import Process, Pipe
 2  
 3 def f(conn):
 4     conn.send([42, None, 'hello'])
 5     conn.close()
 6  
 7 if __name__ == '__main__':
 8     parent_conn, child_conn = Pipe()
 9     p = Process(target=f, args=(child_conn,))
10     p.start()
11     print(parent_conn.recv())   # prints "[42, None, 'hello']"
12     p.join()

Pipe()返回的两个连接对象表示管道的两端。 每个连接对象都有send()和recv()方法(等等)。 请注意,如果两个进程(或线程)尝试同时读取或写入管道的同一端,则管道中的数据可能会损坏。 当然,同时使用管道的不同端的进程没有corruption(损坏的意思?)的风险。

16.Managers

Manager()返回的管理器对象控制一个保存Python对象的服务器进程,并允许其他进程使用代理来操作它们。

Manager()返回的管理器将支持类型列表,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Barrier,Queue,Value和Array。 例如,

 1 from multiprocessing import Process, Manager
 2  
 3 def f(d, l):
 4     d[1] = '1'
 5     d['2'] = 2
 6     d[0.25] = None
 7     l.append(1)
 8     print(l)
 9  
10 if __name__ == '__main__':
11     with Manager() as manager:
12         d = manager.dict()
13  
14         l = manager.list(range(5))
15         p_list = []
16         for i in range(10):
17             p = Process(target=f, args=(d, l))
18             p.start()
19             p_list.append(p)
20         for res in p_list:
21             res.join()
22  
23         print(d)
24         print(l)

17.进程同步

Without using the lock output from the different processes is liable to get all mixed up.

没有使用进程同步的时候,输出的时候可能会导致输出混乱

 1 from multiprocessing import Process, Lock
 2  
 3 def f(l, i):
 4     l.acquire()
 5     try:
 6         print('hello world', i)
 7     finally:
 8         l.release()
 9  
10 if __name__ == '__main__':
11     lock = Lock()
12  
13     for num in range(10):
14         Process(target=f, args=(lock, num)).start()

18.进程池 

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池中有两个方法:

  • apply
  • apply_async   常用
 1 from  multiprocessing import Process,Pool
 2 import time
 3  
 4 def Foo(i):
 5     time.sleep(2)
 6     return i+100
 7  
 8 def Bar(arg):
 9     print('-->exec done:',arg)
10  
11 pool = Pool(5)
12  
13 for i in range(10):
14     pool.apply_async(func=Foo, args=(i,),callback=Bar)
15     #pool.apply(func=Foo, args=(i,))
16  
17 print('end')
18 pool.close()
19 pool.join()#进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

转载于:https://www.cnblogs.com/wayde-Z/p/python-process.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值