├── .gitignore ├── vthread ├── __init__.py ├── test3.py ├── test2.py ├── test.py └── vthread.py ├── LICENSE ├── setup.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vthread.egg-info/ 2 | __pycache__/ 3 | dist/ 4 | build/ -------------------------------------------------------------------------------- /vthread/__init__.py: -------------------------------------------------------------------------------- 1 | from .vthread import * 2 | 3 | __all__ = vthread.__all__ 4 | __title__ = 'vthread' 5 | __description__ = 'the best threadpool pack.' 6 | __version__ = '0.1.4' 7 | __author__ = 'vilame' 8 | __author_email__ = 'opaquism@hotmail.com' 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cilame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vthread/test3.py: -------------------------------------------------------------------------------- 1 | import time, random, queue 2 | from vthread import pool, lock 3 | 4 | ls1 = queue.Queue() 5 | ls2 = queue.Queue() 6 | producer = 'pr' 7 | consumer1 = 'co1' 8 | consumer2 = 'co2' 9 | 10 | @pool(6, gqueue=producer) 11 | def creater(num): 12 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 13 | num1, num2 = num, num*num+1000 14 | print("数据进入队列: num:{}".format(num)) 15 | ls1.put(num1) 16 | ls2.put(num2) 17 | 18 | # 两个消费者 19 | @pool(1, gqueue=consumer1) 20 | def coster1(): 21 | while not pool.check_stop(gqueue=producer): 22 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 23 | pp = [ls1.get() for _ in range(ls1.qsize())] 24 | print('当前消费的列表 list: {}'.format(pp)) 25 | @pool(1, gqueue=consumer2) 26 | def coster2(): 27 | while not pool.check_stop(gqueue=producer): 28 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 29 | pp = [ls2.get() for _ in range(ls2.qsize())] 30 | print('当前消费的列表 list: {}'.format(pp)) 31 | for i in range(30): creater(i) 32 | coster1() 33 | coster2() 34 | 35 | pool.waitall() 36 | print('当生产和消费的任务池数据都结束后,这里才会打印') 37 | print('current queue 1 size:{}'.format(ls1.qsize())) 38 | print('current queue 2 size:{}'.format(ls2.qsize())) 39 | print('end') -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from setuptools import setup, find_packages 3 | import sys 4 | 5 | setup( 6 | name="vthread", 7 | version="0.1.5", 8 | author="vilame", 9 | author_email="opaquism@hotmail.com", 10 | description="the best threadpool pack.", 11 | long_description=""" 12 | You can implement thread pools by adding a single line of code without changing the order of any previous code. 13 | =============================================================================================================== 14 | 15 | .. code-block:: python 16 | 17 | import vthread 18 | 19 | @vthread.pool(3) # just use this line to make pool, Create a threadpool with three threads 20 | def crawl(i): 21 | import time;time.sleep(1) # Simulation time consuming 22 | print("crawl_url:",i) 23 | 24 | urls = ["http://url1", 25 | "http://url2", 26 | "http://url3", 27 | "http://url4"] 28 | 29 | for u in urls: 30 | crawl(u) # This function becomes a function that adds the original function to the thread pool. 31 | 32 | 33 | It provides a method for grouping the thread pool 34 | ================================================= 35 | 36 | .. code-block:: python 37 | 38 | import vthread 39 | pool_1 = vthread.pool(5,gqueue=1) # open a threadpool with 5 threads named 1 40 | pool_2 = vthread.pool(2,gqueue=2) # open a threadpool with 2 threads named 2 41 | 42 | @pool_1 43 | def foolfunc1(num): 44 | time.sleep(1) 45 | print(f"foolstring1, test3 foolnumb1:{num}") 46 | 47 | @pool_2 48 | def foolfunc2(num): 49 | time.sleep(1) 50 | print(f"foolstring2, test3 foolnumb2:{num}") 51 | 52 | @pool_2 53 | def foolfunc3(num): 54 | time.sleep(1) 55 | print(f"foolstring3, test3 foolnumb3:{num}") 56 | 57 | for i in range(10): foolfunc1(i) 58 | for i in range(4): foolfunc2(i) 59 | for i in range(2): foolfunc3(i) 60 | # default gqueue is 0 61 | """, 62 | long_description_content_type="text/markdown", 63 | license="MIT", 64 | url="https://github.com/cilame/vthread", 65 | packages=['vthread'], 66 | classifiers=[ 67 | "Environment :: Web Environment", 68 | "Intended Audience :: Developers", 69 | "Operating System :: OS Independent", 70 | ] 71 | ) 72 | -------------------------------------------------------------------------------- /vthread/test2.py: -------------------------------------------------------------------------------- 1 | import vthread 2 | # 生成两个线程池装饰器来并行处理多线程的 “请求” 和 “写入” 两种功能 3 | pool_gets = vthread.pool(8,gqueue=1)# 线程池1 多线程请求任务 4 | pool_save = vthread.pool(1,gqueue=2)# 线程池2 数据写入文件,只开一个线程的性能足够处理写入任务 5 | 6 | 7 | import os, re, json, time, queue, traceback 8 | import requests 9 | from lxml import etree 10 | datapipe = queue.Queue() # 不同线程之间用管道传递数据 11 | @pool_gets 12 | def crawl(page): 13 | url = 'https://www.baidu.com/s?wd=123&pn={}'.format(page*10) 14 | headers = { 15 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 16 | "Accept-Encoding": "gzip, deflate, ", # auto delete br encoding. cos requests and scrapy can not decode it. 17 | "Accept-Language": "zh-CN,zh;q=0.9", 18 | "Cache-Control": "no-cache", 19 | "Connection": "keep-alive", 20 | "Cookie": ( 21 | "BAIDUID=AEFC0BF73865D50E79A88E1D572BDFFC:FG=1; " 22 | ), 23 | "Host": "www.baidu.com", 24 | "Pragma": "no-cache", 25 | "Referer": "https://www.baidu.com/", 26 | "Sec-Fetch-Mode": "navigate", 27 | "Sec-Fetch-Site": "same-origin", 28 | "Sec-Fetch-User": "?1", 29 | "Upgrade-Insecure-Requests": "1", 30 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36" 31 | } 32 | s = requests.get(url, headers=headers) 33 | if s.history and s.history[0].status_code == 302: 34 | print('retrying {}. curr statcode:{}'.format(s.history[0].request.url, s.status_code)) 35 | crawl(page) # 百度现在有验证码问题,直接重新提交任务即可。 36 | return 37 | tree = etree.HTML(s.content.decode('utf-8')) 38 | for x in tree.xpath('//div/h3[@class="t"]/parent::*'): 39 | d = {} 40 | d["href"] = x.xpath('./h3/a[1][@target]/@href')[0] 41 | d["title"] = x.xpath('string(./h3[@class="t"])').strip() 42 | datapipe.put(d) 43 | @pool_save 44 | def save_jsonline(): 45 | def check_stop(): 46 | try: ret = vthread.pool.check_stop(gqueue=1) # 检查1号线程池的任务是否全部请求结束 47 | except: ret = False; print(traceback.format_exc()) # 当你不用 vthread 时也能运行的保底处理 48 | return ret 49 | timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) # 年月日_时分秒 50 | filename = 'v{}.json'.format(timestamp) # 输出文件名(这里使用jsonlines格式存储) 51 | with open(filename, "a", encoding="utf-8") as f: 52 | while True: 53 | for _ in range(datapipe.qsize()): 54 | data = datapipe.get() 55 | print('write: {}'.format(data)) 56 | f.write(json.dumps(data, ensure_ascii=False)+"\n") 57 | time.sleep(.25) 58 | if check_stop() and datapipe.qsize() == 0: 59 | break 60 | 61 | # 提交 n 个网页请求的任务,然后开启写入任务 62 | for page in range(30): crawl(page) 63 | save_jsonline() 64 | # 虽然这里的处理按照该脚本代码的逻辑可以不必让 save_jsonline 变成线程执行,不过数据保存也写成线程池的好处就是 65 | # 如果存在多个不同的表格存储,你就可以按照类似的方式, 新加一个数据管道,新加一个保存函数进行多文件并行存储 66 | 67 | 68 | # 由于被 vthread.pool 装饰的函数变成了任务提交函数, 69 | # 所以在提交完任务时候很快就执行到下一行而不会等待任务执行完 70 | # 所以如果需要等待任务执行完再执行下一行内容的话,需要使用 vthread.pool.wait(gqueue) 来处理 71 | # 不过 vthread 已经自动挂钩的主线程,都会等待任务结束,脚本才会完全停止,下面的两行代码视不同情使用即可 72 | # vthread.pool.wait(gqueue=1) # 等待1号线程池任务执行完 73 | # vthread.pool.wait(gqueue=2) # 等待2号线程池任务执行完 74 | # print('end') -------------------------------------------------------------------------------- /vthread/test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import vthread 3 | 4 | 5 | 6 | 7 | 8 | # vthread.thread 9 | #========# 10 | # 多线程 # 11 | #========# 12 | # eg.1 13 | @vthread.thread(5) # 只要这一行就能让函数变成开5个线程执行同个函数 14 | def foolfunc(num): 15 | time.sleep(1) 16 | print(f"foolstring, test1 foolnumb: {num}") 17 | 18 | foolfunc(123) # 加入装饰器后,这个函数就变成了开5个线程执行的函数了 19 | 20 | # eg.2 21 | # 为了使函数执行更独立可以用 vthread.thread(1) 来装饰 22 | # 但是为了使用更为简便 这里的 vthread.thread 等同于 vthread.thread(1) 23 | @vthread.thread 24 | def foolfunc(num): 25 | time.sleep(1) 26 | print(f"foolstring, test1 foolnumb: {num}") 27 | 28 | for i in range(5): 29 | foolfunc(123) # 执行与数量分离,可以使得参数传递更为动态 30 | 31 | # 注意: 32 | # 这种本身就用于简单测试的方法不要将带参数和不带参数的thread装饰器混用! 33 | # 可能会造成装饰出现问题。 34 | 35 | 36 | 37 | 38 | 39 | # vthread.pool 40 | #========# 41 | # 线程池 # 42 | #========# 43 | @vthread.pool(6) # 只用加这一行就能实现6条线程池的包装 44 | def foolfunc(num): 45 | time.sleep(1) 46 | print(f"foolstring, test2 foolnumb: {num}") 47 | 48 | for i in range(10): 49 | foolfunc(i) # 加入装饰器后,这个函数变成往伺服线程队列里塞原函数的函数了 50 | 51 | # 不加装饰就是普通的单线程 52 | # 只用加一行就能不破坏原来的结构直接实现线程池操作,能进行参数传递 53 | 54 | 55 | 56 | 57 | 58 | # vthread.pool 59 | #==============# 60 | # 多组的线程池 # 61 | #==============# 62 | pool_1 = vthread.pool(5,gqueue=1) # 开5个伺服线程,组名为1 63 | pool_2 = vthread.pool(2,gqueue=2) # 开2个伺服线程,组名为2 64 | 65 | @pool_1 66 | def foolfunc1(num): 67 | time.sleep(1) 68 | print(f"foolstring1, test3 foolnumb1:{num}") 69 | 70 | @pool_2 # foolfunc2 和 foolfunc3 用gqueue=2的线程池 71 | def foolfunc2(num): 72 | time.sleep(1) 73 | print(f"foolstring2, test3 foolnumb2:{num}") 74 | @pool_2 # foolfunc2 和 foolfunc3 用gqueue=2的线程池 75 | def foolfunc3(num): 76 | time.sleep(1) 77 | print(f"foolstring3, test3 foolnumb3:{num}") 78 | 79 | for i in range(10): foolfunc1(i) 80 | for i in range(10): foolfunc2(i) 81 | for i in range(10): foolfunc3(i) 82 | # 额外开启线程池组的话最好不要用gqueue=0 83 | # 因为gqueue=0就是默认参数 84 | 85 | 86 | 87 | 88 | 89 | # 有时候你需要把某些操作进行原子化 90 | # 可以把你要原子化的操作写成函数,用vthread.atom装饰就行 91 | #==========# 92 | # 加锁封装 # 93 | #==========# 94 | @vthread.thread(5) 95 | def foolfunc_(): 96 | 97 | @vthread.atom # 将函数加锁封装 98 | def do_some_fool_thing1(): 99 | pass # do_something 100 | @vthread.atom # 将函数加锁封装 101 | def do_some_fool_thing2(): 102 | pass # do_something 103 | 104 | # 执行时就会实现原子操作 105 | do_some_fool_thing1() 106 | do_some_fool_thing2() 107 | 108 | 109 | 110 | 111 | 112 | # 另外: 113 | # 为了便于调试函数在任意第一次装饰过后会对 print 打猴子补丁 114 | # 自带的 print 函数变成带锁的函数了,还加了些打印线程名字的操作 115 | # 可以通过 vthread.toggle 函数对这些或其他一些功能进行关闭 116 | # 也可以用 vthread.unpatch_all() 直接将 print 还原成系统默认函数 117 | # 更多详细内容可以 help(vthread) 118 | 119 | # 额外细节: 120 | # 如果想将自己的某些函数进行原子操作的封装可以考虑用 @vthread.atom 装饰那个函数 121 | # 如果你想用原函数的话,你可以用 vthread.orig_func["foolfunc1"] 获得原函数地址 122 | # vthread.orig_func 就是一个包装【原函数名字】对应【原函数地址】的字典。 123 | # 虽然 vthread.atom 可以实现原子操作 124 | # 这里仍然将 lock 暴露出去,用 vthread.lock 就可以拿到这个唯一的线程锁实体 125 | 126 | # 为了不用使用者收尾: 127 | # 当使用者装饰任意数量的线程池的时候,都会默认只开一个不计入数量的线程MainMonitor 128 | # 就是监视主线程执行情况,一旦主线程执行完就向线程队列注入相应数量的停止标记 129 | # 因为该线程池的原理就是让主线程变成派发函数的进程,执行到尾部自然就代表 130 | # 分配的任务已经分配完了,这时就可以注入停止标记让线程执行完就赶紧结束掉 131 | # 因为是队列操作不会影响线程效率,只是为了防止在命令行下控制权不交还的情况。 132 | # 当然在之前设计的时候是可以用人为在所有代码最后执行一次 vthread.pool_close_all() 即可解决。 133 | # 但是为了更易用,为了不让使用者在代码最后添加这一句话,就设计了这个监控线程 134 | 135 | 136 | 137 | ''' 138 | #==========================# 139 | # 测试同组线程池数量的切换 # 140 | #==========================# 141 | @vthread.pool(10) 142 | def foolfunc1(): 143 | time.sleep(1) 144 | print("foolstring1") 145 | 146 | @vthread.pool(18) 147 | def foolfunc2(): 148 | time.sleep(1) 149 | print("foolstring2") 150 | 151 | time.sleep(1.5) 152 | # 关闭线程池也需要时间,因为默认不是默认线程join, 153 | # 所以这里不等等的话,主线程调用 threading.active_count() 可能会计算进将关未关的线程数 154 | import threading 155 | print(threading.active_count()) 156 | ''' 157 | 158 | 159 | 160 | 161 | 162 | ''' 163 | #==============================================# 164 | # # 165 | # 注意!关于多个函数装饰器,线程池数量的定义 # 166 | # # 167 | #==============================================# 168 | # 相同的gqueue,默认使用最后一个 "人为定义" 的伺服线程数量 169 | # eg.1 170 | @vthread.pool(10) 171 | def foolfunc1(): 172 | pass 173 | @vthread.pool(18) 174 | def foolfunc1(): 175 | pass 176 | # 这样就意味着gqueue=0的线程池数量为18 177 | 178 | # eg.2 179 | @vthread.pool(10) 180 | def foolfunc1(): 181 | pass 182 | @vthread.pool() 183 | def foolfunc1(): 184 | pass 185 | # 这样就意味着gqueue=0的线程池数量为10 186 | 187 | # eg.3 188 | @vthread.pool() 189 | def foolfunc1(): 190 | pass 191 | @vthread.pool() 192 | def foolfunc1(): 193 | pass 194 | # 这样就意味着gqueue=0的线程池数量为默认的cpu核心数 195 | 196 | # eg.4 197 | pool1 = vthread.pool(gqueue=1) 198 | pool2 = vthread.pool(6,gqueue=2) 199 | pool2 = vthread.pool(8,gqueue=2) 200 | 这样就意味着gqueue=1的线程池数量为默认的cpu核心数 201 | 这样就意味着gqueue=2的线程池数量为8 202 | ''' 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## python 多线程函数库 vthread ,简而强大 2 | 3 | - ##### 安装 4 | ``` 5 | C:\Users\Administrator> pip3 install vthread 6 | ``` 7 | - ##### 线程池(核心功能) 8 | 不加装饰器就是普通的单线程,只用加一行就能在不破坏原来的结构直接实现线程池操作,能进行参数传递,支持分组,这已经到了不破坏代码的极限了。 9 | ```python 10 | import time 11 | import vthread 12 | 13 | @vthread.pool(6) # 只用加这一行就能实现6条线程池的包装 14 | def foolfunc(num): 15 | time.sleep(1) 16 | print(f"foolstring, test2 foolnumb: {num}") 17 | 18 | for i in range(10): 19 | foolfunc(i) # 加入装饰器后,这个函数变成往伺服线程队列里塞原函数的函数了 20 | 21 | # 不加装饰就是普通的单线程 22 | # 只用加一行就能不破坏原来的代码结构直接实现线程池操作,能进行参数传递 23 | 24 | 执行效果如下: 25 | [ Thread-1_0 ] foolstring, test2 foolnumb: 0 26 | [ Thread-3_0 ] foolstring, test2 foolnumb: 2 27 | [ Thread-6_0 ] foolstring, test2 foolnumb: 5 28 | [ Thread-2_0 ] foolstring, test2 foolnumb: 1 29 | [ Thread-5_0 ] foolstring, test2 foolnumb: 4 30 | [ Thread-4_0 ] foolstring, test2 foolnumb: 3 31 | [ Thread-2_0 ] foolstring, test2 foolnumb: 9 32 | [ Thread-3_0 ] foolstring, test2 foolnumb: 7 33 | [ Thread-6_0 ] foolstring, test2 foolnumb: 8 34 | [ Thread-1_0 ] foolstring, test2 foolnumb: 6 35 | ``` 36 | - ##### 支持分组线程池 37 | 如果你想要让你的某几个函数有M个线程执行,而另外几个函数要N个线程去执行。 38 | 那么请看看下面的使用说明 39 | ``` 40 | import time 41 | import vthread 42 | 43 | pool_1 = vthread.pool(5,gqueue=1) # 开5个伺服线程,组名为1 44 | pool_2 = vthread.pool(2,gqueue=2) # 开2个伺服线程,组名为2 45 | 46 | @pool_1 47 | def foolfunc1(num): 48 | time.sleep(1) 49 | print(f"foolstring1, test3 foolnumb1:{num}") 50 | 51 | @pool_2 # foolfunc2 和 foolfunc3 用gqueue=2的线程池 52 | def foolfunc2(num): 53 | time.sleep(1) 54 | print(f"foolstring2, test3 foolnumb2:{num}") 55 | @pool_2 # foolfunc2 和 foolfunc3 用gqueue=2的线程池 56 | def foolfunc3(num): 57 | time.sleep(1) 58 | print(f"foolstring3, test3 foolnumb3:{num}") 59 | 60 | for i in range(10): foolfunc1(i) 61 | for i in range(10): foolfunc2(i) 62 | for i in range(10): foolfunc3(i) 63 | # 额外开启线程池组的话最好不要用gqueue='v' 64 | # 因为gqueue='v'就是默认参数 65 | ``` 66 | 67 | - ##### 原子封装 68 | 如果需要考虑对函数内的某些步骤进行锁的操作,那么请看下面的使用说明。 69 | ``` 70 | # 有时候你需要把某些操作进行原子化 71 | # 可以把你要原子化的操作写成函数,用vthread.atom装饰就行 72 | import time 73 | import vthread 74 | 75 | @vthread.pool(5) 76 | def foolfunc_(): 77 | 78 | @vthread.atom # 将函数加锁封装 79 | def do_some_fool_thing1(): 80 | pass # do_something 81 | @vthread.atom # 将函数加锁封装 82 | def do_some_fool_thing2(): 83 | pass # do_something 84 | 85 | # 执行时就会实现原子操作 86 | do_some_fool_thing1() 87 | do_some_fool_thing2() 88 | ``` 89 | - ##### 等待执行完毕再继续任务 90 | 再某些情况下需要等待线程池任务完成之后再继续后面的操作,请看如下使用。 91 | ``` 92 | # 可以使用 vthread.pool.wait 函数来等待某一组线程池执行完毕再继续后面的操作 93 | # 该函数仅有一个默认参数 gqueue='v',需要等待的分组。 94 | # 该函数的本质就是一个定时循环内部使用 vthread.pool.check_stop 函数不停检测某个任务组是否结束。 95 | # check_stop 函数返回结果为 0 则为线程池已执行结束。 96 | # 如果有比 wait 更丰富的处理请使用 check_stop 。 97 | import time 98 | import vthread 99 | 100 | @vthread.pool(5) 101 | def foolfunc_(): 102 | time.sleep(1) 103 | print(123) 104 | for i in range(10): foolfunc_() 105 | 106 | vthread.pool.wait() # 等待gqueue='v'分组线程执行完毕再继续后面的代码 107 | # vthread.pool.waitall() # 当你的程序执行过程比较单调时,可以考虑等待全部线程池都执行完再往后继续。 108 | print('end.') 109 | ``` 110 | - ##### 一个简单的一边生产一边消费的代码 111 | ``` 112 | import time, random, queue 113 | from vthread import pool, lock 114 | 115 | ls = queue.Queue() 116 | producer = 'pr' 117 | consumer = 'co' 118 | 119 | @pool(6, gqueue=producer) 120 | def creater(num): 121 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 122 | print("数据进入队列: {}".format(num)) 123 | ls.put(num) 124 | @pool(1, gqueue=consumer) 125 | def coster(): 126 | # 这里之所以使用 check_stop 是因为,这里需要边生产边消费 127 | while not pool.check_stop(gqueue=producer): 128 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 129 | pp = [ls.get() for _ in range(ls.qsize())] 130 | print('当前消费的列表 list: {}'.format(pp)) 131 | 132 | for i in range(30): creater(i) 133 | coster() # 写作逻辑限制了这里的数量 134 | pool.wait(gqueue=producer) # 等待默认的 gqueue=producer 组线程池全部停止再执行后面内容 135 | pool.wait(gqueue=consumer) # 等待默认的 gqueue=consumer 组线程池全部停止再执行后面内容 136 | print('当生产和消费的任务池数据都结束后,这里才会打印') 137 | print('current queue size:{}'.format(ls.qsize())) 138 | print('end') 139 | ``` 140 | - ##### 多个消费者并行处理的代码会变得简单 141 | ``` 142 | import time, random, queue 143 | from vthread import pool, lock 144 | 145 | ls1 = queue.Queue() 146 | ls2 = queue.Queue() 147 | producer = 'pr' 148 | consumer1 = 'co1' 149 | consumer2 = 'co2' 150 | 151 | @pool(6, gqueue=producer) 152 | def creater(num): 153 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 154 | num1, num2 = num, num*num+1000 155 | print("数据进入队列: num:{}".format(num)) 156 | ls1.put(num1) 157 | ls2.put(num2) 158 | 159 | # 两个消费者 160 | @pool(1, gqueue=consumer1) 161 | def coster1(): 162 | while not pool.check_stop(gqueue=producer): 163 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 164 | pp = [ls1.get() for _ in range(ls1.qsize())] 165 | print('当前消费的列表 list: {}'.format(pp)) 166 | @pool(1, gqueue=consumer2) 167 | def coster2(): 168 | while not pool.check_stop(gqueue=producer): 169 | time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 170 | pp = [ls2.get() for _ in range(ls2.qsize())] 171 | print('当前消费的列表 list: {}'.format(pp)) 172 | for i in range(30): creater(i) 173 | coster1() 174 | coster2() 175 | 176 | pool.waitall() # 当需要简单等待全部任务结束再执行某些任务时,这样处理即可,这个等于下面注释中的内容。 177 | # pool.wait(gqueue=producer) 178 | # pool.wait(gqueue=consumer1) 179 | # pool.wait(gqueue=consumer2) 180 | print('当生产和消费的任务池数据都结束后,这里才会打印') 181 | print('current queue 1 size:{}'.format(ls1.qsize())) 182 | print('current queue 2 size:{}'.format(ls2.qsize())) 183 | print('end') 184 | ``` 185 | 186 | 187 | 188 | - ## 百度爬虫,线程池示例脚本 189 | 简单使用该库写一个简单的多线程 “请求”和 “写入文件”并行处理的百度爬虫作为使用范例。这里的代码仍然是注释装饰器则为正常单线程,添加装饰器则自动变成多线程池处理。 190 | ```python 191 | import vthread 192 | # 生成两个线程池装饰器来并行处理多线程的 “请求” 和 “写入” 两种功能 193 | pool_gets = vthread.pool(8,gqueue=1)# 线程池1 多线程请求任务 194 | pool_save = vthread.pool(1,gqueue=2)# 线程池2 数据写入文件,只开一个线程的性能足够处理写入任务 195 | 196 | 197 | import os, re, json, time, queue, traceback 198 | import requests 199 | from lxml import etree 200 | datapipe = queue.Queue() # 不同线程之间用管道传递数据 201 | @pool_gets 202 | def crawl(page): 203 | url = 'https://www.baidu.com/s?wd=123&pn={}'.format(page*10) 204 | headers = { 205 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 206 | "Accept-Encoding": "gzip, deflate, ", # auto delete br encoding. cos requests and scrapy can not decode it. 207 | "Accept-Language": "zh-CN,zh;q=0.9", 208 | "Cache-Control": "no-cache", 209 | "Connection": "keep-alive", 210 | "Cookie": ( 211 | "BAIDUID=AEFC0BF73865D50E79A88E1D572BDFFC:FG=1; " 212 | ), 213 | "Host": "www.baidu.com", 214 | "Pragma": "no-cache", 215 | "Referer": "https://www.baidu.com/", 216 | "Sec-Fetch-Mode": "navigate", 217 | "Sec-Fetch-Site": "same-origin", 218 | "Sec-Fetch-User": "?1", 219 | "Upgrade-Insecure-Requests": "1", 220 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36" 221 | } 222 | s = requests.get(url, headers=headers) 223 | if s.history and s.history[0].status_code == 302: 224 | print('retrying {}. curr statcode:{}'.format(s.history[0].request.url, s.status_code)) 225 | crawl(page) # 百度现在有验证码问题,直接重新提交任务即可。 226 | return 227 | tree = etree.HTML(s.content.decode('utf-8')) 228 | for x in tree.xpath('//div/h3[@class="t"]/parent::*'): 229 | d = {} 230 | d["href"] = x.xpath('./h3/a[1][@target]/@href')[0] 231 | d["title"] = x.xpath('string(./h3[@class="t"])').strip() 232 | datapipe.put(d) 233 | @pool_save 234 | def save_jsonline(): 235 | def check_stop(): 236 | try: ret = vthread.pool.check_stop(gqueue=1) # 检查1号线程池的任务是否全部请求结束 237 | except: ret = False; print(traceback.format_exc()) # 当你不用 vthread 时也能运行的保底处理 238 | return ret 239 | timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) # 年月日_时分秒 240 | filename = 'v{}.json'.format(timestamp) # 输出文件名(这里使用jsonlines格式存储) 241 | with open(filename, "a", encoding="utf-8") as f: 242 | while True: 243 | for _ in range(datapipe.qsize()): 244 | data = datapipe.get() 245 | print('write: {}'.format(data)) 246 | f.write(json.dumps(data, ensure_ascii=False)+"\n") 247 | time.sleep(.25) 248 | if check_stop() and datapipe.qsize() == 0: 249 | break 250 | 251 | # 提交 n 个网页请求的任务,然后开启写入任务 252 | for page in range(30): crawl(page) 253 | save_jsonline() 254 | # 虽然这里的处理按照该脚本代码的逻辑可以不必让 save_jsonline 变成线程执行,不过数据保存也写成线程池的好处就是 255 | # 如果存在多个不同的表格存储,你就可以按照类似的方式, 新加一个数据管道,新加一个保存函数进行多文件并行存储 256 | 257 | 258 | # 由于被 vthread.pool 装饰的函数变成了任务提交函数, 259 | # 所以在提交完任务时候很快就执行到下一行而不会等待任务执行完 260 | # 所以如果需要等待任务执行完再执行下一行内容的话,需要使用 vthread.pool.wait(gqueue) 来处理 261 | # 不过 vthread 已经自动挂钩的主线程,都会等待任务结束,脚本才会完全停止,下面的两行代码视不同情使用即可 262 | # vthread.pool.wait(gqueue=1) # 等待1号线程池任务执行完 263 | # vthread.pool.wait(gqueue=2) # 等待2号线程池任务执行完 264 | # print('end') 265 | ``` 266 | 267 | - ##### 额外说明 268 | ``` 269 | # 另外: 270 | # 为了便于调试函数在任意第一次装饰过后会对 print 打猴子补丁 271 | # 自带的 print 函数变成带锁的函数了,还加了些打印线程名字的操作 272 | # 可以通过 vthread.toggle 函数对这些或其他一些功能进行关闭 273 | # 也可以用 vthread.unpatch_all() 直接将 print 还原成系统默认函数 274 | # 更多详细内容可以 help(vthread) 275 | 276 | # 额外细节: 277 | # 如果想将自己的某些函数进行原子操作的封装可以考虑用 @vthread.atom 装饰那个函数 278 | # 如果你想用原函数的话,你可以用 vthread.orig_func["foolfunc1"] 获得原函数地址 279 | # vthread.orig_func 就是一个包装【原函数名字】对应【原函数地址】的字典。 280 | # 虽然 vthread.atom 可以实现原子操作 281 | # 这里仍然将 lock 暴露出去,用 vthread.lock 就可以拿到这个唯一的线程锁实体 282 | # 可以用 vthread.pool.show 方法来查看线程池数量情况。 283 | 284 | # 为了不用使用者收尾: 285 | # 当使用者装饰任意数量的线程池的时候,都会默认只开一个不计入数量的线程MainMonitor 286 | # 就是监视主线程执行情况,一旦主线程执行完,同时所有线程池函数处于伺服状态就向线程队列注入相应数量的停止标记 287 | # 需要两个条件: 288 | # 1. 主线程执行完毕 289 | # 因为该线程池的原理就是让主线程变成派发函数的进程,执行到尾部自然就代表 290 | # 分配的任务已经分配完了,这时就可以注入停止标记让线程执行完就赶紧结束掉 291 | # 2. 每个函数都处于等待获取函数参数状态(即保证函数执行完毕) 292 | # 当线程内嵌套其他分组的线程池函数的时,被嵌套的函数在之前是有可能不执行的 293 | # 所以就设计了条件2,以确保所有需要分发的函数能够全部分发完成且执行完毕 294 | # 因为是队列操作不会影响线程效率,MainMonitor线程只是为了防止在命令行下控制权不交还的情况。 295 | # 当然在之前设计的时候是可以用人为等所有代码执行完毕最后执行一次 vthread.pool.close_all() 即可解决。 296 | # 但是为了更易用,为了不让使用者在代码最后添加这一句话,就设计了这个监控线程 297 | ``` 298 | - ##### 另外强调的困惑 299 | 假如在使用过程中装饰了多个函数会怎么计算线程池的线程数量呢? 300 | 这里给出了说明,在 vthread.pool 函数库中,是以 gqueue 这个参数来确定线程池分组的。 301 | 而相同的分组,则会默认使用最后一个 "人为定义" 的伺服线程数量。 302 | ``` 303 | #==============================================# 304 | # # 305 | # 注意!关于多个函数装饰器,线程池数量的定义 # 306 | # # 307 | #==============================================# 308 | # -------------------- eg.1 -------------------- 309 | @vthread.pool(10) 310 | def foolfunc1(): 311 | pass 312 | @vthread.pool(18) 313 | def foolfunc1(): 314 | pass 315 | # 这样就意味着gqueue='v'的线程池数量为18 316 | # -------------------- eg.2 -------------------- 317 | @vthread.pool(10) 318 | def foolfunc1(): 319 | pass 320 | @vthread.pool() 321 | def foolfunc1(): 322 | pass 323 | # 这样就意味着gqueue='v'的线程池数量为10 324 | # -------------------- eg.3 -------------------- 325 | @vthread.pool() 326 | def foolfunc1(): 327 | pass 328 | @vthread.pool() 329 | def foolfunc1(): 330 | pass 331 | 这样就意味着gqueue='v'的线程池数量为默认的cpu核心数 332 | # -------------------- eg.4 -------------------- 333 | pool1 = vthread.pool(gqueue=1) 334 | pool2 = vthread.pool(6,gqueue=2) 335 | pool2 = vthread.pool(8,gqueue=2) 336 | @pool1 337 | def foolfunc1(): 338 | pass 339 | @pool2 340 | def foolfunc1(): 341 | pass 342 | # 这样就意味着gqueue=1的线程池数量为默认的cpu核心数,gqueue=2的线程池数量为8 343 | #==============================================# 344 | # 为了避免这种定义时可能出现的问题。 345 | # 建议在多个函数需要使用线程池的情况下,最好使用 eg.4 中的处理方式: 346 | # 1 先生成装饰器对象 2 然后再装饰需要进入多线程的函数 347 | #==============================================# 348 | ``` 349 | 350 | - ##### 普通的多线程 351 | 【不建议使用】 考虑到函数库的多用性,可能是觉得这种直接粗暴的开启多线程函数的测试需求比较常见,所以会保留有这样的一个功能。 352 | ```python 353 | import time 354 | import vthread 355 | 356 | @vthread.thread(5) # 只要这一行就能让函数变成开5个线程执行同个函数 357 | def foolfunc(num): 358 | time.sleep(1) 359 | print(f"foolstring, test1 foolnumb: {num}") 360 | 361 | foolfunc(123) # 加入装饰器后,这个函数就变成了开5个线程执行的函数了 362 | 363 | # 考虑到函数库的易用性,个人觉得这种直接粗暴的开启多线程函数的测试需求比较常见 364 | # 所以才保留了这样的一个功能。 365 | 366 | 执行效果如下: 367 | [ Thread-1 ] foolstring, test1 foolnumb: 123 368 | [ Thread-2 ] foolstring, test1 foolnumb: 123 369 | [ Thread-3 ] foolstring, test1 foolnumb: 123 370 | [ Thread-4 ] foolstring, test1 foolnumb: 123 371 | [ Thread-5 ] foolstring, test1 foolnumb: 123 372 | 373 | # 为了使函数执行更独立(方便参数传递)可以用 vthread.thread(1) 来装饰 374 | # 但是为了使用更为简便 这里的 vthread.thread 等同于 vthread.thread(1) 375 | @vthread.thread 376 | def foolfunc(num): 377 | time.sleep(1) 378 | print(f"foolstring, test1 foolnumb: {num}") 379 | 380 | for i in range(5): 381 | foolfunc(123) # 执行与数量分离,可以使得参数传递更为动态 382 | 383 | # 执行效果同上 384 | # 注意: 385 | # vthread.thread 不带参数的方式只能装饰一个函数,装饰多个函数会出现问题,仅用于测试单个函数。 386 | # vthread.thread(1) 带参数的可以装饰多个函数,但是已经有了分组线程池的强大功能,为什么还要这样浪费资源呢? 387 | ``` 388 | -------------------------------------------------------------------------------- /vthread/vthread.py: -------------------------------------------------------------------------------- 1 | ''' 2 | #============================================================== 3 | # 更加方便的多线程调用,类装饰器封装,一行代码实现线程池 4 | # 5 | # 注意: 6 | # 装饰器会默认对 print 函数进行 monkey patch 7 | # 会对python自带的 print 函数加锁使 print 带有原子性便于调试 8 | # 默认打开 log 让 print 能够输出线程名字 9 | # 可以通过 toggle 函数关掉显示线程名功能(不关锁) 10 | # 可以通过执行 vthread.unpatch_all() 解除这个补丁还原 print 11 | #============================================================== 12 | ''' 13 | import time 14 | import queue 15 | import traceback 16 | from threading import Thread,Lock,RLock,\ 17 | current_thread,main_thread 18 | import builtins 19 | import functools 20 | 21 | # 兼容 isAlive 函数被完全遗弃的新版 22 | Thread.isAlive = Thread.is_alive 23 | 24 | lock = RLock() 25 | 26 | class log_flag: 27 | _decorator_toggle = True 28 | _vlog = True # print是否显示线程名字 29 | _elog = True # 是否打印错误信息 30 | 31 | 32 | # 所有被装饰的原始函数都会放在这个地方 33 | orig_func = {} 34 | 35 | _org_print = print 36 | def _new_print(*arg,**kw): 37 | lock.acquire() 38 | if log_flag._vlog: 39 | name = current_thread().getName() 40 | name = "[{}]".format(name.center(13)) 41 | _org_print(name,*arg,**kw) 42 | else: 43 | _org_print(*arg,**kw) 44 | lock.release() 45 | 46 | 47 | def toggle(toggle=False,name="thread"): 48 | ''' 49 | #============================================================== 50 | # 开关显示方式 51 | # 目前提供修改的参数有三个: 52 | # 1. "thread" # 是否在print时在最左显示线程名字 53 | # 2. "error" # 是否显示error信息 54 | #============================================================== 55 | ''' 56 | # 因为装饰器是每次装饰都会默认打开 _vlog 一次,所以添加这个参数放置 57 | # 使得这个函数一旦在最开始执行之后,装饰器就不会再打开 _vlog 了 58 | global _monitor 59 | log_flag._decorator_toggle = False 60 | if name == "thread" : log_flag._vlog = toggle 61 | if name == "error" : log_flag._elog = toggle 62 | 63 | 64 | class thread: 65 | ''' 66 | #============================================================== 67 | # 普通的多线程装饰 68 | # 69 | # >>> import vthread 70 | # >>> 71 | # >>> # 将 foolfunc 变成动态开启3个线程执行的函数 72 | # >>> @vthread.thread(3) # 默认参数:join=False,log=True 73 | # ... def foolfunc(): 74 | # ... print("foolstring") 75 | # >>> 76 | # >>> foolfunc() # 一次执行开启三个线程执行相同函数 77 | # [ Thread-1 ]: foolstring 78 | # [ Thread-2 ]: foolstring 79 | # [ Thread-3 ]: foolstring 80 | # >>> 81 | #============================================================== 82 | # 为了和pool的使用方法共通(一次函数执行只是一次函数单独执行的效果) 83 | # 这里添加了一种更为简便的装饰手段 84 | # 85 | # >>> import vthread 86 | # >>> 87 | # >>> # 将 foolfunc 变成开启新线程执行的函数 88 | # >>> @vthread.vthread # 这时的 vthread.thread 等同于 vthread.thread(1) 89 | # ... def foolfunc(): 90 | # ... print("foolstring") 91 | # >>> 92 | # >>> for i in range(4): 93 | # ... foolfunc() # 每次执行都会开启新线程,默认不join。 94 | # [ Thread-1 ]: foolstring 95 | # [ Thread-2 ]: foolstring 96 | # [ Thread-3 ]: foolstring 97 | # [ Thread-4 ]: foolstring 98 | # >>> 99 | # 100 | # 不过需要注意的是,不要将 vthread.thread 带参数和不带参数的装饰器混用 101 | # 可能会导致一些不可预知的异常。 102 | #============================================================== 103 | ''' 104 | def __init__(self,num=1,join=False,log=True): 105 | ''' 106 | #============================================================== 107 | # *args 108 | # :num 线程数量 109 | # **kw 110 | # :join 多线程是否join 111 | # :log print函数的输出时是否加入线程名作前缀 112 | #============================================================== 113 | ''' 114 | # 为了兼容不带参数的装饰方式,这里做了如下修改。 115 | if type(num)==type(lambda:None): 116 | def _no_params_func(self,*args,**kw): 117 | v = Thread(target=num,args=args,kwargs=kw) 118 | v.start() 119 | thread.__call__ = _no_params_func 120 | else: 121 | self.num = num 122 | self.join = join 123 | 124 | # 让配置在 toggle 执行变成只能手动配置 log_flag 125 | if log_flag._decorator_toggle: 126 | log_flag._vlog = log 127 | 128 | # 默认将 print 函数进行monkey patch 129 | patch_print() 130 | 131 | def __call__(self,func): 132 | ''' 133 | #============================================================== 134 | # 类装饰器入口 135 | #============================================================== 136 | ''' 137 | orig_func[func.__name__] = func 138 | @functools.wraps(func) 139 | def _run_threads(*args,**kw): 140 | p = [] 141 | for _ in range(self.num): 142 | # 这里包装一下异常捕捉,防止异常导致的不 join 143 | def _func(): 144 | try: 145 | func(*args,**kw) 146 | except Exception as e: 147 | if log_flag._elog: 148 | print(traceback.format_exc()) 149 | p.append(Thread(target=_func)) 150 | for i in p: i.start() 151 | if self.join: 152 | for i in p: i.join() 153 | return _run_threads 154 | 155 | class KillThreadParams(Exception): 156 | '''一个用来杀死进程的函数参数''' 157 | pass 158 | 159 | class pool: 160 | ''' 161 | #============================================================== 162 | # 线程池的多线程装饰 163 | # 对代码入侵较小,例如 164 | # 165 | # >>> import vthread 166 | # >>> import time 167 | # >>> 168 | # >>> # 只需要加下面这一行就可以将普通迭代执行函数变成线程池多线程执行 169 | # >>> @vthread.pool(5) # 对于 foolfunc 开启5个线程池 170 | # >>> def foolfunc(num): 171 | # ... time.sleep(1) 172 | # ... print(f"foolstring, foolnumb:{num}") 173 | # >>> 174 | # >>> # 默认参数:pool_num=None,log=True,gqueue='v' 175 | # >>> # pool_num不选时就自动选 cpu 核心数 176 | # >>> # 就是说,装饰方法还可以更简化为 @vthread.pool() 177 | # >>> 178 | # >>> for i in range(10): 179 | # ... foolfunc(i) 180 | # [ Thread-3 ] foolstring, foolnumb:1 181 | # [ Thread-2 ] foolstring, foolnumb:2 182 | # [ Thread-1 ] foolstring, foolnumb:0 183 | # [ Thread-5 ] foolstring, foolnumb:3 184 | # [ Thread-4 ] foolstring, foolnumb:4 185 | # [ Thread-3 ] foolstring, foolnumb:5 186 | # [ Thread-2 ] foolstring, foolnumb:6 187 | # [ Thread-1 ] foolstring, foolnumb:7 188 | # [ Thread-5 ] foolstring, foolnumb:8 189 | # [ Thread-4 ] foolstring, foolnumb:9 190 | # >>> # 这里的函数执行都是放在伺服线程中执行。 191 | # >>> # 如果不指定 gqueue 参数,默认是共用0号队列 192 | # >>> # 不指定 gqueue 参数给多个函数装饰的情况下 193 | # >>> # 用的都是一组伺服线程 194 | # >>> 195 | #============================================================== 196 | # 可以尝试用gqueue的参数来实现不同函数不同作用域 197 | # 开启多组伺服线程 198 | # 199 | # >>> 200 | # >>> import vthread 201 | # >>> pool1 = vthread.pool(5,gqueue=1) # 开5个伺服线程,组名为1 202 | # >>> pool2 = vthread.pool(1,gqueue=2) # 开1个伺服线程,组名为2 203 | # >>> 204 | # >>> @pool1 205 | # >>> def foolfunc1(num): 206 | # >>> time.sleep(1) 207 | # >>> print(f"foolstring1, foolnumb1:{num}") 208 | # >>> @pool2 209 | # >>> def foolfunc2(num): 210 | # >>> time.sleep(1) 211 | # >>> print(f"foolstring2, foolnumb2:{num}") 212 | # >>> 213 | # >>> for i in range(5): foolfunc1(i) 214 | # >>> for i in range(5): foolfunc2(i) 215 | # [ Thread-1 ] foolstring1, foolnumb1:0 216 | # [ Thread-3 ] foolstring1, foolnumb1:2 217 | # [ Thread-4 ] foolstring1, foolnumb1:3 218 | # [ Thread-2 ] foolstring1, foolnumb1:1 219 | # [ Thread-6 ] foolstring2, foolnumb2:0 220 | # [ Thread-5 ] foolstring1, foolnumb1:4 221 | # [ Thread-6 ] foolstring2, foolnumb2:1 222 | # [ Thread-6 ] foolstring2, foolnumb2:2 223 | # [ Thread-6 ] foolstring2, foolnumb2:3 224 | # [ Thread-6 ] foolstring2, foolnumb2:4 225 | # >>> 226 | # >>> # 通过上面的代码执行就很容易发现 227 | # >>> # pool2 装饰后的函数频率、线程数和 pool1 的不一样 228 | # >>> # 你可能某几个函数要用一个组,某几个函数用另一组 229 | # >>> # 分组功能可以更加灵活地使用线程池 230 | # >>> 231 | #============================================================== 232 | ''' 233 | 234 | _monitor = None # 监视主线程是否在运行的线程 235 | _monitor_run_num = {} # 用判断队列是否为空监视线程是否执行完毕 236 | 237 | # 默认0号作为全局函数队列 238 | _pool_queue = {} 239 | _pool_func_num = {} 240 | 241 | def __init__(self,pool_num=None,gqueue='v',log=True,monitor=True): 242 | ''' 243 | #============================================================== 244 | # **kw 245 | # :pool_num 伺服线程数量 246 | # :gqueue 全局队列表的index,默认0,建议用数字标识 247 | # :log print函数的输出时是否加入线程名作前缀 248 | #============================================================== 249 | ''' 250 | 251 | # 让配置在 toggle 函数执行后的装饰行为都变成只能手动配置 log_flag 252 | if log_flag._decorator_toggle: 253 | log_flag._vlog = log 254 | 255 | # 默认用的是全局队列 256 | if gqueue not in self._pool_queue: 257 | self._pool_queue[gqueue] = queue.Queue() 258 | self._pool = self._pool_queue[gqueue] 259 | 260 | # 默认将 print 函数进行monkey patch 261 | patch_print() 262 | 263 | # 新线程,监视主线程执行情况,一旦停止就向线程队列注入相应数量的停止标记 264 | # 因为该线程池的原理就是让主线程变成派发函数的进程,执行到尾部自然就代表 265 | # 分配的任务已经分配完了,这时就可以注入停止标记让线程执行完就赶紧结束掉 266 | # 防止在命令行下控制权不交还的情况。 267 | if monitor and not self._monitor: 268 | self.main_monitor() 269 | else: 270 | self._monitor = "close" 271 | 272 | # 在函数执行前put进该组队列,在函数执行完毕后get该组队列 273 | # 对每组函数分配进行管理,实现函数执行完毕的挂钩 274 | if gqueue not in self._monitor_run_num: 275 | self._monitor_run_num[gqueue] = queue.Queue() 276 | 277 | # 智能选择线程数量 278 | num = self._auto_pool_num(pool_num) 279 | 280 | # 这里考虑的是控制伺服线程数量,相同的gqueue以最后一个人为定义的线程池数为基准 281 | if gqueue not in self._pool_func_num: 282 | self._pool_func_num[gqueue] = num 283 | self._run(num,gqueue) 284 | else: 285 | # 是以最后一个主动设置的线程池数为基准 286 | # 所以要排除不设置的情况 287 | if pool_num is not None: 288 | self.change_thread_num(num,gqueue) 289 | 290 | def __call__(self,func): 291 | ''' 292 | #============================================================== 293 | # 类装饰器入口 294 | #============================================================== 295 | ''' 296 | orig_func[func.__name__] = func 297 | @functools.wraps(func) 298 | def _run_threads(*args,**kw): 299 | # 将函数以及参数包装进 queue 300 | self._pool.put((func,args,kw)) 301 | return _run_threads 302 | 303 | @classmethod 304 | def change_thread_num(self,num,gqueue='v'): 305 | ''' 306 | #============================================================== 307 | # 通过组名字,用来修改线程数量的函数,默认修改gqueue='v'的组 308 | # 是静态函数,你可以直接用 vthread.self.change_thread_num(3)修改 309 | # 就是简单的多退少补,用来动态修改伺服线程数量的。 310 | # 311 | # 因为原理是向线程队列注入停止标记,线程执行和线程接收停止信号是互斥安全的 312 | # 也是在设计初对任务执行完整性的一种考虑 313 | #============================================================== 314 | ''' 315 | if gqueue in self._pool_func_num: 316 | x = self._pool_func_num[gqueue] - num 317 | # 当前线程数少于最后一次定义的数量时候会增加伺服线程 318 | # 多了则会杀掉多余线程 319 | if x < 0: 320 | self._run(abs(x),gqueue) 321 | if x > 0: 322 | for _ in range(abs(x)): 323 | self._pool_queue[gqueue].put(KillThreadParams) 324 | self._pool_func_num[gqueue] = num 325 | 326 | @classmethod 327 | def _run(self,num,gqueue): 328 | ''' 329 | #============================================================== 330 | # 运行伺服线程,不指定数量则默认以 cpu 核心数作为伺服线程数量 331 | # 每个线程都等待任意函数放进队列,然后被线程抓出然后执行 332 | #============================================================== 333 | ''' 334 | # 伺服函数 335 | def _pools_pull(): 336 | ct = current_thread() 337 | name = ct.getName() 338 | ct.setName("{}_{}".format(name, gqueue)) 339 | while True: 340 | v = self._pool_queue[gqueue].get() 341 | if v == KillThreadParams: return 342 | try: 343 | func,args,kw = v 344 | self._monitor_run_num[gqueue].put('V') # 标记线程是否执行完毕 345 | func(*args,**kw) 346 | except BaseException as e: 347 | if log_flag._elog: 348 | print(traceback.format_exc()) 349 | finally: 350 | self._monitor_run_num[gqueue].get('V') # 标记线程是否执行完毕 351 | # 线程的开启 352 | for _ in range(num): Thread(target=_pools_pull).start() 353 | 354 | @classmethod 355 | def main_monitor(self): 356 | ''' 357 | #============================================================== 358 | # 对主线程进行监视的函数 359 | # 一旦主线程执行完毕就会向所有线程池函数队列尾注入停止标记 360 | # 使所有的线程在执行完任务后都停止下来 361 | # 对于命令行使用的 python 脚本尤为重要 362 | # 因为如果所有线程不停止的话,控制权就不会交还给命令窗口 363 | # 364 | # 在任意被含有该函数的装饰类装饰的情况下,这个是默认被打开的 365 | # 可以在装饰时通过设置 monitor 参数是否打开,默认以第一个装饰器设置为准 366 | #============================================================== 367 | ''' 368 | def _func(): 369 | while True: 370 | time.sleep(.25) 371 | if not main_thread().isAlive() and all(map(lambda i:i.empty(),self._monitor_run_num.values())): 372 | self.close_all() 373 | break 374 | if not self._monitor: 375 | self._monitor = Thread(target=_func,name="MainMonitor") 376 | self._monitor.start() 377 | 378 | @staticmethod 379 | def _auto_pool_num(num): 380 | if not num: 381 | try: 382 | from multiprocessing import cpu_count 383 | num = cpu_count() 384 | except: 385 | if log_flag._elog: 386 | print("cpu_count error. use default num 4.") 387 | num = 4 388 | return num 389 | 390 | @classmethod 391 | def close_by_gqueue(self,gqueue='v'): 392 | ''' 393 | #============================================================== 394 | # 通过组名关闭该组所有的伺服线程 395 | # 默认关闭gqueue='v'组的所有伺服线程 396 | #============================================================== 397 | ''' 398 | self.change_thread_num(0,gqueue) 399 | 400 | @classmethod 401 | def close_all(self): 402 | ''' 403 | #============================================================== 404 | # 关闭所有伺服线程 405 | #============================================================== 406 | ''' 407 | for i in self._pool_func_num: 408 | self.change_thread_num(0,i) 409 | 410 | @classmethod 411 | def show(self): 412 | ''' 413 | #============================================================== 414 | # 简单的打印一下当前的线程池的组数 415 | # 以及打印每一组线程池的线程数量 416 | # 417 | # >>> vthread.show() 418 | # [ MainThread ] threads group number: 3 419 | # [ MainThread ] gqueue:0, alive threads number:6 420 | # [ MainThread ] gqueue:1, alive threads number:5 421 | # [ MainThread ] gqueue:2, alive threads number:2 422 | # >>> 423 | #============================================================== 424 | ''' 425 | l = len(self._pool_func_num) 426 | print("threads group number: {}".format(l)) 427 | for i,j in self._pool_func_num.items(): 428 | print("gqueue:{}, alive threads number:{}".format(i, j)) 429 | 430 | @classmethod 431 | def waitall(self): 432 | while any([not self.check_stop(gqueue) for gqueue in self._monitor_run_num]): 433 | time.sleep(.25) 434 | 435 | @classmethod 436 | def wait(self, gqueue='v'): 437 | ''' 438 | #============================================================== 439 | # 等待任务结束,以下是实例代码 440 | # 441 | # import vthread, time 442 | # @vthread.pool(6) # 生成默认的 gqueue='v' 组线程池,6个线程 443 | # def foolfunc1(num): 444 | # time.sleep(1) 445 | # print("foolstring, test foolnumb {}".format(num)) 446 | # ls = [] 447 | # @vthread.pool(2, gqueue='h') # 生成 gqueue='h' 组线程池,2个线程 448 | # def foolfunc2(num): 449 | # time.sleep(1) 450 | # print('123123') 451 | # ls.append(num*3/2) 452 | # for i in range(30): foolfunc1(i) 453 | # for i in range(30): foolfunc2(i) 454 | # vthread.pool.wait() # 等待默认的 gqueue='v' 组线程池全部停止再执行后面内容 455 | # vthread.pool.wait(gqueue='h') # 等待默认的 gqueue='v' 组线程池全部停止再执行后面内容 456 | # print('ls:{}'.format(ls)) 457 | # print('end') 458 | #============================================================== 459 | ''' 460 | while not self.check_stop(gqueue): 461 | time.sleep(.25) 462 | 463 | @classmethod 464 | def check_stop(self, gqueue='v'): 465 | ''' 466 | #============================================================== 467 | # 该函数数用于一边生产一边消费的代码中会很方便,扩展成多消费者也很容易 468 | # 下面是示例代码 469 | # 470 | # import time, random, queue 471 | # from vthread import pool, lock 472 | # ls = queue.Queue() 473 | # producer = 'pr' 474 | # consumer = 'co' 475 | # @pool(6, gqueue=producer) 476 | # def creater(num): 477 | # time.sleep(random.random()) # 随机睡眠 0.0 ~ 1.0 秒 478 | # with lock: 479 | # print("数据进入队列: {}".format(num)) 480 | # ls.put(num) 481 | # @pool(1, gqueue=consumer) 482 | # def coster(): 483 | # # 这里之所以使用 check_stop 是因为,这里需要边生产边消费 484 | # while not pool.check_stop(gqueue=producer): 485 | # time.sleep(random.random()) # 随机睡眠 2.0 ~ 3.0 秒 486 | # pp = [ls.get() for _ in range(ls.qsize())] 487 | # print('当前消费的列表 list: {}'.format(pp)) 488 | # for i in range(30): creater(i) 489 | # coster() 490 | # pool.wait(gqueue=producer) # 等待默认的 gqueue=producer 组线程池全部停止再执行后面内容 491 | # pool.wait(gqueue=consumer) # 等待默认的 gqueue=consumer 组线程池全部停止再执行后面内容 492 | # print('当生产和消费的任务池数据都结束后,这里才会打印') 493 | # print('ls:{}'.format(ls.qsize())) 494 | # print('end') 495 | #============================================================== 496 | ''' 497 | return not (self._monitor_run_num[gqueue].qsize() or self._pool_queue[gqueue].qsize()) 498 | 499 | 500 | 501 | def atom(func): 502 | ''' 503 | #============================================================== 504 | # 对任意函数进行原子包装(加锁) 505 | #============================================================== 506 | ''' 507 | def _atom(*arg,**kw): 508 | lock.acquire() 509 | v = func(*arg,**kw) 510 | lock.release() 511 | return v 512 | return _atom 513 | 514 | def patch_print(): 515 | ''' 516 | #============================================================== 517 | # print 补丁函数 518 | # 519 | # monkey patch 式的修改 520 | # 对python内建 print 函数进行加锁 521 | # 使其在调试阶段能更方便使用 522 | #============================================================== 523 | ''' 524 | builtins.print = _new_print 525 | 526 | def unpatch_all(can_be_repatch=False): 527 | ''' 528 | #============================================================== 529 | # 去补丁函数 530 | # :can_be_repatch=False 531 | # 因为设计是在每次装饰时就会默认patch一次 532 | # 卸载后不可被重新patch的参数添加就是为了 533 | # 可以使得在头部执行这个函数后后面的装饰都不会再patch 534 | #============================================================== 535 | ''' 536 | global _new_print,_org_print 537 | builtins.print = _org_print 538 | if not can_be_repatch: 539 | _new_print = builtins.print 540 | 541 | 542 | # 函数 543 | funcs = ["thread", 544 | "pool", 545 | "atom", 546 | "patch_print", 547 | "toggle", 548 | "unpatch_all"] 549 | 550 | # 全局参 551 | values = ["orig_func", 552 | "lock"] 553 | 554 | 555 | __all__ = funcs + values 556 | --------------------------------------------------------------------------------