多线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程必须包含一个线程。
在线程中,所有状态在默认情况下都是共享的,比如内存共享。
先来试试
python标准库包括:低级模块_thread 和高级模块 threading,绝大多数情况我们使用threading.
1 | from threading import Thread |
1 | def worker(arg): # 线程执行的目标函数 |
最简单的多线程,哪怕只做一件事,也是新开了一个线程。
线程的传参和普通函数没有区别,只是格式必须为元祖格式。
当函数执行完之后,线程也就跟着退出了。
线程退出条件
- 线程内的函数语句执行完毕,线程自动结束。(如上面的例子)
- 线程内的函数抛出未处理的异常。
1 | import threading |
上面例子中,演示了触发异常自动退出线程。但最先打印的是主程序的”===end===”语句,是因为在程序中,主线程启动一个线程后,不会等待子线程执行完毕,就继续执行了后续语句,在执行完主线程语句后,发现还有子线程没有结束,于是等待子线程执行结束,子线程在运行时抛出了未处理的异常,最终子线程结束,主线程也随之结束。
方法属性
Threading属性
threading.current_thread()
返回当前线程对象threading.main_thread()
返回主线程对象threading.active_count()
返回处于Active状态的线程个数threading.enumerate()
返回所有存活的线程的列表,不包括已经终止的线程和未启动的线程threading.get_ident()
返回当前线程的ID,非0整数
Thread实例的属性
threading.current_thread().name
线程名,只是一个标识符,可以使用getName()
、setName()
获取和运行时重命名。threading.current_thread().ident
线程ID,非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问。此ID可以重复使用threading.current_thread().is_alive()
返回线程是否存活,布尔值,True或False。
1 | import threading |
线程退出后,尝试再次启动线程时,抛出RuntimeError
异常,线程对象在定义后只能启动一次。
run和start方法
单线程情况
1 | import threading |
解释:
start(): 程序启动,运行到start时,调用start()方法启动了一个新的线程,而初始线程继续进行,判断线程存活执行了while操作,当用户键盘输入完成后,start开启的新线程结束,while亦结束。一旦退出,输出总结信息。
run(): 程序启动,运行到run时,调用InputReader类的run函数,不开启新的线程,当用户键盘输入完成后,run方法结束,所以再往下执行的时候,while不生效。
另一点,可以看出start()方法会先运行start()方法,再运行run()方法;而运行线程的run()方法只能调用到run()方法。
start() –> run() –> _target()
run() –> _target()
多线程情况
1 | import threading |
多线程情况下
- 当两个子线程都用run()方法启动时,会先运行t1.run(),运行完之后才按顺序运行t2.run(),两个线程都工作在主线程,没有启动新线程,因此,run()方法仅仅是普通函数调用。
- 当两个子线程都用start()方法启动时,start()方法启动了两个新的子线程并交替运行,线程名是我们定义的name,每个子进程ID也不同。
- join()方法,正常场景,主线程在起了一个新的子线程后,主线程和子线程是并行的,互不干扰。但是,假如主线程调用了join方法,那它就得等待子线程完全执行完毕才能执行join()之后的语句,相当于又变成了单线程按顺序执行。
全局解释器锁GIL(Global Interpreter Lock)
可能究其根本很复杂,但简单理解来说
- GIL 是CPython引入的概念,是设计之初为解决多线程之间数据完整性和状态同步,用了最简单自然的方法-加锁。后来由于代码开发者接受了这种设定,重度依赖这种特性而难以去除。
- CPython中
- IO密集型,某个线程阻塞,就会调度其他就绪线程;
- CPU密集型,当前线程可能会连续的获得GIL,导致其它线程几乎无法使用CPU。
- 在CPython中由于有GIL存在
- IO密集型,多线程
- CPU密集型,多进程
- 多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
- Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
- 由于多线程共享内存,线程并发执行的顺序又是随机无法控制的,可能会造成多个线程同时改一个变量,把内容给改乱了。可通过
threading.Lock()
锁保证某段关键代码只能由一个线程从头到尾完整地执行,但可能会造成死锁,廖雪峰多线程中有更详细的介绍。
参考
Python 多线程 start()和run()方法的区别(三)
(这一系列都蛮清晰的)