Python高级编程和异步IO并发编程
一、多线程、多进程和线程池编程
1、GIL
gil global interpreter lock (全局解释器锁)
python中一个线程对应于c语言中的一个线程 (cpython)
gil使得同一个时刻只有一个线程在一个cpu上执行字节码, 无法将多个线程映射到多个cpu上执行
GIL主动释放的情况:
gil会根据执行的字节码行数以及时间片释放gil
gil在遇到io的操作时候会主动释放
2、多线程编程 --threading
操作系统能调度的最小单元是线程
对于io操作来说,多线程和多进程性能差别不大
setdaemon 方法:守护线程,主线程运行完毕,立刻结束所有线程
join 方法:线程阻塞,无论主线程是否运行完毕,都需等待子线程执行完成才能结束
创建多线程两种方式
1)直接使用:
importtimeimportthreadingdefget_detail_html(url):print("get detail html started")
time.sleep(2)print("get detail html end")defget_detail_url(url):print("get detail url started")
time.sleep(4)print("get detail url end")if __name__ == "__main__":
thread1= threading.Thread(target=get_detail_html)
thread1= threading.Thread(target=get_detail_html)#thread1.setDaemon(True)
#thread2.setDaemon(True) # 守护线程
start_time=time.time()
thread1.start()
thread2.start()
thread1.join()
thread2.join()#线程阻塞
print ("last time: {}".format(time.time()-start_time))
2)使用thread继承方式:
推荐这种方式,我们可以在类中做更多需要的处理。
classGetDetailHtml(threading.Thread):def __init__(self, name):
super().__init__(name=name)def run(self): #重载 threading.Thread 中的方法
print("get detail html started")
time.sleep(2)print("get detail html end")classGetDetailUrl(threading.Thread):def __init__(self, name):
super().__init__(name=name)defrun(self):print("get detail url started")
time.sleep(4)print("get detail url end")if __name__ == "__main__":
thread1= GetDetailHtml("get_detail_html")
thread2= GetDetailUrl("get_detail_url")
start_time=time.time()
thread1.start()
thread2.start()
thread1.join()
thread2.join()print ("last time: {}".format(time.time()-start_time))
3、线程间通信:共享变量和 Queue
1)共享变量的方式,即全局定义一个变量,给多个子线程中调用。安全性不高,不建议使用
2)Queue 队列的方式,比较安全,推荐使用.Queue常用方法可查看源码
#通过queue的方式进行线程间同步,更安全
from queue importQueueimporttimeimportthreadingdefget_detail_html(queue):#爬取文章详情页
whileTrue:
url= queue.get() #queue get方法 阻塞
print("get detail html started")
time.sleep(2)print("get detail html end")defget_detail_url(queue):#爬取文章列表页
whileTrue:print("get detail url started")
time.sleep(4)for i in range(20):
queue.put("http://projectsedu.com/{id}".format(id=i)) #queue put 方法
print("get detail url end")if __name__ == "__main__":
detail_url_queue= Queue(maxsize=1000)
thread_detail_url= threading.Thread(target=get_detail_url, args=(detail_url_queue,))for i in range(10):
html_thread= threading.Thread(target=get_detail_html, args=(detail_url_queue,))
html_thread.start()
start_time=time.time()
detail_url_queue.task_done() 结束队列阻塞
detail_url_queue.join()#队列阻塞
print ("last time: {}".format(time.time()-start_time))
4. 线程同步:Lock、RLock
1)Lock
from threading importLock
lock=Lock()
lock.acquire()#获取锁
lock.acquire() #上面已获取锁,再acquire则会阻塞住,即在锁未释放前,两次acquire就会造成死锁
total += 1lock.release()#释放锁,在释放锁之前其他线程都会被hold住,等待锁释放
lock.release()
注意:1、用锁会影响性能
2、用锁注意造成死锁问题:
1)未释放锁前,连续两次acquire 会造成死锁
2)资源竞争:即线程一要的资源在线程二中,线程二要的资源在线程一中,线程一获取锁需要用到线程二的资源,然而锁在线程一中,线程二锁住(阻塞住)无法将资源给到线程一,因此造成死锁
2)RLock
可重入的锁 ,在同一个线程里面,可以连续调用多次acquire, 一定要注意acquire的次数要和release的次数相等
from threading importLock, RLock, Condition#RLock可重入的锁,在同一个线程里面,可以连续调用多次acquire, 一定要注意acquire的次数要和release的次数相等
total=0
lock=RLock()defadd():globallockglobaltotalfor i in range(1000000):
lock.acquire()#获取锁
lock.acquire() #RLock ,不会造成死锁
total += 1lock.release()#释放锁
lock.release() #release次数需与acquire次数一致
RLock的可用性要远远高于Lock,如果要使用锁,建议使用RLock
from threading importLock, RLock, Condition
total=0
lock=RLock()defadd():globallockglobaltotalfor i in range(1000000):
lock.acquire()#获取锁
lock.acquire()
total+= 1lock.release()#释放锁
lock.release()defdesc():globaltotalgloballockfor i in range(1000000):
lock.acquire()
total-= 1lock.release()importthreading
thread1= threading.Thread(target=add)
thread2= threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()print(total)
demo
5、线程同步 - condition 使用以及源码分析
condition中实现了魔法函数:__enter__、__exit__ ,是上下文管理器,可以用with处理
#通过condition完成协同读诗
importthreadingclassXiaoAi(threading.Thread):def __init__(self, cond):
super().__init__(name="小爱")
self.cond=conddefrun(self):
with self.cond:
self.cond.wait()print("{} : 在".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 好啊".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 君住长江尾".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 共饮长江水".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 此恨何时已".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 定不负相思意".format(self.name))
self.cond.notify()classTianMao(threading.Thread):def __init__(self, cond):
super().__init__(name="天猫精灵")
self.cond=conddefrun(self):
with self.cond:print("{} : 小爱同学".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 我们来对古诗吧".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 我住长江头".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 日日思君不见君".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 此水几时休".format(self.name))
self.cond.notify()
self.cond.wait()print("{} : 只愿君心似我心".format(self.name))
self.cond.notify()
self.cond.wait()if __name__ == "__main__":from concurrent importfutures
cond=threading.Condition()
xiaoai=XiaoAi(cond)
tianmao=TianMao(cond)
xiaoai.start()
tianmao.start()#启动顺序很重要
#在调用with cond之后才能调用wait或者notify方法
#condition有两层锁, 一把底层锁会在线程调用了wait方法的时候释放, 上面的锁会在每次调用wait的时候分配一把并放入到cond的等待队列中,等到notify方法的唤醒
6、线程同步 - Semaphore 使用以及源码分析
Semaphore:信号量, 是用于控制进入数量的锁。本质上是锁,Lock是单锁,信号量是指定多把锁,也就是说通过信号量指定多个数线程可以访问相同资源,一般情况下读操作可以有多个,但写操作同时只有一个
Semaphore 管理一个计数器,每调用一次 acquire() 方法,计数器就减一,每调用一次 release() 方法,计数器就加一。计时器的值默认为 1 ,计数器的值不能小于 0,当计数器的值为 0 时,调用 acquire() 的线程就会等待,直到 release() 被调用。 因此,可以利用这个特性来控制线程数量
控制爬虫数量,一次最多执行3个线程:
importthreadingimporttime#控制爬虫数量,一次执行三次
classHtmlSpider(threading.Thread):def __init__(self, url, sem):
super().__init__()
self.url=url
self.sem=semdefrun(self):
time.sleep(2)print("got html text success")
self.sem.release()#Semaphore.release(),释放锁
classUrlProducer(threading.Thread):def __init__(self, sem):
super().__init__()
self.sem=semdefrun(self):for i in range(20):
self.sem.acquire()#Semaphore.acquire(),每运行一个爬虫自动减1,当3个爬虫都运行时,计数器减为零,此时再调用acquire方法则会hold
html_thread = HtmlSpider("https://baidu.com/{}".format(i), self.sem)
html_thread.start()if __name__ == "__main__":
sem= threading.Semaphore(3) #控制爬虫数量为3个
url_producer =UrlProducer(sem)
url_producer.start()
7、ThreadPoolExecutor线程池
线程池使用:不仅仅是数量控制,可以获取线程状态、任务状态、线程返回值等信息; 当一个线程完成的时候我们主线程能立即知道 ; futures可以让多线程和多进程编码接口一致 。
线程池模块 ThreadPollExecutor
线程池使用过程:
实例化线程池
提交任务,会有个返回对象,submit是不会堵塞,立即返回
让主线程等待线程执行完成
关闭线程池
线程池几个方法:
done() :判断任务是否完成
result() :获取任务执行结果,会阻塞
cancle():取消任务,任务在执行中或者已经执行完成则无法取消
from concurrent.futures importThreadPoolExecutorimporttimedefget_html(times):
time.sleep(times)print("get page {} success".format(times))returntimes#程序执行时,会到线程池执行线程
executor = ThreadPoolExecutor(max_workers=2) #线程数最大两个#通过submit函数提交执行的函数到线程池中, submit不会阻塞,有个返回值,线程状态等可以通过这个返回值查看
task1 = executor.submit(get_html, (3))
task2= executor.submit(get_html, (2))#状态查询:#done方法用于判定某个任务是否完成#print(task1.done()) # 返回:False#print(task2.cancel()) # 可以取消没有执行的#time.sleep(3)#print(task1.done()) # 返回:True
##result方法可以获取task的执行结果#print(task1.result()) # 返回:3
1)futures下的as_completed()方法 :获取已经执行完成的任务的结果(推荐)
from concurrent.futures import as_completed
urls = [3,2,4]
all_task= [executor.submit(get_html, (url)) for url inurls]#wait(all_task, return_when=FIRST_COMPLETED)
for future inas_completed(all_task): as_completed是个生成器,执行完成的任务都能获取到
data=future.result()print("get {} page".format(data)) #打印执行成功的任务
2)线程池自带的map()方法:获取已经执行完成的任务结果
from concurrent.futures importThreadPoolExecutor, as_completed, wait, FIRST_COMPLETEDimporttimedefget_html(times):
time.sleep(times)print("get page {} success".format(times))returntimes
executor= ThreadPoolExecutor(max_workers=2)#通过executor的map获取已经完成的task的值
for data inexecutor.map(get_html, urls):print("get {} page".format(data))
3)futures下的wait()方法 :等待线程完成,才执行下面的程序
urls = [3,2,4]
all_task= [executor.submit(get_html, (url)) for url inurls]
wait(all_task, return_when=FIRST_COMPLETED) #阻塞,return_when(条件),FIRST_COMPLETED:第一个线程执行完成才能执行下面的,未完成前会阻塞住
print("main")for future inas_completed(all_task):
data=future.result()print("get {} page".format(data))
8、multiprocessing 多进程编程
1)多进程创建方式
方式一:
importmultiprocessingimporttimedefworker_1(interval):print "worker_1"time.sleep(interval)print "end worker_1"
defworker_2(interval):print "worker_2"time.sleep(interval)print "end worker_2"
defworker_3(interval):print "worker_3"time.sleep(interval)print "end worker_3"
if __name__ == "__main__":
p1= multiprocessing.Process(target = worker_1, args = (2,))
p2= multiprocessing.Process(target = worker_2, args = (3,))
p3= multiprocessing.Process(target = worker_3, args = (4,))
p1.start()
p2.start()
p3.start()print("The number of CPU is:" +str(multiprocessing.cpu_count()))for p inmultiprocessing.active_children():print("child p.name:" + p.name + "\tp.id" +str(p.pid))print "END!!!!!!!!!!!!!!!!!"
# 结果:
The number of CPU is:4
child p.name:Process-3 p.id7992
child p.name:Process-2 p.id4204
child p.name:Process-1 p.id6380
END!!!!!!!!!!!!!!!!!
worker_1
worker_3
worker_2
end worker_1
end worker_2
end worker_3
方式二:
importmultiprocessingimporttimeclassClockProcess(multiprocessing.Process):def __init__(self, interval):
multiprocessing.Process.__init__(self)
self.interval=intervaldefrun(self):
n= 5
while n >0:print("the time is {0}".format(time.ctime()))
time.sleep(self.interval)
n-= 1
if __name__ == '__main__':
p= ClockProcess(3)
p.start()
# 结果:
the time is Tue Apr 21 20:31:30 2015
the time is Tue Apr 21 20:31:33 2015
the time is Tue Apr 21 20:31:36 2015
the time is Tue Apr 21 20:31:39 2015
the time is Tue Apr 21 20:31:42 2015
2)进程池:
方式一:
from concurrent.futures importProcessPoolExecutorimportrequestsimporttimedeftask(url):
response=requests.get(url)print(url,response)#写正则表达式
pool= ProcessPoolExecutor(7)
url_list=['http://www.cnblogs.com/wupeiqi','http://huaban.com/favorite/beauty/','http://www.bing.com','http://www.zhihu.com','http://www.sina.com','http://www.baidu.com','http://www.autohome.com.cn',
]for url inurl_list:
pool.submit(task,url)
pool.shutdown(wait=True)
方式二:
from concurrent.futures importProcessPoolExecutorimportrequestsimporttimedeftask(url):
response=requests.get(url)returnresponsedef done(future,*args,**kwargs):
response=future.result()print(response.status_code,response.content)
pool= ProcessPoolExecutor(7)
url_list=['http://www.cnblogs.com/wupeiqi','http://huaban.com/favorite/beauty/','http://www.bing.com','http://www.zhihu.com','http://www.sina.com','http://www.baidu.com','http://www.autohome.com.cn',
]for url inurl_list:
v=pool.submit(task,url)
v.add_done_callback(done)
pool.shutdown(wait=True)
3)进程下的pool
使用进程池(非阻塞):
#coding: utf-8
importmultiprocessingimporttimedeffunc(msg):print "msg:", msg
time.sleep(3)print "end"
if __name__ == "__main__":
pool= multiprocessing.Pool(processes = 3)for i in xrange(4):
msg= "hello %d" %(i)
pool.apply_async(func, (msg, ))#维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
print "Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~"pool.close()
pool.join()#调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
print "Sub-process(es) done."
函数解释:
apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞,apply(func[, args[, kwds]])是阻塞的(理解区别,看例1例2结果区别)
close() 关闭pool,使其不在接受新的任务。
terminate() 结束工作进程,不在处理未完成的任务。
join() 主进程阻塞,等待子进程的退出, join方法要在close或terminate之后使用。
执行说明:创建一个进程池pool,并设定进程的数量为3,xrange(4)会相继产生四个对象[0, 1, 2, 4],四个对象被提交到pool中,因pool指定进程数为3,所以0、1、2会直接送到进程中执行,当其中一个执行完事后才空出一个进程处理对象3,所以会出现输出“msg: hello 3”出现在"end"后。因为为非阻塞,主函数会自己执行自个的,不搭理进程的执行,所以运行完for循环后直接输出“mMsg: hark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~”,主程序在pool.join()处等待各个进程的结束。
使用进程池(阻塞):
#coding: utf-8
importmultiprocessingimporttimedeffunc(msg):print "msg:", msg
time.sleep(3)print "end"
if __name__ == "__main__":
pool= multiprocessing.Pool(processes = 3)for i in xrange(4):
msg= "hello %d" %(i)
pool.apply(func, (msg, ))#维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
print "Mark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~"pool.close()
pool.join()#调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
9、进程间通信 - Queue、Pipe,Manager
1)Queue队列
是 multiprocessing自带的 Queue
importtimefrom multiprocessing importProcess, Queue#from queue import Queue #不是这个Queue
defproducer(queue):
queue.put("a")
time.sleep(2)defconsumer(queue):
time.sleep(2)
data=queue.get()print(data)if __name__ == "__main__":
queue= Queue(10)
my_producer= Process(target=producer, args=(queue,))
my_consumer= Process(target=consumer, args=(queue,))
my_producer.start()
my_consumer.start()
my_producer.join()
my_consumer.join()
multiprocessing中的Queue不能用于multiprocessing下的pool进程池,pool中的进程间通信需要使用manager中的queu
目前我们已经说到了三个Queue:
from queue import Queue :用于线程通信
from multiprocessing import Queue :用于进程通信
from multiprocessing import Manager → q=Manager().Queue() :用于multiprocessing下的pool进程池的进程通信
2)Pipe 管道
multiprocessing下的Pipe ,Pipe只能用于两个进程间的通信,Pipe的性能高于Queue的
from multiprocessing importProcess, Pipedefproducer(pipe):
pipe.send("MJ")defconsumer(pipe):print(pipe.recv())if __name__ == "__main__":
recevie_pipe, send_pipe=Pipe()#pipe只能适用于两个进程间通信
my_producer= Process(target=producer, args=(send_pipe, ))
my_consumer= Process(target=consumer, args=(recevie_pipe,))
my_producer.start()
my_consumer.start()
my_producer.join()
my_consumer.join()
GIL锁知识点:
Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
即使在多核上运行多线程,同一时刻也只有一核会运行一个线程。
遇到CPU密集型这种,建议不使用多线程,而改用多进程处理,但多进程更耗成本