10.1. 线程简介
- 在一个进程中,若想做多个子任务,我们把每个子任务称为线程
- 线程可以理解为轻量级的进程
- 进程之间的数据是独立,而一个进程下的线程数据是共享的
- 线程是CPU分配时间的最小单位,进程和线程的调度都是操作系统的事
- 一个进程默认都有一个线程,我们称为主线程
10.2. 线程模块
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
- 低级模块(
_thread):非常简陋,不建议使用
1 | import _thread |
- 高级模块(
threading)
1 | import threading |
threading.main_thread(): 返回主线程对象。可以通过name属性获取主线程名称,默认MainThreadthreading.current_thread():返回当前线程对象,可以通过name属性获取当前线程,默认Thread-Nthreading.active_count():返回当前存活的线程个数threading.enumrate():以列表形式返回当前所有存活的Thread对象threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):构造线程对象group:应该为None,为了日后扩展ThreadGroup类实现而保留target:用于任务函数run()调用的可调用对象,默认为None,表示不需要调用任何方法name:线程名称,默认情况下,由Thread-N格式构成一个唯一的名称,其中N是小的十进制数。args:用于调用目标函数的参数元组,默认为()kwargs:是用于调用目标函数的关键字参数字典,默认为{}daemon:默认为None,线程将继承当前线程的守护模式属性;不是None,线程将被显式的设置为守护模式,不管该线程是否是守护模式
sub.start():开始线程活动sub.run():代表线程活动的方法。也可以在子类型里重载这个方法,标准的run()方法对作为target参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从args和kwargs参数分别获取的位置和关键字参数。sub.join(timeount=None):等待,直到线程结束。这会阻塞调用这个方法的线程,直到被调用join()的线程终结。当timeout不为None时,因为join()总是返回None,所以你一定要在join()后调用is_alive()才能判断是否发生超时。sub.is_active():判断线程是否存活着os.exit():用于退出当前线程os._exit():用于退出当前进程中的主线程
10.3. 数据共享
全局变量是可以共享的。
1 | import threading |
线程之间共享进程的所有数据
10.4. 线程锁
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
1 | import threading |
在上面的例子中,两个线程同时一存一取,最终导致余额不对,所以我们必须要保证一个线程在修改balance的时候,其他的线程一定不能改。所以,这时候就需要线程锁来做这件事情,当某个线程获取锁之后,除非锁被释放才可以继续执行其他线程,保证同意时刻最多只有一个线程持有该所,这样才不会造成冲突。
threading.Lock():创建线程锁
1 | import threading |
10.5. 自定义线程
自定义线程,需要继承自Thread类,还需要重写run()方法,开启线程时会自动调用run()方法
1 | import threading |
10.6. 定时线程
在threading模块中,有一个Timer类,继承自Thread类,可以延迟执行线程任务。
threading.Timer(interval, function[, args, kwargs]):设置一定时间后执行任务,interval表示设置的时间(s),function表示要执行的任务,args与kwargs表示传入的参数timer.start():开启定时任务timer.end():取消定时任务
1 | import threading |
10.7. 信号传递(Event)
在threading模块中,有一个Event类,可以用来控制线程的执行。
1 | import threading |
10.8. 多核CPU(Python无法利用多线程实现多核任务)
如果你用于一个多核CPU,你肯定回想,多核应该可以同时执行多个线程,可以写一个死循环,在N核CPU上执行N个死循环线程,结果会发生什么?
1 | import threading, multiprocessing |
启动上面的程序,双核的CPU上可以监控CPU利用率仅有97%,也就是仅使用了一核。但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为
Python的线程虽然是真正的线程,但解释器执行时,会存在一个全局解释器锁(GIL锁),任何Python线程执行前,必须先获得GIL锁,然后每执行100条字节码,解释器就会自动释放GIL锁,让别的线程有机会执行,这个GIL锁实际上把所有线程的执行代码都给加了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在
Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
10.9. 进程 vs 线程
- 进程
- 进程是操作系统资源分配的最小单位
- 进程拥有独立的地址空间,每启动一次进程,系统就会为它分配地址空间
- 多进程程序更健壮
- 多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。
- 多进程模式的缺点是创建进程的代价大,在
Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
- 线程
- 程序执行(调度)的最小单位
- 线程是共享进程中的数据,使用相同的地址空间,CPU切换一个线程的花费远远小于进程
- 轻量级的进程,进程之间的数据是独立的,而一个进程下的数据是可以共享的,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
- 线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
- 本文作者: Lajos
- 本文链接: https://www.lajos.top/2020/05/05/No-10-Python语言基础-线程/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!
