• python并行编程


    一、编程思想

    并行编程的思想:分而治之,有两种模型

    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()
  • 相关阅读:
    Python--面向对象编程(2)
    Python--面向对象编程(1)
    Python--常用模块
    Python--函数
    Hadoop综合大作业
    hive基本操作与应用
    MapReduce作业
    熟悉HBase基本操作
    熟悉常用的HDFS操作
    爬虫大作业(对电影的爬取)
  • 原文地址:https://www.cnblogs.com/shixisheng/p/7119217.html
Copyright © 2020-2023  润新知