一、编程思想
并行编程的思想:分而治之,有两种模型
1.MapReduce:将任务划分为可并行的多个子任务,每个子任务完成后合并得到结果
例子:统计不同形状的个数。
先通过map进行映射到多个子任务,分别统计个数,然后在用reduce进行归纳一下。
2.流水:将任务分为串行的多个子任务,每个子任务并行。ProductConsume
例子:
多个生产者进行并行,多个消费者进行并行。生产者生产出来东西放到队列里;队列里有东西时,消费者就可以进行消费,这样双方没有太大的依赖关系。
为什么要并行编程呢?
多核,云计算,使得实现并行编程的条件更容易满足。
大数据(导致数据多),机器学习(复杂),高并发,使得并行编程很必要。
为什么很少用呢?
任务分割,共享数据的访问,死锁,互斥,信号量,利用管道,队列通信。线程,进程的管理。
这些概念使得并行编程的实现看上去很难
怎么学并行编程?
库: Threading,实现多线程
Multiprocess,实现多进程
Parallepython,实现分布式计算,同时解决CPU和网络资源受限问题。
Celery+RabbitMQ/Redis,可实现分布式任务队列 Django和它搭配可实现异步任务队列
Gevent,可实现高效异步IO,协成
2.进程和线程
CPU同一时刻只能调度一个进程,进程之间memory独立,进程内线程共享memory。
我们主要解决的问题是:
进程间通信问题;
线程间同步问题
例子:计算10000000000自减到0,然后用多进程和多线程计算,看看他们用时多久
# -*- coding: utf-8 -*- # CopyRight by heibanke import time from threading import Thread from multiprocessing import Process def countdown(n): while n > 0: n -= 1 COUNT = 100000000 # 1亿 def thread_process_job(n, Thread_Process, job): """ n: 多线程或多进程数 Thread_Process: Thread/Process类 job: countdown任务 """ local_time=time.time() #实例化多线程或多进程 threads_or_processes = [Thread_Process(target=job,args=(COUNT//n,)) for i in xrange(n)]#学习这种写法,很高大上,把不同的类放到列表里边 #threads_or_processes中保存了三个Thread_process个对象 for t in threads_or_processes: t.start() #开始线程或进程,必须调用 for t in threads_or_processes: t.join() #等待直到该线程或进程结束 #join的作用是阻塞进程,直到所有的线程执行完毕之后,才可以执行后边的语句 print n,Thread_Process.__name__," run job need ",time.time()-local_time if __name__=="__main__": print "Multi Threads" for i in [1,2,4]: thread_process_job(i,Thread, countdown) print "Multi Process" for i in [1,2,4]: thread_process_job(i,Process, countdown)
输出结果:
从结果中看出来,多线程时,随着线程的增多,时间反而更多;多进程随着进程的增多,时间变少。原因是python的GIL机制
GIL
当有多个线程的时候,并不是真的是并行运行的,实际上有一个锁,谁申请到了谁运行
在python的原始解释器CPython中存在着GIL(Global Interpreter Lock,全局解释器锁),因此在解释执行python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。
所以,虽然CPython的线程库直接封装了系统的原生线程,但CPython整体作为一个进程,同一时间只会有一个获得GIL的线程在跑,其他线程则处于等待状态。这就造成了即使在多核CPU中,多线程也只是做着分时切换而已。
所以它更适合处理I/O密集型的任务,不适合处理CPU密集型的任务。
不过muiltprocessing的出现,已经可以让多进程的python代码编写简化到了类似多线程的程度了。(链接:https://www.zhihu.com/question/23474039/answer/35418893)这是两个线程在运行,并不是并行,而是串行,红色的线表示在申请cpu
四个线程在运行
进程可以快,而线程反而慢的原因是,我的电脑有多个核,进程可以进行并行的,而线程在python里边还是串行的,申请cpu也需要花费时间的
三、I/O密集型任务
I/O密集型任务是诸如频繁的磁盘读取,或者通过网络进行获取数据,如爬虫
比如,第一个线程运行,然后遇到I/O请求,这个I/O请求不会满上满足你,所以就切换到线程2上进行,过了会儿,线程2也有I/O请求,所以切换到线程3,然后线程3也有I/O请求,此时线程1的I/O请求完成,然后切换到线程1运行……
举例:对韩寒博客进行爬取
步骤:
1.获取urls;
2.将urls分给不同的进程或线程(相当于分配子任务);
3.多进程/线程抓取
分析:韩寒网站是:http://blog.sina.com.cn/s/articlelist_1191258123_0_1.html
如图示,第几页,对应的相应的网址的箭头部位就变成几,所以我们用下面语句获取每一页的内容:
for i in xrange(7): #这里的extend要注意,是在list后边接上 #list.extend(seq):在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表) #str(i+1),因为我们从网站中可以看到,每一页的变化,第一页网站的此处就是1,第二页就是2,以此类推,一共七页 urls.extend(parseTarget('http://blog.sina.com.cn/s/articlelist_1191258123_0_'+str(i+1)+'.html'))
总的程序如下所示:
#!/usr/bin/env python # coding: utf-8 #copyRight by heibanke import urllib import os import re from threading import Thread from multiprocessing import Process import time def downloadURL(urls,dirpath): """ urls: 需要下载的url列表 dirpath: 下载的本地路径 """ for url in urls: if len(url)>0: #print "current process id is ",os.getpid() content = urllib.urlopen(url).read() if not os.path.exists(dirpath): os.makedirs(dirpath) #dirpath实际上是文件夹,例如是1Process,+后边的文件夹中的文件名字 #把从网址中读到的所有信息存到该文件中 #url[-26:]是取url的倒数26个,'w'是写模式 open(dirpath+r'/'+url[-26:],'w').write(content) def parseTarget(url): """ 根据目标url获取文章列表的urls """ urls=[] content=urllib.urlopen(url).read() #{.*} -- 尽可能多的吸取匹配字符串 (贪婪模式) #{.*?} -- 只要一匹配到,就不再往后吸取字符 (懒惰模式) pattern = r'<a title=(.*?) href="(.*?)">' hrefs = re.findall(pattern,content) for href in hrefs: urls.append(href[1])#只把href读取出来 return urls def thread_process_job(n, Thread_or_Process, url_list, job): """ n: 多线程或多进程数 Thread_Process: Thread/Process类 job: countdown任务 """ local_time=time.time() threads_or_processes = [Thread_or_Process(target=job,args=(url_list[i],str(n)+Thread_or_Process.__name__)) for i in xrange(n)] for t in threads_or_processes: t.start() for t in threads_or_processes: t.join() print n,Thread_or_Process.__name__," run job need ",time.time()-local_time if __name__=="__main__": t=time.time() urls=[]#urls是列表,和numpy中的array要区分开 for i in xrange(7): #这里的extend要注意,是在list后边接上 #list.extend(seq):在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表) #str(i+1),因为我们从网站中可以看到,每一页的变化,第一页网站的此处就是1,第二页就是2,以此类推,一共七页 urls.extend(parseTarget('http://blog.sina.com.cn/s/articlelist_1191258123_0_'+str(i+1)+'.html')) url_len = len(urls) print "total urls number is ",url_len for n in [8,4,2,1]: #将urls分割到url_list url_list=[] #从Python2.2开始,增加了一个操作符 // ,以执行地板除://除法不管操作数为何种数值类型, #总是会舍去小数部分,返回数字序列中比真正的商小的最接近的数字。 url_split_len = url_len//n for i in xrange(n): if i==n-1: #和上边的extend区别开来 #list.append(obj):在列表末尾添加新的对象,所以它被作为单独整体加入的 #extend是飞散成一个一个的被加入的,这里注意区别 url_list.append(urls[i*url_split_len:url_len]) else: url_list.append(urls[i*url_split_len:(i+1)*url_split_len]) #分割任务后创建线程 #url_list_len=len(url_list) #print "total urls_list number is ",url_list_len thread_process_job(n,Thread, url_list, downloadURL) thread_process_job(n,Process, url_list, downloadURL) print "All done in ",time.time()-t
输出结果显示:
>>> total urls number is 315 8 Thread run job need 33.6749999523 8 Process run job need 33.5950000286 4 Thread run job need 40.2200000286 4 Process run job need 90.7750000954 2 Thread run job need 86.0289998055 2 Process run job need 87.0989999771 1 Thread run job need 131.422999859 1 Process run job need 123.995000124 All done in 629.394000053 >>>
由于网速等原因,时间上会有起伏。
上述代码有一个地方有问题,就是在生成目录的时候,有可能会发生一个进程/线程在创建着目录,另一个进程/线程发现没有目录,然后也创建目录的情况。
四、LOCK锁
我们可以用lock锁来保护公共资源。
还是上边用的生产者和消费者模型
没有lock的时候:
#!/usr/bin/env python # coding: utf-8 #copyRight by heibanke import time import random import threading #当还剩下0个产品时,则不进行消费,待生产者生产 #当生产了100个产品时,则不进行生产,待消费者消费 #生产者 class Producer(threading.Thread): def __init__(self, product,filename): self.product = product self.file = filename threading.Thread.__init__(self) def run(self): while len(self.product)<100: tmp = random.randint(0,10) self.product.append(tmp) print "add %d, product = %s" %(tmp,str(self.product)) fp=open(self.file,'a') fp.write("add %d, product = %s " %(tmp,str(self.product))) fp.close() time.sleep(0.1) #time.sleep(random.randrange(5)) #消费者 class Consumer(threading.Thread): def __init__(self, product, filename): self.product = product self.file = filename threading.Thread.__init__(self) def run(self): while True: if len(self.product)>0: tmp = self.product[0] del self.product[0] print 'consum %d, product = %s'%(tmp,str(self.product)) fp=open(self.file,'a') fp.write('consum %d, product = %s '%(tmp,str(self.product))) fp.close() time.sleep(0.1) #time.sleep(random.randrange(4)) if __name__ == '__main__': product = [] #产品初始化时为0 for i in range(5):#五个生产者 p = Producer(product,'log.txt') p.start() for i in range(3):#三个消费者 s = Consumer(product,'log.txt') s.start()
会出错。
有锁的时候:
1 #!/usr/bin/env python 2 # coding: utf-8 3 #copyRight by heibanke 4 5 import time 6 import random 7 import threading 8 9 #当还剩下0个产品时,则不进行消费,待生产者生产 10 #当生产了100个产品时,则不进行生产,待消费者消费 11 12 lock = threading.Condition() 13 14 #生产者 15 class Producer(threading.Thread): 16 def __init__(self, lock, product,filename): 17 self._lock = lock 18 self.product = product 19 self.file = filename 20 threading.Thread.__init__(self) 21 22 def run(self): 23 while True: 24 if self._lock.acquire(): 25 if len(self.product) >= 100: 26 self._lock.wait() 27 else: 28 tmp = random.randint(0,10) 29 self.product.append(tmp) 30 print "add %d, product = %s" %(tmp,str(self.product)) 31 fp=open(self.file,'a') 32 fp.write("add %d, product = %s " %(tmp,str(self.product))) 33 fp.close() 34 self._lock.notify() 35 self._lock.release() 36 time.sleep(0.1) 37 #time.sleep(random.randrange(5)) 38 39 40 #消费者 41 class Consumer(threading.Thread): 42 def __init__(self, lock, product, filename): 43 self._lock = lock 44 self.product = product 45 self.file=filename 46 threading.Thread.__init__(self) 47 48 def run(self): 49 while True: 50 if self._lock.acquire(): 51 if len(self.product)== 0: 52 self._lock.wait() 53 else: 54 tmp = self.product[0] 55 del self.product[0] 56 print 'consum %d, product =%s'%(tmp,str(self.product)) 57 fp=open(self.file,'a') 58 fp.write('consum %d, product = %s '%(tmp,str(self.product))) 59 fp.close() 60 self._lock.notify() 61 self._lock.release() 62 time.sleep(0.1) 63 #time.sleep(random.randrange(4)) 64 65 66 if __name__ == '__main__': 67 product = [] #产品初始化时为0 68 for i in range(5): 69 p = Producer(lock,product,'log_lock.txt') 70 p.start() 71 72 for i in range(3): 73 s = Consumer(lock,product,'log_lock.txt') 74 s.start()