进程:

  之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

进程间通信

用Queue模块:

IPC(Inter-Process Communication)

队列

概念介绍

队列是先进先出

必须put放进东西后  才能get来取值

创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

Queue([maxsize])
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。
Queue([maxsize])
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。
Queue的实例q具有以下方法: q.get( [ block [ ,timeout ] ] )
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。 q.get_nowait( )
同q.get(False)方法。 q.put(item [, block [,timeout ] ] )
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。 q.qsize()
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。 q.empty()
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。 q.full()
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。。

方法介绍

队列中的进程的内容是共享的  因为不同的进程的数据是隔离的  我们可以用队列  让他们之间的数据进行共享

在进程中使用队列可以完成双向通信

from multiprocessing import Process ,Queue
q = Queue(10)
try:
q.get_nowwait() # 如果你用 nowwait的话你的获取嗯u过没有就不会阻塞就会报错
except:
print('queue.Empty')
q.get()
for i in range(10):
q.get(i) print(q.qsize(10))
from  multiprocessing import Process ,Queue
q = Queue(10) # 创建一个可以存放10个值的队列
# try:
# q.get_nowwait()
# except:
# print('queue.Empty')
#
# q.get() for i in range(10):
q.put(i)
print(q.qsize()) # 获取你的队列可以存放的最大值
print(q.full()) # 判定是不是满了 返回的值布尔值
# q.put(111) # 给这个队列放进值
# print(q.grt())
print('*'*10)
print(q.empty()) # 判定队列是不是为空 返回的也是布尔值
 
生产者消费者模型
解决数据供需不平衡的情况
队列是进程安全的 内置了锁来保证队列中的每一个数据都不会被多个进程重复取
import time
import random
from multiprocessing import Process,Queue
生产者消费者模型
解决数据供需不平衡的情况
队列是进程安全的 内置了锁来保证队列中的每一个数据都不会被多个进程重复取
def consumer(q,name):
while True:
food = q.get()
if food == 'done':break
time.sleep(random.random())
print('%s吃了%s'%(name,food)) def producer(q,name,food):
for i in range(10):
time.sleep(random.random())
print('%s生产了%s%s'%(name,food,i))
q.put('%s%s'%(food,i)) if __name__ == '__main__':
q = Queue()
p1 = Process(target=producer,args=[q,'Egon','泔水'])
p2 = Process(target=producer,args=[q,'Yuan','骨头鱼刺'])
p1.start()
p2.start()
Process(target=consumer,args=[q,'alex']).start()
Process(target=consumer,args=[q,'wusir']).start()
p1.join()
p2.join()
q.put('done')
q.put('done')

生产者消费者来进性队列的安全

import time
import random
from multiprocessing import Process,JoinableQueue
def consumer(q,name):
while True:
food = q.get()
time.sleep(random.random())
print('%s吃了%s'%(name,food))
q.task_done() def producer(q,name,food):
for i in range(10):
time.sleep(random.random())
print('%s生产了%s%s'%(name,food,i))
q.put('%s%s'%(food,i))
q.join() # 等到所有的数据都被taskdone才结束 if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer,args=[q,'Egon','泔水'])
p2 = Process(target=producer,args=[q,'Yuan','骨头鱼刺'])
p1.start()
p2.start()
c1 = Process(target=consumer,args=[q,'alex'])
c2 = Process(target=consumer,args=[q,'wusir'])
c1.daemon = True
c2.daemon = True
c1.start()
c2.start()
p1.join()
p2.join() # producer
# put
# 生产完全部的数据就没有其他工作了
# 在生产数据方 : 允许执行q.join
# join会发起一个阻塞,直到所有当前队列中的数据都被消费
# consumer
# get 获取到数据
# 处理数据
# q.task_done() 告诉q,刚刚从q获取的数据已经处理完了 # consumer每完成一个任务就会给q发送一个taskdone
# producer在所有的数据都生产完之后会执行q.join()
# producer会等待consumer消费完数据才结束
# 主进程中对producer进程进行join
# 主进程中的代码会等待producer执行完才结束
# producer结束就意味着主进程代码的结束
# consumer作为守护进程结束 # consumer中queue中的所有数据被消费
# producer join结束
# 主进程的代码结束
# consumer结束
# 主进程结束

