Python 中的多线程
什么是线程
一个进程中包括多个线程,线程是 CPU 调度和分派的基本单位,是进程中执行运算的最小单位,真正在 CPU 上运行的是线程,可以与同一个进程中的其他线程共享进程的全部资源
Python 中实现多线程
Python 中有两种方式床架多线程,一种是调用底层的 thread
模块(Python3 中已弃用),另一种是使用threading
模块,下面我说的也是使用这个模块实现多线程的方法
从形式上将,多线程的实现和多进程的实现十分类似,threading
模块提供了Thread
类来创建线程,同Process
类一样,我们可以通过直接调用或创建子类来继承这两种方式来创建线程
使用 Thread 实现多线程
直接调用threading
,模块的Thread
类来创建线程十分简单,使用threading.Thread([target], [(item1, item2, ...)])
方法,target
为目标函数名称(如果有目标函数的话),后边为参数元组,用来传递函数参数
线程创建好后,调用Thread.start()
方法,就可以运行线程,如果没有目标函数,start()
会自动执行Thread
类中的run()
方法,示例如下:
1 | import threading |
运行结果:
1 | create 0 Thread |
上边的程序也证明了,程序在创建了子线程后,不会等待线程执行完毕,而是会继续向下执行,而当主程序全部执行完毕后,却会等待所有子线程执行完毕再结束,这点和进程有些区别
继承 Thread 实现多线程
另外一种方式是通过创建继承Thread
类的子类来实现多线程,这样做的好处就是可以将线程要运行的代码全部放入run
函数中,用起来更方便,示例如下:
1 | import threading |
运行结果:
1 | I am Thread-1 @ 0 |
Python 多线程中全局变量和非全局变量的使用问题
开始就说过,线程可以和同一个进程中的其他线程共享进程的全部资源,那么在多线程程序中,各线程对全局变量和非全局变量的使用到底是怎样的呢
非全局变量
非全局变量在多线程中是不会被共享的,这就像是假设有一个存在局部变量a
的函数,当程序调用两次这个函数时,每次调用所产生的局部变量a
都是一个新的变量,不会受另一个函数执行的干扰,示例如下:
1 | from threading import Thread |
运行结果:
1 | Thread-1 |
可以看到,局部变量g_num
并不会因为另一个线程中的同名函数而收到影响
全局变量
在多线程中,全局变量是可以在各线程间共享的,这也就是说,线程间通信不需要通过管道、内存映射等方法,只需要使用一个全局变量(同一个进程中的共享资源)便可以,示例如下:
1 | from threading import Thread |
运行结果:
1 | ---g_num= 372639 --- |
到这里,我们可以发现一个问题,正常来讲,g_num
最后的值不应给是2000000
吗,为什么不是呢?这就是多线程使用全局变量时有可能出现的 bug,请继续阅读!
Python 多线程中如何防止使用全局变量出现 bug(轮询和互斥锁)
通过刚才在多线程中使用全局变量我们发现,当代码逻辑稍微复杂一些时,在两个线程中同时使用一个全局变量会出现问题,是什么导致了这个问题呢?
从代码中我们可以发现g_num += 1
这句代码,实际上是g_num + 1
和将其结果赋给g_num
两步,正是因为这连续的两次对全局变量的操作造成了这个问题
当一个线程执行到g_num + 1
这步后,cpu 有可能会转头去处理另一个线程,另一个线程也运行了g_num + 1
,当 cpu 再回头执行第一个线程时,g_num
已经不止被运算过一次了
那么怎么避免这样的情况发生呢,只能是如果存在对全局变量变量值的修改时,我们要优先运行一个线程,当它结束修改后,再允许另一个线程去访问这个局部变量,下面提供两种方式,轮询和互斥锁
轮询
顾名思义,轮询的意思就是反复询问,抽象起来理解就是,我们可以设置另一个用来作为目标值的全局变量,两个线程执行的条件根据目标值的不同而不同,当目标值满足一个线程执行时,其他线程就会一直处在一个堵塞的过程,它会一直询问目标值是否符合自己,当上一个线程结束时,这个线程会将目标值修改,这样下一个符合目标值的线程就会运行,示例如下:
1 | from threading import Thread |
运行结果:
1 | ---g_num= 181893 --- |
如结果所示,线程之间使用全局变量的 bug 已经解决,但是轮询的方法十分消耗资源,堵塞的线程其实一直都处在一个死循环的状态占用系统资源
互斥锁
相比而言,互斥锁就是一种比较优化的方法,互斥锁会使用threading
模块的Lock
类
互斥锁的思想是,当一个线程运行时,它会给它需要的这部分资源上锁,这样同样使用这把锁的其他线程全部都会被堵塞,但被互斥锁所堵塞的线程不会占用系统资源,它们会处在睡眠状态,当运行的线程用完被锁的这部分资源后,它会解锁,这时其他线程就会被唤醒来抢占 cpu 资源,得到资源的线程会再次上锁,达到多线程下全局变量的访问安全,示例如下:
1 | from threading import Thread |
运行结果:
1 | ---g_num= 200009 --- |
互斥锁的方法说明:
1 | # 创建锁 |
使用互斥锁时要注意,为了提高运算效率,上锁的资源越少,运算的效率越高
另外,线程等待解锁的方式不是通过轮询,二十通过通知,线程会睡眠,等待唤醒的通知,所以互斥锁较轮询来讲更为优化