这一篇教程,我们一起来了解多线程。
我们通过进程的学习,能够知道一个进程是一个任务。
那么,线程又是什么?
线程的作用执行进程的任务。
实际上,每一个进程的启动,都至少包含了一个线程。
这就好像一家食品公司(进程),至少得有一条生产线(线程),食品公司(进程)要生产食品(任务),但是实际上进行生产(执行任务)的是这家公司的生产线(线程)。
我们可以通过代码,验证一下这个概念。
示例代码:
import multiprocessing, threading print(multiprocessing.current_process().name) # 显示输出结果为:MainProcess print(threading.current_thread().name) # 显示输出结果为:MainThread
通过运行上方代码,我们能够看到主进程和主线程的名称。
也就意味着,程序执行的时候,启动了主进程,主进程启动了主线程。
接下来是多线程。
还是以食品公司举例。
速冻食品公司不但能生产饺子,还能够生产馄饨。
假如这时来了多个订单(任务),有些订单是饺子,有些订单是馄饨。
虽然一条生产线能够生产这两种食品,但是必须先生产完其中一种,再生产另外一种。
那么,当有多个订单要同时进行的时候,怎么解决呢?
一种方法,我们可以把公司(主进程)变成集团,在集团下开设多个分公司(子进程),每个公司建立一条生产线(主线程)。
另一种方法,我们可以把食品公司(主进程)的生产线升级生产组(主线程),在生产组内建立多条生产线(子线程)。
还有一种方法,我们可以把公司(主进程)变成集团,在集团下开设多个分公司(子进程),每个公司设立生产组(主线程),每个生产组建立多条生产线(子线程)。
简单来说,在程序中多任务的实现有3种方式:
- 多进程单线程
- 单进程多线程
- 多进程多线程
接下来,我们通过代码看一下如何通过多线程执行多任务。
示例代码:
import time, threading # 导入需要使用的模块 def task01(name): print(threading.current_thread().name) # 显示输出结果为:Thread-1 print(name, 1) time.sleep(0.001) print(name, 2) time.sleep(0.001) print(name, 3) def task02(name): print(threading.current_thread().name) # 显示输出结果为:Thread-1 print(name, 1) time.sleep(0.001) print(name, 2) time.sleep(0.001) print(name, 3) if __name__ == '__main__': print(threading.current_thread().name) # 显示输出结果为:MainProcess thread1 = threading.Thread(target=task01, args=('函数01:',)) # 创建子线程运行函数task01 thread2 = threading.Thread(target=task02, args=('函数02:',)) # 创建子线程运行函数task02 thread1.start() # 启动子进程 thread2.start() # 启动子进程
运行上方代码,显示输出结果类似:
MainThread
Thread-1
函数01: 1
Thread-2
函数01: 2
函数02: 1
函数01: 3
函数02: 2
函数02: 3
这里,大家能够看出,这段代码和之前我们通过多进程实现多任务的代码非常相像,只是把进程模块换为了线程模块,创建进程变成了创建线程。
不过,多进程和多线程是有区别的。
多进程中对于同一个变量,各有一份副本在进程中,所以对同一个变量的操作互不影响。
多线程中对于同一个变量,是各个线程共享的,所以对同一个变量,每个线程都能够进行操作。
我们通过代码来尝试一下。
示例代码:(多进程)
import multiprocessing count = 0 def task01(): print(multiprocessing.current_process().name) # 显示输出结果为:Process-1 global count count += 1 print(count) # 显示输出结果为:1 def task02(): print(multiprocessing.current_process().name) # 显示输出结果为:Process-2 global count count += 1 print(count) # 显示输出结果为:1 if __name__ == '__main__': process1 = multiprocessing.Process(target=task01) process2 = multiprocessing.Process(target=task02) process1.start() process2.start()
示例代码:(多线程)
import threading count = 0 def task01(): print(threading.current_thread().name) # 显示输出结果为:Thread-1 global count count+=1 print(count) # 显示输出结果为:1 def task02(): print(threading.current_thread().name) # 显示输出结果为:Thread-2 global count count+=1 print(count) # 显示输出结果为:2 if __name__ == '__main__': thread1 = threading.Thread(target=task01) thread2 = threading.Thread(target=task02) thread1.start() thread2.start()
通过段代码显示输出结果的对比,就能够看出不同。
而且,因为这个原因,使用多线程对同一个变量进行操作时,还容易出现意料之外的错误。
例如下面这段代码。
示例代码:
import threading count = 0 def task01(proc): global count for i in range(500000): count += 1 count -= 1 def task02(proc): global count for i in range(500000): count += 1 count -= 1 if __name__=='__main__': thread1=threading.Thread(target=task01,args=('线程1',)) thread2=threading.Thread(target=task02,args=('线程2',)) thread1.start() thread2.start() thread1.join() thread2.join() print(count)
按代码里面的内容推断,代码运行结束时,全局变量count的值应该为0。
但是,实际上你会发现,运行之后结果不是固定的。
这是就是因为两个线程同时操作一个变量所导致的问题。
其中一种情况就是,thread1在执行了加法语句之后,尚未执行减法语句时,thread2的加法语句被执行了。
那后面不还是会执行两个减法语句吗?
看似是这个道理,但是实际上“count += 1”这个语句,在给变量count赋值的时候,并不是直接把计算结果写入变量count,而是先把“count+1”的计算结果写入到一个临时的局部变量,再把这个临时局部变量的值写入全局变量count。
过程是这样的(count初始值为0):
- thread1:temp1 = count + 1 # 此时temp1为1,count为0。
- thread2:temp2 = count + 1 # 此时temp2为1,count为0
- thread1:count = temp1 # 此时count为1。
- thread2:count = temp2 # 此时count为1。
- thread1:temp1 = count – 1 # 此时temp1为0,count为1。
- thread1:count = temp1 # 此时count为0。
- thread2:temp2 = count – 1 # 此时temp2为-1,count=0。
- thread2:count = temp2 # 此时count为-1。
这样的错误,在代码运行足够的次数后就会发生。
那么,如何避免这种错误呢?
我们通过Lock类来解决。
Lock类是实现原始锁对象的类。一旦一个线程获得了一个锁,就会执行锁之后的语句块,直到锁被释放;锁的释放可以在任何线程中执行。
示例代码:(以task01函数为例)
lock = threading.Lock() # 创建锁 def task01(proc): global count for i in range(500000): lock.acquire() # 加锁 count += 1 count -= 1 lock.release() # 解锁
在上方代码中,创建一个锁的对象,并且为函数task01和task02都添加acquire()获得锁和release()释放锁的语句,就可以避免之前的错误出现。
加锁固然能够让某一段代码在执行完毕前不被其它线程所干扰,但是这样也导致其它线程不能并发执行,从而降低了代码的效率。
就拿上方代码来说,大家应该可以感受到加锁后代码的运行速度比加锁前慢了许多。
如果感受不明显,可以把循环次数再增加10倍试一试。
最后,大家还要知道,在Python解释器执行代码时,也有一个锁叫Global Interpreter Lock(简称GIL:全局解释器锁),这个锁导致任何Python线程执行之前,必须先开启GIL锁,每执行一定数量的代码后,再自动关闭GIL锁,让其它线程能够执行。这个全局解释器锁的机制导致Python中的线程不是真正的并发,而是轮流执行,所以,再多的线程也是一个CPU核心在执行,无法使用多个CPU核心。这也就意味着多核CPU执行多线程的Python代码时,无法发挥多核的性能。如果想有效利用CPU的多个核心,可以在Python的代码中使用多进程。多进程虽然也有多个线程,但是各个进程的GIL锁是独立的,不会互相影响。
本节知识点:
1、线程的概念;
2、多线程的使用;
3、线程的锁。
本节英文单词与中文释义:
1、thread:线程
2、lock:锁
3、acquire:获得
4、release:释放
5、temp(temporary):临时
6、interpreter:解释器
转载请注明:魔力Python » Python3萌新入门笔记(48)