例子

Queue([maxsize])
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。
Queue的实例q具有以下方法: q.get( [ block [ ,timeout ] ] )
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。 q.get_nowait( )
同q.get(False)方法。 q.put(item [, block [,timeout ] ] )
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。 q.qsize()
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。 q.empty()
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。 q.full()
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。

JoinableQueue([maxsize]) 
创建可连接的共享进程队列。这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

JoinableQueue的实例p除了与Queue对象相同的方法之外,还具有以下方法:

q.task_done()
使用者使用此方法发出信号,表示q.get()返回的项目已经被处理。如果调用此方法的次数大于从队列中删除的项目数量,将引发ValueError异常。 q.join()
生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。

方法介绍

from multiprocessing import Process,JoinableQueue
import time,random,os
def consumer(q):
while True:
res=q.get()
time.sleep(random.randint(1,3))
print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))
q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了 def producer(name,q):
for i in range(10):
time.sleep(random.randint(1,3))
res='%s%s' %(name,i)
q.put(res)
print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
q.join() #生产完毕,使用此方法进行阻塞,直到队列中所有项目均被处理。 if __name__ == '__main__':
q=JoinableQueue()
#生产者们:即厨师们
p1=Process(target=producer,args=('包子',q))
p2=Process(target=producer,args=('骨头',q))
p3=Process(target=producer,args=('泔水',q)) #消费者们:即吃货们
c1=Process(target=consumer,args=(q,))
c2=Process(target=consumer,args=(q,))
c1.daemon=True
c2.daemon=True #开始
p_l=[p1,p2,p3,c1,c2]
for p in p_l:
p.start() p1.join()
p2.join()
p3.join()
print('主') #主进程等--->p1,p2,p3等---->c1,c2
#p1,p2,p3结束了,证明c1,c2肯定全都收完了p1,p2,p3发到队列的数据
#因而c1,c2也没有存在的价值了,不需要继续阻塞在进程中影响主进程了。应该随着主进程的结束而结束,所以设置成守护进程就可以了。

JoinableQueue队列实现消费之生产者模型

管道:

管道是双向通信,数据进程不安全,队列是管道加锁来实现的

#创建管道的类:
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
#参数介绍:
dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
#主要方法:
conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
#其他方法:
conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法
conn1.fileno():返回连接使用的整数文件描述符
conn1.poll([timeout]):如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。 conn1.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。
conn.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收 conn1.recv_bytes_into(buffer [, offset]):接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。

介绍

from multiprocessing import Process, Pipe

def f(conn):
conn.send("Hello The_Third_Wave")
conn.close() if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
p.start()
print(parent_conn.recv())
p.join()

pipe初介绍

应该特别注意管道端点的正确管理问题。如果是生产者或消费者中都没有使用管道的某个端点,就应将它关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。

# 管道
# from multiprocessing import Pipe
# left,right = Pipe()
# left.send('1234')
# print(right.recv())
# left.send('1234')
# print(right.recv())
管道的信息发送接收信息是不需要进行编码转码的
from multiprocessing import Process, Pipe def f(parent_conn,child_conn):
parent_conn.close() #不写close将不会引发EOFError
while True:
try:
print(child_conn.recv())
except EOFError:
child_conn.close()
break if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(parent_conn,child_conn,))
p.start()
child_conn.close()
parent_conn.send('hello')
parent_conn.send('hello')
parent_conn.send('hello')
parent_conn.close()
p.join()

进程之间的数据共享

展望未来,基于消息传递的并发编程是大势所趋

即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。

这样极大地减少了对使用锁定和其他同步手段的需求,还可以扩展到分布式系统中。

但进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。

以后我们会尝试使用数据库来解决现在进程之间的数据共享问题。

进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的
虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此 A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies. A manager returned by Manager() will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array.

Manager模块介绍

