python:多任务(线程、进程、协程)
一、线程
1、创建线程
# 创建线程 import threading,time def task1(): for i in range(5): print('task1 -- 任务:%s' % i) time.sleep(1) def task2(): for j in range(10): print('task2 -- 任务:%s' % j) time.sleep(1) def main(): # 创建线程 t1 = threading.Thread(target=task1) t2 = threading.Thread(target=task2) # 执行线程 t1.start() t2.start() if __name__ == '__main__': main()
2、查看线程数量
import threading,time # 创建线程 import threading,time def task1(): for i in range(5): print('task1 -- 任务:%s' % i) time.sleep(1) def task2(): for j in range(5): print('task2 -- 任务:%s' % j) time.sleep(1) def main(): # 创建线程 t1 = threading.Thread(target=task1) t2 = threading.Thread(target=task2) # 执行线程 t1.start() t2.start() while True: # 打印当前线程情况 thread_num = len(threading.enumerate()) print("当前线程的数量为:" + str(thread_num)) print('线程详情:') print(threading.enumerate()) if thread_num <= 1: # 当剩下一个线程的情况下(也就是只剩下主线程的情况下,退出) break time.sleep(0.5) if __name__ == '__main__': main()
3、通过继承Thread来创建线程
import threading,time class MyThread(threading.Thread): # 当线程对象调用start()方法的时候,就会自动调用run()方法 def run(self): for i in range(5): # self.name:当前线程的名字 print("I'm " + self.name + ' @ ' + str(i)) time.sleep(1) if __name__ == '__main__': # 创建线程 t1 = MyThread() t2 = MyThread() # 运行线程 t1.start() t2.start()
4、多线程共享全局变量
import threading,time # 定义全局变量 g_num = 100 def task1(): global g_num g_num += 1 print("---- in task1: g_num = %d" % g_num) def task2(): print("---- in task2: g_num = %d" % g_num) def main(): t1 = threading.Thread(target=task1) t2 = threading.Thread(target=task2) t1.start() time.sleep(1) t2.start() time.sleep(1) print("---- in main: g_num = %d" % g_num) if __name__ == '__main__': main()
5、args 参数
import threading,time # 定义全局变量 g_nums = [10, 20] def task1(temp): temp.append(30) print("---- in task1: temp = %s" % str(temp)) def task2(temp): print("---- in task1: temp = %s" % str(temp)) def main(): # args 参数:指定调用函数的时候,传递什么参数过去(args 是元组类型) t1 = threading.Thread(target=task1, args=(g_nums,)) t2 = threading.Thread(target=task2, args=(g_nums,)) t1.start() time.sleep(1) t2.start() time.sleep(1) print("---- in task1: g_nums = %s" % str(g_nums)) if __name__ == '__main__': main()
6、互斥锁解决资源竞争
import threading,time # 定义一个全局变量 g_num = 0 # 创建一个互斥锁 mutex = threading.Lock() def task1(num): global g_num for i in range(num): # 上锁 mutex.acquire() g_num += 1 # 解锁 mutex.release() print('---- in stak1: g_num = %d' % g_num) def task2(num): global g_num for i in range(num): # 上锁 mutex.acquire() g_num += 1 # 解锁 mutex.release() print('---- in stak2: g_num = %d' % g_num) if __name__ == '__main__': # 创建线程 t1 = threading.Thread(target=task1, args=(1000000,)) t2 = threading.Thread(target=task2, args=(1000000,)) # 执行线程 t1.start() t2.start() time.sleep(2) print('---- in main: g_num = %d' % g_num)
7、多线程udp 聊天器
import threading,socket def recv_msg(udp_socket): # 接收数据 while True: recv_data = udp_socket.recvfrom(1024) print('接收到的数据为:%s' % str(recv_data)) def send_msg(udp_socket, dest_ip, dest_port): while True: send_data = input('请输入你要发送的数据:') udp_socket.sendto(send_data.encode('utf-8'), (dest_ip, dest_port)) def main(): # 创建套接字 udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定本地信息 udp_socket.bind(("", 8899)) # 发送数据 dest_ip = "192.168.1.9" dest_port = 8899 # 创建线程 t_recv = threading.Thread(target=recv_msg, args=(udp_socket,)) t_send = threading.Thread(target=send_msg, args=(udp_socket,dest_ip,dest_port)) t_recv.start() t_send.start() if __name__ == '__main__': main()
8、总结
""" 1、什么是线程? 线程操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 2、线程运行: 通过 t = threading.Thread(target=xxx) 来创建线程。 通过 t.start() 来运行线程。如果没有调用 start() 方法,线程是不会执行的。 3、查看线程数量 可通过 threading.enumerate() 来查看线程数量 4、通过继承Thread来创建线程时,需要重写run()方法。子线程对象在调用start()方法的时候,run()方法就会自动调用。 5、共享全局变量: 1)在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据 2)缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全) 6、传递参数: 创建线程的时候,可以设置参数 args,来指定调用函数的时候,需要传递什么参数。args 的数据类型是元组。 7、互斥锁 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。 8、死锁 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。 避免死锁: 1)程序设计时要尽量避免(银行家算法) 2)添加超时时间等 """
二、进程
1、创建进程
import multiprocessing,time def task1(): while True: print('---- task1 ----') time.sleep(1) def task2(): while True: print('---- task2 ----') time.sleep(1) def main(): # 创建进程对象 p1 = multiprocessing.Process(target=task1) p2 = multiprocessing.Process(target=task2) # 开启进程 p1.start() p2.start() if __name__ == '__main__': main()
2、通过队列完成进程间通信
import multiprocessing def download_data(q): """模拟下载数据""" data = [11, 22, 33, 44] # 向队列中写入数据 for temp in data: print('写入数据到队列:' + str(temp)) q.put(temp) print('------下载器已下载完并写入到队列当中了------') def analysis_data(q): """模拟数据处理""" waiting_data = list() while True: # 从队列中获取数据 temp = q.get() print('从队列中获取数据:' + str(temp)) waiting_data.append(temp) # 判断队列是否为空 if q.empty(): break print('----数据已经分析完毕----') print(waiting_data) def main(): # 创建队列 q = multiprocessing.Queue() # 创建进程 p1 = multiprocessing.Process(target=download_data, args=(q,)) p2 = multiprocessing.Process(target=analysis_data, args=(q,)) # 开启进程 p1.start() p2.start() if __name__ == '__main__': main()
3、进程池
import multiprocessing import os,time,random def worker(index): t_start = time.time() print("{}开始执行,进程号为:{}".format(index, os.getpid())) time.sleep(random.random()*2) t_stop = time.time() print("{} 执行完毕,耗时{}".format(index, round(t_stop-t_start, 2))) def main(): # 定义一个进程池,最大进程数量为 3 po = multiprocessing.Pool(3) for i in range(10): # Pool().apply_async(要调用的目标,(传递给目标的参数元祖,)) # 每次循环将会用空闲出来的子进程去调用目标 po.apply_async(func=worker, args=(i,)) print('-'*10 + 'start' + '-'*10) # 关闭进程池,关闭后po不再接收新的请求 po.close() # 等待po中所有子进程执行完成,必须放在close语句之后 po.join() # 保证进程池中的所有进程都执行完毕后,主进程才会结束 print('-'*10 + 'end' + '-'*10) if __name__ == '__main__': main()
4、多任务拷贝文件夹
import multiprocessing,os # 拷贝文件函数 def copy_file(q, file_name, old_folder_name, new_folder_name): old_file = os.path.join(old_folder_name, file_name) new_file = os.path.join(new_folder_name, file_name) with open(old_file, 'rb') as f: while True: # 最多一次取出1024个字节 content = f.read(1024) if content: with open(new_file, 'ab') as f2: f2.write(content) else: break # 如果拷贝完了文件,那么就放入队列中,表示文件已拷贝完成 q.put(file_name) def main(): # 1、获取用户要拷贝的文件夹名称 old_folder_name = input("请输入你要拷贝的文件夹名称:") # 2、创建一个新的文件夹 new_folder_name = "[复制]" + old_folder_name; try: os.mkdir(new_folder_name) except FileExistsError: pass # 3、获取文件夹中所有的文件名称 file_names = os.listdir(old_folder_name) # 4、创建进程池 po = multiprocessing.Pool(5) # 5、创建进程池队列,通过队列来计算进度(进程间通信) q = multiprocessing.Manager().Queue() # 6、向进程池中添加复制文件的任务 for file_name in file_names: po.apply_async(func=copy_file, args=(q, file_name, old_folder_name, new_folder_name)) po.close() # po.join() # 7、获取队列中的内容,计算已经完成拷贝的文件数量 all_file_num = len(file_names) copy_file_num = 0 while True: file_name = q.get() copy_file_num += 1 print("\r当前拷贝文件夹的进度为:%.2f %%"%(copy_file_num*100/all_file_num), end='') if copy_file_num >= all_file_num: print() break print('Done.') if __name__ == '__main__': main()
总结:
""" 1、什么是进程 程序:例如xxx.py这是程序,是一个静态的 进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。 不仅可以通过线程完成多任务,进程也是可以的 2、进程的状态: 就绪、执行、等待 3、如何创建 需使用multiprocessing模块来创建,创建的时候,和线程一样,需要指定要执行的任务是什么,例如: p1 = multiprocessing.Process(target=task1) 4、进程间如何通信? 进程间可通过队列Queue 来进行通信。 5、进程池 当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程。 但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。 6、进程和线程的对比 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ 线程,能够完成多任务,比如 一个QQ中的多个聊天窗口 进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 区别: 1)一个程序至少有一个进程,一个进程至少有一个线程。 2)线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高 3)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率 4)线线程不能够独立执行,必须依存在进程中 5)线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。 """
三、协程
1、迭代器
""" 迭代是访问集合元素的一种方式。 迭代器是一个可以记住遍历的位置的对象。 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。 总结: 1、如果想要一个对象成为 一个可迭代的对象,即可以使用for循环,那么必须实现 __iter__ 方法 2、要构造一个迭代器,就必须实现 __iter__ 方法和 __next__ 方法 3、迭代器抛出 StopIteration 异常,for 循环就会自动停止 """ from collections.abc import Iterable,Iterator import time class Classmate(object): def __init__(self): self.names = list() self.current_index = 0 def add(self, name): self.names.append(name) def __iter__(self): # 如果想要一个对象成为 一个可迭代的对象,即可以使用for循环,那么必须实现 __iter__ 方法 return self def __next__(self): # 迭代器:要构造一个迭代器,就必须实现 __iter__ 方法和 __next__ 方法 if self.current_index < len(self.names): result = self.names[self.current_index] self.current_index += 1 return result else: # 抛出 StopIteration 异常,for 循环就会自动停止 raise StopIteration classmate = Classmate() # 判断对象是否可迭代 print('对象是否可迭代:', isinstance(classmate, Iterable)) # 获取对象的迭代器 classmate_iter = iter(classmate) print('对象是否是迭代器:', isinstance(classmate_iter, Iterator)) classmate.add('张三') classmate.add('李四') classmate.add('王五') classmate.add('赵六') for name in classmate: # classmate:可迭代,并且迭代器就是本身 # name:打印name,其实质是调用迭代器的 __next__ 方法 print(name) time.sleep(1)
2、迭代器应用——斐波那契数列
# 斐波那契数列:0,1,1,2,3,5,....,a,b,a+b class Fibonacci(object): def __init__(self, count): self.count = count self.index = 0 self.a = 0 self.b = 1 def __iter__(self): return self def __next__(self): if self.index < self.count: result = self.a self.a, self.b = self.b, self.a + self.b self.index += 1 return result else: raise StopIteration fibo = Fibonacci(10) for num in fibo: print(num)
3、迭代器的其他应用
""" list、tuple 也可接收可迭代的对象 比如:list(a):先生成一个空的列表,然后根据a获取 a 的迭代器,再通过next方法获取各个值,最后返回列表 """ class Fibonacci(object): def __init__(self, count): self.count = count self.index = 0 self.a = 0 self.b = 1 def __iter__(self): return self def __next__(self): if self.index < self.count: result = self.a self.a, self.b = self.b, self.a + self.b self.index += 1 return result else: raise StopIteration fibo = Fibonacci(10) list1 = list(fibo) print(list1) tuple1 = (1,2,3,4) list2 = list(tuple1) print(list2) list3 = [1,2,3,4] tuple2 = tuple(list3) print(tuple2)
4、生成器——创建方式 1
""" 生成器是一种特殊的迭代器 创建方式一:只要把一个列表生成式的 [ ] 改成 ( ) """ list1 = [x*2 for x in range(10)] print(list1) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] list2 = (x*2 for x in range(10)) print(list2) # <generator object <genexpr> at 0x101bafad0> for num in list2: print(num) # 0 / 2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 18
5、生成器——创建方式 2
""" 生成器创建方式二:使用 yield 如果一个函数中有yield 语句,那么这个就不再是一个函数,而是一个生成器。 执行的程序遇到yield: 1)保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起 2)将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用 下次再执行的时候,就会从yield 语句挂起那开始执行,不再是在函数的最开始位置执行。 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)。 """ # 打印斐波那契数列 def create_fibonacci(count): a, b = 0, 1 index = 0 while index < count: # 如果一个函数中有yield 语句,那么这个就不再是一个函数,而是一个生成器 # 执行的程序遇到yield,则会在 yield 语句中暂停,并且把这个值返回。 # 下次再执行的时候,就会从yield 语句后面开始执行,不再是在函数的最开始位置执行。 yield a a, b = b, a+b index += 1 return "ok." obj = create_fibonacci(10) print(obj) # <generator object create_fibonacci at 0x108ffaa50> print(type(obj)) # <class 'generator'>:类型是生成器,生成器是一个特殊的迭代器,那么,就可以通过for 循环来获取值 for num in obj: print(num) # 0 / 1 / 1 / 2 / 3 / 5 / 8 / 13 / 21 / 34 print('='*40) obj2 = create_fibonacci(2) while True: try: num = next(obj2) print(num) except Exception as ex: # ex.value:获取生成器上的"ok."字符串 print(ex.value) break # print(next(obj)) # print(next(obj)) # print(next(obj)) # print('='*40) # obj2 = create_fibonacci(2) # print(next(obj2)) # print(next(obj2))
6、使用send()方法来唤醒生成器
""" 我们除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。 使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据。 """ # 打印斐波那契数列 def create_fibonacci(count): a, b = 0, 1 index = 0 while index < count: ret2 = yield a print(">>>ret>>>", ret2) a, b = b, a+b index += 1 obj = create_fibonacci(10) # 第一次运行,执行到 yield 的时候,将 yield 后面的表达式返回,即将 a 返回。 # 所以就打印出:0 ret = next(obj) print(ret) # 0 # 通过 send 方法传递参数来唤醒 yield 时,程序将在上次断点的位置开始执行,即:ret2 = yield a。 # 这个时候,传递过来的参数,将会将 yield a 替换成传递过来的参数 'hello world!',然后返回给 ret2。 # 所以就接着打印出:>>>ret>>> hello world! # 然后程序会再次循环到 yield,将 yield 后面的表达式返回,即将 a 返回。所以得到的结果是:1 # 所以就打印出:1 ret = obj.send('hello world!') print(ret)
7、使用 yield 实现多任务
""" 了解 """ import time def task1(): while True: print('-'*10 + '1' + '-'*10) time.sleep(0.5) # 使用yield,此时这个函数已经不再是函数,而是一个生成器 yield def task2(): while True: print('-'*15 + '2' + '-'*15) time.sleep(0.5) yield def main(): count = 0 t1 = task1() t2 = task2() while True: count += 1 # 这是一个假的多任务 # 实现了 2 个任务交替执行,也就是并发执行的。 next(t1) next(t2) if count > 5: break if __name__ == '__main__': main()
8、使用 greenlet 实现多任务
""" 了解:greenlet相当于对 yield 进行了封装,不再需要使用 yield 来创建生成器 """ from greenlet import greenlet import time def task1(): while True: print('-'*10 + '1' + '-'*10) # 切换到gr2中运行 gr2.switch() time.sleep(0.5) def task2(): while True: print('-'*15 + '2' + '-'*15) # 切换到gr1中运行 gr1.switch() time.sleep(0.5) gr1 = greenlet(task1) gr2 = greenlet(task2) # 切换到gr1中运行 gr1.switch()
9、使用gevent协程完成多任务
""" gevent 相当于是对 greenlet 再次进行了封装,即:greenlet 对yield 进行了封装,gevent 对 greenlet 进行了封装 """ import gevent,time from gevent import monkey # 1、将程序中耗时的代码,换为 gevent中自己实现的代码 monkey.patch_all() def work(current_name): for i in range(5): print(current_name, i) time.sleep(0.5) # 2.等待gevent.joinall这个方法里面的所有协程列表都执行完毕,程序才会到 joinall后面的方法 gevent.joinall([ # 创建协程 gevent.spawn(work, 'work1'), gevent.spawn(work, 'work2'), ]) print('Done.')
10、案例:通过gevent实现图片下载器
import gevent from gevent import monkey import uuid, hashlib # 1、将程序中耗时的代码,换为 gevent中自己实现的代码 monkey.patch_all() # import requests需放在key.patch_all()之后,不然会有警告! import requests # 不重复名称的文件名 def get_unique_str(): uuid_str = str(uuid.uuid4()) md5 = hashlib.md5() md5.update(uuid_str.encode('utf-8')) return md5.hexdigest() # 下载图片 def download_image(image_url): print('准备请求图片:' + image_url) # 图片名称 imagename = get_unique_str() + "." + image_url.split('.')[-1] res = requests.get(image_url) if res.status_code == 200: content = res.content # 保存图片 with open('./images/' + imagename, 'wb') as f: f.write(content) print('图片已下载到本地:' + imagename) else: print('图片请求失败:' + image_url) def main(): imgurls = [ "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3865738611,3013226268&fm=26&gp=0.jpg", "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3057356668,282499874&fm=26&gp=0.jpg" ] tasks = [] for imgur in imgurls: # 添加协程 tasks.append(gevent.spawn(download_image, imgur)) # 执行任务 gevent.joinall(tasks) print('Done.') if __name__ == '__main__': main()
11、案例:爬取百度图片
from gevent import monkey # 将程序中耗时的代码,换为 gevent中自己实现的代码 monkey.patch_all() from gevent.queue import Queue import gevent import requests import re import uuid, hashlib import os # 图片列表队列 works = Queue() def get_unique_str(): """不重复名称的文件名""" uuid_str = str(uuid.uuid4()) md5 = hashlib.md5() md5.update(uuid_str.encode('utf-8')) return md5.hexdigest() def download_image(): """下载图片""" while not works.empty(): image_url = works.get_nowait() print('准备请求图片:' + image_url) # 图片名称 imagename = get_unique_str() + "." + image_url.split('.')[-1] res = requests.get(image_url) if res.status_code == 200: content = res.content image_folder = "./images/" if not os.path.isdir(image_folder): os.makedirs(image_folder) # 保存图片 with open('./images/' + imagename, 'ab') as f: f.write(content) print('图片已下载到本地:' + imagename) else: print('图片请求失败:' + image_url) def main(): # 1.获取图片地址 word = "美女" url = "http://image.baidu.com/search/flip?tn=baiduimage&ie=utf-8&word={}&pn=0".format(word) print('正请求图片地址...') res = requests.get(url) if res.status_code == 200: html = res.text # 利用正则表达式找到图片url pattern = '"objURL":"([^\s]*?(jpge|jpg|png|PNG|JPG))"' pic_urls = re.findall(pattern, html, re.S) for pic_url in pic_urls: # print(pic_url) works.put_nowait(pic_url[0]) else: print('图片列表访问失败!') # 设置协程去下载图片 tasks = list() for i in range(5): tasks.append(gevent.spawn(download_image)) # 执行任务 gevent.joinall(tasks) print('Done.') if __name__ == '__main__': main()
12、总结
""" 协程:在处理等待某些资源的同时,去做其他的任务。 进程、线程、协程对比: 进程是资源分配的单位 线程是操作系统调度的单位 进程切换需要的资源很最大,效率很低 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下) 协程切换任务资源很小,效率高 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发 """