多线程
多任务可以由多进程完成,也可以由一个进程内的多线程完成
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,
threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:
1 | import time, threading # 新线程执行的代码: |
执行结果1
2
3
4
5
6
7
8
9thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,
Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名
字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来
显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于
每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量
都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时
改一个变量,把内容给改乱了
1 | import time, threading |
我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0
,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就
不一定是0了。
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:1
balance = balance + n
也分两步:
计算balance + n,存入临时变量中;
将临时变量的值赋给balance。
也就是可以看成:1
2x = balance + n #赋值给临时变量
balance = x
由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:1
2
3
4
5
6
7
8
9
10
11
12
13初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0 #正常
但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:
1 | 初始值 balance = 0 |
两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地
变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改
如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,
我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,
获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,
不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
1 | balance = 0 |
当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,
其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。
所以我们用try…finally来确保锁一定会被释放
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻
止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多
个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止
小结
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为
局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
局部变量也有问题,就是在函数调用的时候,传递起来很麻烦1
2
3
4
5
6
7
8
9
10
11
12
13def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理
不同的Student对象,不能共享。
1 | import threading |
执行结果
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,
但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是
线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,
这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源