from multiprocessing import Manager,Process,Lock
def work(d,lock):
with lock: #不加锁而操作共享的数据,肯定会出现数据错乱
d['count']-=1 if __name__ == '__main__':
lock=Lock()
with Manager() as m:
dic=m.dict({'count':100})
p_l=[]
for i in range(100):
p=Process(target=work,args=(dic,lock))
p_l.append(p)
p.start()
for p in p_l:
p.join()
print(dic)

Manager例子

from multiprocessing import Manager,Process,Lock
def func(dic,lock):
# lock.acquire()
# dic['count'] = dic['count']-1
# lock.release()
with lock: # 上下文管理 :必须有一个开始动作 和 一个结束动作的时候
dic['count'] = dic['count'] - 1 if __name__ == '__main__':
m = Manager()
lock = Lock()
dic = m.dict({'count':100})
p_lst = []
for i in range(100):
p = Process(target=func,args=[dic,lock])
p_lst.append(p)
p.start()
for p in p_lst:p.join()
print(dic) # 同一台机器上 : Queue
# 在不同台机器上 :消息中间件

进程池和multiprocess.Pool模块

进程池

为什么要有进程池?进程池的概念。

在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?

在这里,要给大家介绍一个进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。

multiprocess.Pool模块

Pool([numprocess  [,initializer [, initargs]]]):创建进程池

概念介绍

1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组

参数介绍

1 p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
2 '''需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()'''
3
4 p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
5 '''此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。'''
6
7 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
8
9 P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用

主要方法

1 方法apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法
2 obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
3 obj.ready():如果调用完成,返回True
4 obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
5 obj.wait([timeout]):等待结果变为可用。
6 obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数

其他方法

 '''
一 进程池与线程池
在刚开始学多进程或多线程时,我们迫不及待地基于多进程或多线程实现并发的套接字通信,
然而这种实现方式的致命缺陷是:服务的开启的进程数或线程数都会随着并发的客户端数目地增多而增多,
这会对服务端主机带来巨大的压力,甚至于不堪重负而瘫痪,于是我们必须对服务端开启的进程数或线程数加以控制,
让机器在一个自己可以承受的范围内运行,这就是进程池或线程池的用途,
例如进程池,就是用来存放进程的池子,本质还是基于多进程,只不过是对开启进程的数目加上了限制
''' 
进程池一般就是运用在服务端口的,比如12306官网就是这样的,如果不进行限定的话 同时无限个进程一起来访问的话就会引起服务器的崩溃的

同步和异步
import os,time
from multiprocessing import Pool def work(n):
print('%s run' %os.getpid())
time.sleep(3)
return n**2 if __name__ == '__main__':
p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
res_l=[]
for i in range(10):
res=p.apply(work,args=(i,)) # 同步调用,直到本次任务执行完毕拿到res,等待任务work执行的过程中可能有阻塞也可能没有阻塞
# 但不管该任务是否存在阻塞,同步调用都会在原地等着
print(res_l)

进程池的同步调用

import os
import time
import random
from multiprocessing import Pool def work(n):
print('%s run' %os.getpid())
time.sleep(random.random())
return n**2 if __name__ == '__main__':
p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
res_l=[]
for i in range(10):
res=p.apply_async(work,args=(i,)) # 异步运行,根据进程池中有的进程数,每次最多3个子进程在异步执行
# 返回结果之后,将结果放入列表,归还进程,之后再执行新的任务
# 需要注意的是,进程池中的三个进程不会同时开启或者同时结束
# 而是执行完一个就释放一个进程,这个进程就去接收新的任务。
res_l.append(res) # 异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果
# 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
p.close()
p.join()
for res in res_l:
print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get

进程池的异步调用

import time
import random
from multiprocessing import Pool
def func(i):
print('func%s' % i)
time.sleep(random.randint(1,3))
return i**2
if __name__ == '__main__':
p = Pool(5)
ret_l = []
for i in range(15):
# p.apply(func=func,args=(i,)) # 同步调用
ret = p.apply_async(func=func,args=(i,))# 异步调用
ret_l.append(ret)
for ret in ret_l : print(ret.get())
# 主进程和所有的子进程异步了

