顺序执行任务(串行)
想象一下,你有100张图片,你需要将每一张图片压缩一下,那么这个任务可以使用下面的代码来进行执行
def compress(picture_id): print('压缩图片') tasks = [1,2,3,4,5,6] for picture_id in tasks: compress(picture_id)
我们可以使用一个for循环,来每次迭代一张图片,然后使用compress来进行压缩。假设压缩一张图片需要2s,那么100张图片将需要200s。可能200s对你而言还没有那么长。但如果你有一万张图片呢?这种顺序执行的方式将会带来极大的时间成本。
并行执行任务
在python中,有下面几种方式可以加速你的代码
- 多线程(
threading
) - 多进程(
multiprocessing
) - 协程(
asyncio
)
线程和进程是通过操作系统来进行调度的。所谓调度,简单理解就是操作系统可以告诉哪个线程/进程可以使用CPU来进行计算,哪些需要暂时休眠的过程。而协程则是是一种并行的编程模型,它不需要操作系统来参与调度,而是由不同的语言来进行实现,他没有调度成本,比如Golang语言可以被大量的使用,其中有一个很重要的因素就是Golang中优秀的协程。这篇文章主要介绍python中的线程和进程的使用。在python中由于存在GIL锁的缘故,在任一时刻,一个进程下面,程序只能存在一个正在执行的线程,而进程则没有限制。这样听来,是不是突然觉得python中的线程好像没有那么大用,其实不然。线程一般处理一个IO密集型的数据,虽然当前线程不能使用CPU,但是他仍然可以从硬盘中读取/写入内容,这样一来,也可以达到多个线程同时运行的效果。在操作系统层面,进程和线程存在下面的关系,每个CPU核上面只能同时运行一个进程,而一个进程中可以同时运行多个线程。不同的进程之间的数据是相互隔绝的,所以不同进程之间数据是相互隔绝的,不能直接相互通信;而同一进程下面的不同线程之间是共享数据的。
线程
线程的创建
python中线程的使用需要threading
这个官方库。使用方法有三种
- 直接使用
Thread
类来进行创建线程 - 重写
Thread
类的run
方法 - 继承
Thread
类,在初始化的时候,进行改写
def compress(picture_id): """ 压缩图片 """ if type(picture_id) is tuple: picture_id = picture_id[0] print('压缩图片 % d' % picture_id)
直接使用Thread
from threading import Thread # target即为线程内部要运行的函数 # args为函数所需要的参数,要以tuple类型传入 thread = Thread(target=compress, args=(picture_id, ))
使用继承Thread
import threading class MyThread1(threading.Thread): """ 通过继承,来重写init方法来启动线程 """ def __init__(self, func, picture_id): if type(picture_id) is int: picture_id = (picture_id, ) super().__init__(target=func, args=picture_id) thread = MyThread1(compress, picture_id)
重写run方法
class MyThread2(threading.Thread): """ 通过重写run方法,来启动线程 """ def __init__(self, picture_id): self.picture_id = picture_id super().__init__() def run(self): # 在这里直接使用compress函数,而不是通过传参的方式 compress(self.picture_id) thread = MyThread2(picture_id)
线程的启动与等待
无论使用上面三种的哪一种方法去创建线程,都可以得到一个threading.Thread
类型的对象。通过调用start
方法可以启动线程。
thread: threading.Target = MyThread2(picture_id) # 启动线程 thread.start() # 等待,直到线程运行结束(即内部的函数运行结束) # 阻塞,即等待当前线程运行结束,才会继续往下执行 thread.join() # 直到压缩完成,才会打印 print('压缩完成')
守护线程
线程之中分为主线程和子线程,子线程由主线程启动。非守护线程:当主线程启动一个子线程时,如果子线程仍在运行,则主线程会等待子线程运行结束,然后一起结束 守护线程:当主线程启动一个子线程时,同时子线程设置为守护线程,那么当主线程运行结束时,不会等待子线程,而是子线程随着主线程一起结束。
import time def need_exec_long_time(): time.sleep(10) print('执行结束') thread = threading.Thread(target=need_exec_long_time) thread.daemon = True # 设置为守护线程 thread.start() # 此时主线程已经运行结束 # 因为子线程需要sleep 10s, # 但因为子线程是守护线程,所以子线程会跟着主线程直接结束 # 上面这段代码不会打印 执行结束
修改一下上面的代码,就可以让主线程一直等着子线程运行结束,而不会直接退出
thread.daemon = False # 将子线程设置为非守护线程
同步
前面我们讲到,由于线程之间可以共享数据,那么这就引入了一个新问题——资源竞争 看下面的例子
import threading import time tasks = list(range(4)) def pop(): global tasks while tasks: time.sleep(2) print(tasks.pop()) if __name__ == '__main__': threads = [] for _ in range(2): threads.append(threading.Thread(target=pop)) for t in threads: t.start() for t in threads: t.join()
在运行上面的代码后,可能会出现图片中所出现的错误。前面我们讲过,进程和线程是操作系统来进行调度的,也就是说,任何一个正在运行的线程,都可能被操作系统暂时中断,然后启动其他线程或进程。
在while循环判断后,由于存在线程切换,所以并不能保证进入while循环后,tasks中仍存在值,我们需要保证在pop的时候,tasks中一定要含有值,这样才能正常地调用pop函数。