• python---基础知识回顾(十)进程和线程(py2中自定义线程池和py3中的线程池使用)


    一:自定义线程池的实现

    前戏:

    在进行自定义线程池前,先了解下Queue队列

    队列中可以存放基础数据类型,也可以存放类,对象等特殊数据类型

    from queue import Queue
    
    class T:
        def __init__(self,num):
            self.num = num
    
        def printf(self):
            print(self.num,id(self.num))
    
    if __name__ == "__main__":
        queue = Queue(0)
    
        num = 12
        queue.put(num)  #可以存放基础数据类型
    
    
        t = T(num)
        queue.put(t)    #可以存放对象
    
        cls = T
        queue.put(cls)  #可以存放类
    
        dt = queue.get()
        print(id(dt),dt,type(dt))   #1385649280 12 <class 'int'>
    
        dt = queue.get()
        print(id(dt),dt,type(dt))   #7652128 <__main__.T object at 0x000000000074C320> <class '__main__.T'>
    
        dt = queue.get()
        print(id(dt),dt,type(dt))   #18042264 <class '__main__.T'> <class 'type'>

    线程池应该具备的功能:

    1. 先创建线程池,
    2. 之后去获取数据的时候,若是有直接拿走
    3. 若是没有,需要去阻塞等待,直到有数据到达
    4. 线程池可以设置指定大小
    5. 满足上面“阻塞”要求,设置大小,的数据结构,我们可以使用Queue队列

    简单版本(Low,简单了解,不要使用):

    import threading
    from queue import Queue
    
    class ThreadPool(object):
        def __init__(self,max_num):
            self.queue = Queue(max_num)
            for i in range(max_num):
                self.queue.put(threading.Thread)    #存放了max_num个类名,是指向同一个内存空间的
    
        def get_thread(self):
            return self.queue.get() #获取一个类名
    
        def add_thread(self):
            self.queue.put(threading.Thread)
    
    def func(pool,i):
        print(i)
        pool.add_thread()   #在我们从队列中获取一个线程类时,我们需要去放入一个,始终保持10个
    
    if __name__ == "__main__":
        p = ThreadPool(10)  #生成10个线程在线程池
        for i in range(100):
            thread = p.get_thread()  #获取一个线程(实际上只是获取类名)
            t = thread(target=func,args=(p,i,))
            t.start()
    上面这个Low版线程池,只是使用队列来维护一个数量,能够保持阻塞这一功能。队列中只包含了线程类名(所有类名指向一个内存空间),当我们获取到类名产生的线程,无法复用,浪费空间,而且,操作复杂,不太明了

    改进版:

    需求:想要实现线程复用,首先考虑,每个线程只可以绑定一个执行函数,那么如何去执行多个任务1.可以在执行函数中去调用任务,再去执行。
    
    2.将任务放在队列中,那么线程可以在执行函数中循环从队列中获取任务,再去执行。没有任务,那么线程将阻塞,等待任务到来,继续去执行。

    实现
    1.创建队列去存放任务(无限个任务都可以)

    2.设置一个统一的执行函数,所有的线程都会从这个执行函数中去获取到任务,回调执行。
    需求:同进程池相比。
    
    1.创建线程池时,不是马上去创建所有的线程,而是当有需求的时候再去创建线程,线程数量不能超过我们所要求的。
    
    2.当任务较多,复杂,此时可能就需要去将所有的线程都生成,
    
    3.当任务简单,那我们完全不必要去生成所有的线程,这样浪费空间。我们只需要去使用开始创建的那几个线程去执行这些任务即可。

    实现
    1.我们需要一个变量去标识我们最大应该创建的线程数(没有则是可以一直创建)

    2.我们需要一个变量去标志我们创建了多少线程(一个线程列表,去存放我们真实创建的线程数generate_list)<当空闲线程没有时,会去创建一个新的线程,但是数量不会超过所设置的数量>

    3.我们需要一个变量去标识我们空闲的线程数(一个线程列表,去存放我们的空闲线程free_list)<有任务来了,我们都会去这里面获取线程>
    需求
    1.如何终止所有线程:
    实现:
    1.close方法:(当所有任务结束后)设置标识,当从任务队列中获取到该标识(StopEvent),当前线程就移出已生成线程列表,并销毁。所以我们可以向队列中加入对应线程数的停止符

    2.terminate方法:设置标识,Flag,当其为False时,终止线程(不管是否还有任务,当执行完现在的任务后将其他的任务舍弃,然后跳出任务循环),为了防止当执行terminate方法(设置了Flag后),在获取任务处
    处阻塞,在terminate方法设置完标识后加入终止符StopEvent

    实现代码:

    import threading
    import time
    from queue import Queue
    
    StopEvent = object()    #这是一个标志(可以写成其他的数据,像是NULL或者其他),标志任务停止,向队列中加入相应个数的对象,每个线程,谁获取,谁终止
    
    class ThreadPool(object):
        def __init__(self,max_num):
            self.max_num = max_num  #最多创建的线程数量(线程池最大容量)
            self.generate_list = [] #(真实已经创建的线程列表)
            self.free_list = [] #空闲的线程列表
            self.queue = Queue()    #用于存放任务(函数名,参数,回调)的队列,不对其限制大小
            self.terminal = False   #是否直接终止
    
    
        def run(self,func,args=(),callback=None):
            '''
            加任务到队列中
            :param func:
            :param args:
            :param callback:
            :return:
            '''
            evt = (func,args,callback)
            self.queue.put(evt) #将任务放入到队列中
    
            #若是空闲列表为空,且还允许再去生成线程,则去生成一个新的线程,否则等待
            if len(self.free_list) == 0 and len(self.generate_list) < self.max_num:
                self.generate_thread()  #和下面的call方法任务获取,相关联,
                # 当用户任务执行完后,线程会阻塞在队列get()处,此时free_list不为空
                # 当用户都在执行任务时,会判断,然后去生成一个新的线程
                # 当线程数量达到最大,且都在执行任务。这时任务会放在队列中,对线程不做处理
    
        def generate_thread(self):
            '''
            创建一个线程
            :return:
            '''
            t = threading.Thread(target=self.call)
            t.start()
    
        def call(self):
            '''
            线程在其中去获取任务执行(是循环去获取)
            :return:
            '''
            #获取当前线程,加入到生成的线程列表中,也可以写在generate_thread中
            current_thread = threading.currentThread
            self.generate_list.append(current_thread)
    
            #循环去取任务并执行
            event = self.queue.get()
            while event != StopEvent:
                #是元组,是任务,去解开任务包
                #开始执行任务
    
                func, args, callback = event
                status = True
                try:
                    ret = func(*args)    #获取返回结果,注意这里的参数传递,参数逆收集
                except Exception as e:
                    status = False
                    ret = e
    
                if callback is not None:
                    try:
                        callback(status, ret)  # 将结果传递给回调函数
                    except Exception as e:
                        pass
    
                #执行完毕任务
                #判断是否接受到终止命令(是在执行完当前任务后,去判断)
                if not self.terminal:
                    #注意:只需要考虑当前自己的线程,不要考虑太多,不要想其他线程
                
                    #标记:我当前是空闲的,可以放在空闲列表中
                    self.free_list.append(current_thread)
                    #又去获取任务
                    event = self.queue.get()    #阻塞去获取任务,每个线程都会阻塞生成后都会循环在此处去获取任务
                    #当前线程开始取执行任务
                    self.free_list.remove(current_thread)
                else:
                    event = StopEvent
            else:
                #不是元组,不是任务,结束当前线程
                self.generate_list.remove(current_thread)   #当移除掉所有生成的线程后,不会在进入上面的循环,不会被阻塞在队列中的get方法
    
        def close(self):
            '''
            添加终止符,数量和线程数一致
            :return:
            '''
            num = len(self.generate_list)
            while num:
                self.queue.put(StopEvent)
                num -= 1
    
        def termindate(self):
            self.terminal = True
            self.close()
            #直接使用terminal可能出现问题,
            #event = self.queue.get()会在这里阻塞(进入这里,发现队列中没有任何任务,也没有终止符)
            # 我们需要在向队列中加入几个终止符
    
    def work(i):
        # time.sleep(1)
        print(i)
    
    if __name__ == "__main__":
        pool = ThreadPool(10)
    
        for i in range(50):
            pool.run(func=work,args=(i,))
    
        pool.termindate()
        # pool.close()
    
    
        time.sleep(5)
        print(pool.generate_list)

     使用上下文管理器实现代码:

    上下文管理器了解

    import threading
    import time
    from queue import Queue
    import contextlib
    
    StopEvent = object()    #这是一个标志(可以写成其他的数据,像是NULL或者其他),标志任务停止,向队列中加入相应个数的对象,每个线程,谁获取,谁终止
    
    class ThreadPool(object):
        def __init__(self,max_num):
            self.max_num = max_num  #最多创建的线程数量(线程池最大容量)
            self.generate_list = [] #(真实已经创建的线程列表)
            self.free_list = [] #空闲的线程列表
            self.queue = Queue()    #用于存放任务(函数名,参数,回调)的队列,不对其限制大小
            self.terminal = False   #是否直接终止
    
    
        def run(self,func,args=(),callback=None):
            '''
            加任务到队列中
            :param func:
            :param args:
            :param callback:
            :return:
            '''
            evt = (func,args,callback)
            self.queue.put(evt) #将任务放入到队列中
    
            #若是空闲列表为空,且还允许再去生成线程,则去生成一个新的线程,否则等待
            if len(self.free_list) == 0 and len(self.generate_list) < self.max_num:
                self.generate_thread()  #和下面的call方法任务获取,相关联,
                # 当用户任务执行完后,线程会阻塞在队列get()处,此时free_list不为空
                # 当用户都在执行任务时,会判断,然后去生成一个新的线程
                # 当线程数量达到最大,且都在执行任务。这时任务会放在队列中,对线程不做处理
    
        def generate_thread(self):
            '''
            创建一个线程
            :return:
            '''
            t = threading.Thread(target=self.call)
            t.start()
    
        def call(self):
            '''
            线程在其中去获取任务执行(是循环去获取)
            :return:
            '''
            #获取当前线程,加入到生成的线程列表中,也可以写在generate_thread中
            current_thread = threading.currentThread
            self.generate_list.append(current_thread)
    
            #循环去取任务并执行
            event = self.queue.get()
            while event != StopEvent:
                #是元组,是任务,去解开任务包
                #开始执行任务
    
                func, args, callback = event
                status = True
                try:
                    ret = func(*args)    #获取返回结果,注意这里的参数传递,参数逆收集
                except Exception as e:
                    status = False
                    ret = e
    
                if callback is not None:
                    try:
                        callback(status, ret)  # 将结果传递给回调函数
                    except Exception as e:
                        pass
    
                #执行完毕任务
                #判断是否接受到终止命令(是在执行完当前任务后,去判断)
                if not self.terminal:
                    #注意:只需要考虑当前自己的线程,不要考虑太多,不要想其他线程
                
                    with self.worker_state(self.free_list,current_thread):
                        event = self.queue.get()    #阻塞去获取任务,每个线程都会阻塞生成后都会循环在此处去获取任务
    
                else:
                    event = StopEvent
            else:
                #不是元组,不是任务,结束当前线程
                self.generate_list.remove(current_thread)   #当移除掉所有生成的线程后,不会在进入上面的循环,不会被阻塞在队列中的get方法
    
        def close(self):
            '''
            添加终止符,数量和线程数一致
            :return:
            '''
            num = len(self.generate_list)
            while num:
                self.queue.put(StopEvent)
                num -= 1
    
        def termindate(self):
            self.terminal = True
            self.close()
            #直接使用terminal可能出现问题,
            #event = self.queue.get()会在这里阻塞(进入这里,发现队列中没有任何任务,也没有终止符)
            # 我们需要在向队列中加入几个终止符
    
        @contextlib.contextmanager
        def worker_state(self,state_list,worker_thread):
            '''
            用来记录线程中正在等待的线程数
            :param state_list:
            :param worker_thread:
            :return:
            '''
            state_list.append(worker_thread)
            try:
                yield 
            finally:
                state_list.remove(worker_thread)
    
    def work(i):
        # time.sleep(1)
        print(i)
    
    if __name__ == "__main__":
        pool = ThreadPool(10)
    
        for i in range(50):
            pool.run(func=work,args=(i,))
    
        pool.termindate()
        # pool.close()
    
    
        time.sleep(5)
        print(pool.generate_list)

     二:py3中的concurrent.futures模块提供线程池

    前戏:

    py3.2以后,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutorProcessPoolExecutor两个类,实现了对threadingmultiprocessing的更高级的抽象,对编写线程池/进程池提供了直接的支持。 

    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

    查看两个类源码:发现都是继承于Executor基类(使用方法也是相似的)

    class ThreadPoolExecutor(_base.Executor):
    class ProcessPoolExecutor(_base.Executor):

    Executor是一个抽象类,它不能被直接使用。它为具体的异步执行定义了一些基本的方法。

    class Executor(object):
        def submit(self, fn, *args, **kwargs):   #必须重载 
            raise NotImplementedError()
    
        def map(self, fn, *iterables, timeout=None):
            if timeout is not None:
                end_time = timeout + time.time()
    
            fs = [self.submit(fn, *args) for args in zip(*iterables)]
            def result_iterator():
                try:
                    for future in fs:
                        if timeout is None:
                            yield future.result()
                        else:
                            yield future.result(end_time - time.time())
                finally:
                    for future in fs:
                        future.cancel()
            return result_iterator()
    
        def shutdown(self, wait=True):
            pass
    
        def __enter__(self):
            return self
    
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            self.shutdown(wait=True)
            return False

    线程池的使用

    1.简单使用

    from concurrent.futures import ThreadPoolExecutor
    import time,os
    
    
    def run(msg):
        time.sleep(3)
        print(msg)
    
    
    # 1.创建一个线程池
    pool = ThreadPoolExecutor(10)
    
    # 2.添加任务,开始执行
    for i in range(10): pool.submit(run,i)
    # 3.等待线程结束,默认也是等待 pool.shutdown(wait
    =True)

    2.获取返回值执行状态

    from concurrent.futures import ThreadPoolExecutor
    import time,os
    
    
    def run(msg):
        time.sleep(3)
        print(msg)
        return "hh"+str(msg)
    
    
    # 创建一个线程池
    pool = ThreadPoolExecutor(10)
    pool_list = []
    
    
    for i in range(10):
        p = pool.submit(run,i)
        pool_list.append(p)
    
    
    #time.sleep(5)  #用来测试线程任务是否执行(在执行done方法时)
    
    for item in pool_list:
        print(item.done())
        #返回这个任务是否执行"""Returns True if the future has finished running."""
        #若是未执行:返回false,执行过了返回true
        # print(item.result())    
    #获取返回值,会阻塞直到获取了返回值,才会去执行下一个循环 pool.shutdown(wait
    =True)

    3.使用map方法:用来保证执行的结果是有序的(按照任务)

    def map(self, fn, *iterables, timeout=None, chunksize=1):
        Returns an iterator equivalent to map(fn, iter).
    fn:回调函数
    iterables:可迭代参数
    from concurrent.futures import ThreadPoolExecutor as Pool
    import requests
    
    URLS = ['http://www.baidu.com', 'http://qq.com', 'http://sina.com']
    
    
    def task(url, timeout=10):
        return requests.get(url, timeout=timeout)
    
    
    pool = Pool(max_workers=3)
    results = pool.map(task, URLS)
    
    for ret in results:
        print('%s, %s' % (ret.url, len(ret.content)))
    http://www.baidu.com/, 2381
    http://www.qq.com/, 248768
    http://sina.com/, 22766
    输出结果

    尽管获取的数据大小不一致,但是返回的顺序是按照任务顺序一致

     4.使用add_done_callback回调方法,处理线程的结果数据

    from concurrent.futures import ThreadPoolExecutor
    import requests
    
    URLS = [
        'http://www.baidu.com',
        'http://qq.com',
        'http://sina.com',
        'http://www.cnblogs.com/ssyfj/',
        'http://www.ckook.com/forum.php'
    ]
    
    def task(url):
        reponse = requests.get(url)
        return reponse
    
    def callback(future):
        reponse = future.result()   #result方法中存放的是线程执行结果
        print(reponse.status_code)
        print(reponse.content)
    
    
    pool = ThreadPoolExecutor(3)
    for url in URLS:
        res = pool.submit(task,url)
        res.add_done_callback(callback)
    
    pool.shutdown(wait=True)

     三:其中提供的进程池的使用是一样的

    from concurrent.futures import ProcessPoolExecutor
    import requests
    
    URLS = [
        'http://www.baidu.com',
        'http://qq.com',
        'http://sina.com',
        'http://www.cnblogs.com/ssyfj/',
        'http://www.ckook.com/forum.php'
    ]
    
    def task(url):
        reponse = requests.get(url)
        return reponse
    
    def callback(future):
        reponse = future.result()   #result方法中存放的是线程执行结果
        print(reponse.status_code)
        print(reponse.content)
    
    
    if __name__ == "__main__":
        pool = ProcessPoolExecutor(3)
        for url in URLS:
            res = pool.submit(task, url)
            res.add_done_callback(callback)
    
        pool.shutdown(wait=True)

    注意:如果我们没有写入__name__ == "__main__"

    RuntimeError: 
            An attempt has been made to start a new process before the
            current process has finished its bootstrapping phase.
    
            This probably means that you are not using fork to start your
            child processes and you have forgotten to use the proper idiom
            in the main module:
    
                if __name__ == '__main__':
                    freeze_support()
                    ...
    
            The "freeze_support()" line can be omitted if the program
            is not going to be frozen to produce an executable.

     推文:Python中if __name__=="__main__" 语句在调用多进程Process过程中的作用分析

    对于多进程而言,由于Python运行过程中,新创建进程后,进程会导入正在运行的文件,即在运行代码0.1的时候,代码在运行到mp.Process时,新的进程会重新读入改代码,
    对于没有if __name__=="__main__"保护的代码,新进程都认为是要再次运行的代码,
    这时子进程又一次运行mp.Process,但是在multiprocessing.Process的源码中是对子进程再次产生子进程是做了限制的,是不允许的,于是出现如上的错误提示。
    简而言之,就是在python中子进程不知道那些代码是他可以执行的。
    我们如果不使用__name__=="__main__"的话,子进程也会去执行生成属于子进程的子进程,这是一个死循环,解释器是不允许的,所以出错
  • 相关阅读:
    网站代码优化总结
    移动端 H5 页面注意事项
    js基础知识点收集
    2017-3-26 webpack入门(一)
    gulp教程
    less的使用
    微信小程序接口封装
    div上下左右居中几种方式
    前端知识点-面试
    call和apply
  • 原文地址:https://www.cnblogs.com/ssyfj/p/9022472.html
Copyright © 2020-2023  润新知