回调函数:

需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数

我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。
from multiprocessing import Pool
import requests
import json
import os def get_page(url):
print('<进程%s> get %s' %(os.getpid(),url))
respone=requests.get(url)
if respone.status_code == 200:
return {'url':url,'text':respone.text} def pasrse_page(res):
print('<进程%s> parse %s' %(os.getpid(),res['url']))
parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text']))
with open('db.txt','a') as f:
f.write(parse_res) if __name__ == '__main__':
urls=[
'https://www.baidu.com',
'https://www.python.org',
'https://www.openstack.org',
'https://help.github.com/',
'http://www.sina.com.cn/'
] p=Pool(3)
res_l=[]
for url in urls:
res=p.apply_async(get_page,args=(url,),callback=pasrse_page)
res_l.append(res) p.close()
p.join()
print([res.get() for res in res_l]) #拿到的是get_page的结果,其实完全没必要拿该结果,该结果已经传给回调函数处理了 '''
打印结果:
<进程3388> get https://www.baidu.com
<进程3389> get https://www.python.org
<进程3390> get https://www.openstack.org
<进程3388> get https://help.github.com/
<进程3387> parse https://www.baidu.com
<进程3389> get http://www.sina.com.cn/
<进程3387> parse https://www.python.org
<进程3387> parse https://help.github.com/
<进程3387> parse http://www.sina.com.cn/
<进程3387> parse https://www.openstack.org
[{'url': 'https://www.baidu.com', 'text': '<!DOCTYPE html>\r\n...',...}]
'''

使用多进程请求多个url来减少网络等待浪费的时间

import os
from urllib.request import urlopen
from multiprocessing import Pool
def get_url(url):
print('-->',url,os.getpid())
ret = urlopen(url)
content = ret.read()
return url def call(url):
# 分析
print(url,os.getpid()) if __name__ == '__main__':
print(os.getpid())
l = [
'http://www.baidu.com', #
'http://www.sina.com',
'http://www.sohu.com',
'http://www.sogou.com',
'http://www.qq.com',
'http://www.bilibili.com', #0.1
]
p = Pool(5) # count(cpu)+1
ret_l = []
for url in l:
ret = p.apply_async(func = get_url,args=[url,],callback=call)
ret_l.append(ret)
for ret in ret_l : ret.get() # 回调函数
# 在进程池中,起了一个任务,这个任务对应的函数在执行完毕之后
# 的返回值会自动作为参数返回给回调函数
# 回调函数就根据返回值再进行相应的处理 # 回调函数 是在主进程执行的

最新文章

  1. 数据结构与算法JavaScript (四) 串(BF)
  2. hdu 5237 二进制
  3. ReactiveCocoa常见操作方法介绍/MVVM架构思想
  4. 避免硬编码你的PostgreSQL数据库密码
  5. [iOS基础控件 - 6.9.4] 抓取网页图片资源
  6. SOCI、LiteSQL、POCO数据库访问类库对比
  7. MySQL必知必会笔记&lt;1&gt;
  8. HDU 4067 Random Maze
  9. angularjs的directive详解
  10. XML命名规则
  11. vue2.0生命周期
  12. 【XSY2714】大佬的难题 数学 树状数组
  13. pythonweb服务器编程(二)
  14. Windows android appium python3 环境搭建
  15. tomcat压缩版配置
  16. Nmap 使用技巧及其攻略
  17. js阻止事件冒泡的两种方法
  18. Lodash入门介绍
  19. BigPipe 大的页面分割成一个一个管道
  20. eclipse修改文件编码

热门文章

  1. 【并行】Ubuntu安装MPI库
  2. 第3章—高级装配—bean的作用域
  3. IPC之binder机制
  4. com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote method解决方法
  5. [心平气和读经典]The TCP/IP Guide(001)
  6. CI中使用log4php调试程序
  7. leetcode简单题目两道(2)
  8. eclipse下JAVA的搭建
  9. webstorm中es6语法报错,.vue文件中es6语法报错
  10. sublime下package control安装无效解决