1. 多任务 - 线程
参考
首先考虑一个没有多任务的程序:
import timedef sing():# 唱歌 5 秒钟for i in range(5):print("-----菊花台ing....-----")time.sleep(1)def dance():# 跳舞 5秒钟for i in range(5):print("-----跳舞.....-----")time.sleep(5)def main():sing()dance()if __name__ == "__main__":main()
此时,你想同时执行唱歌、跳舞是无法做到的。如果想要同时执行,可以使用python提供的Thread来完成.
import time
from threading import Threaddef sing():# 唱歌 5 秒钟for i in range(5):print("-----菊花台ing....-----")time.sleep(1)def dance():# 跳舞 5秒钟for i in range(5):print("-----跳舞.....-----")time.sleep(1)def main():t1 = Thread(target=sing)t2 = Thread(target=dance)t1.start()t2.start()if __name__ == "__main__":main()
关键点:
from threading import Thread
: 从threading包钟导入Threadt1 = Thread(target=sing)
: 使用这个将函数变为线程执行的函数~
1.1 多任务的概念
上面体验了如何同时执行2个异步函数~下面补充一下多任务的概念.
简单地说,就是操作系统可以同时运行多个任务.例如: 一边逛浏览器,一遍听音乐
单核CPU执行多任务: 单核CPU执行多任务的关键在于,将cpu的时间切片. 任务1执行0.01秒,然后任务2执行0.01秒,在切到任务1执行0.01秒。由于CPU的运算速度很快,因此,我们感觉就行所有任务都在同时执行一样
多核CPU执行多任务: 真的并行执行多任务只能在多核CPU上实现. 但是,在平常的代码中,任务数量会远远的大于CPU的核心数,因此操作系统会将任务轮流调度到各个核心上执行~
并行: 同一时刻真正运行在不同的CPU上的任务
并发: 在一个很短的时间内,利用CPU的告诉运转.执行多个任务
1.2 查看当前线程数量
在某些情况下,需要查看当前程序中的线程数量,可以使用threading.enumerate()
进行尝试
import threading
from time import sleep, ctimedef sing():for i in range(3):print("正在唱第%d首哥" % i)sleep(2)def dance():for i in range(3):print("正在跳第%d支舞" %i)sleep(2)if __name__ == "__main__":print("开始时间: %s" % ctime())t1 = threading.Thread(target=sing)t2 = threading.Thread(target=dance)t1.start()t2.start()while True:length = threading.enumerate()print("当前的总线程数为: %d" % length)if length <= 1:breaksleep(1)print("结束时间: %s" % ctime())
注意:
- 当调用Thread的时候,不会创建线程
- 当调用Thread创建出来的实例对象的 start方法时,才会创建线程以及让这个线程开始运行
1.3 创建线程的第二种方法
第一种方法是通过: t = Thread(target = 函数名)
来准备, t.start()
来启动
第二种方法是通过类继承threading.Thread来实现,代码如下:
import threading
import timeclass MyThread(threading.Thread):def run(self):for i in range(3):time.sleep(1)msg = "I`m " + self.name + " @" + str(i)print(msg)if __name__ == "__main__":t = MyThread()t.start()
说明:
- 类的继承:
class MyThread(threading.Thread)
- 通过类的方式创建的线程,必须在该类中定义run函数. 这样当调用
t.start()
时候,新创建的线程会去类中寻找run函数并执行 - 一个
t=MyThread()
只会准备一个线程,当t.start()
时,会创建线程~
1.4 线程共享全局变量
有些时候,多个不同的线程可能需要用到同一个变量,下面演示全局变量在多线程中的使用
from threading import Thread
import timedef work1():global g_numfor i in range(3):g_num += 1print("---- in work1, g_num is %d ---" % g_num)def work2():global g_numprint("---- in work2, g_num is %d ---" % g_num)def main():g_num = 100print("---- 线程创建执行之前 g_num is %d ---" % g_num)t1 = Thread(target=work1)t1.start()# 让 t1线程先执行time.sleep(1)t2 = Thread(target=work2)t2.start()if __name__ == "__main__":main()
注意:
- 在一个函数中对全局变量进行修改的时候需要看是否对全局变量的指向进行了修改
- 如果修改了指向,那么必须使用global
- 如果仅修改了指向中的数据,则可以省略global
1.5 带参数的线程调用
在调用的时候,可能需要传递参数进去.这就需要在线程准备的时候,使用args传递参数
from threading import Thread
from time import sleepdef test1(tmp):tmp.append(33)def test2(tmp):tmp.append(66)def main():num_arr = [11, 22]print(str(num_arr))t1 = Thread(target=test1, args=(num_arr,))t2 = Thread(target=test2, args=(num_arr,))t1.start()sleep(1)print(str(num_arr))t2.start()print(str(num_arr))if __name__ == "__main__":main()
注意:
- 多任务共享数据的原因: 多个任务合作同时完成一个大任务~
- 一个任务获取数据
- 一个任务处理数据
- 一个任务发送数据
1.6 资源竞争
共享变量会产生一个资源竞争的问题: 多个线程同时对一个全局变量进行修改~下面复现问题
import threading
import timeg_num = 0def test1(num):global g_numfor i in range(num):g_num += 1print("test1: g_num: %d" % g_num)def test2(num):global g_numfor i in range(num):g_num += 1print("test2: g_num: %d" % g_num)def main():t1 = threading.Thread(target=test1, args=(1000000,))t2 = threading.Thread(target=test2, args=(1000000,))t1.start()t2.start()time.sleep(5)print("main: g_num: %d" % g_num)if __name__ == "__main__":main()
test2: g_num: 1042014
test1: g_num: 1080242
main: g_num: 1080242
以上的原因如下:
-
假设:
- t1代表线程1,t2代表线程2
- g_num +=1 可分解成下面3个步骤:
- 获取 g_num的值, 记为t1.1(t2.1)
- 将g_num的值加1, 记为t1.2(t2.2)
- 将加1后的值存入g_num, 记为t1.3(t2.3)
-
下面模拟执行步骤:(根据CPU的特性,分时执行)
- 假设先执行t1.1
- 再执行t1.2
- 然后执行t2.1, 此时重新获取g_num的值
- 然后执行t1.3, 此时g_num的值并未改变
1.7 同步
以上问题可以通过线程同步来解决,在此之前,需要先了解互斥锁:
- 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
- 互斥锁未资源引入了一个状态: 锁定/非锁定
- 某个线程要更改共享数据的时候,先将其锁定,此时资源的状态为"锁定",其他线程不能更改;知道该线程释放资源,将资源的状态变为"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性
下面是互斥锁的基本使用:
# 创建锁
mutex = threading.Lock()# 锁定
mutex.acquire()# 释放
mutex.release()
注意:
- 如果这个锁之前是没有上锁得,那么acquire不会堵塞
- 如果在调用acquire之前已经被上锁了,那么acquire将会被阻塞直至release释放
具体做法如下:
import threading
import timedef test1(num, ):global g_num, metexfor i in range(num):metex.acquire()g_num += 1metex.release()print("test1: g_num: %d" % g_num)def test2(num, ):global g_num, metexfor i in range(num):metex.acquire()g_num += 1metex.release()print("test2: g_num: %d" % g_num)g_num = 0
metex = threading.Lock()
def main():# 创建互斥锁t1 = threading.Thread(target=test1, args=(1000000,))t2 = threading.Thread(target=test2, args=(1000000,))t1.start()t2.start()time.sleep(5)print("main: g_num: %d" % g_num)if __name__ == "__main__":main()
说明:
- 全局变量中创建一个互斥锁:
metex = threading.Lock()
- 在原子代码前面添上:
metex.acquire()
- 原子代码: 即要么一次性全部执行,要么不执行的不可分割的代码
- 在原子代码后面添上:
metex.release()
1.8 死锁
如果两个线程分别占用一部分资源,并且同时等待对方的资源,就会造成死锁的现象。下面使用python实现一个简单的死锁程序:
import threading
import timeclass MyThread1(threading.Thread):def run(self):# 线程1 假设下面都是原子代码mutexA.acquire()print(self.name + "do1 up")time.sleep(1)mutexB.acquire()print(self.name + "do1 down")mutexB.release()mutexA.release()class MyThread2(threading.Thread):def run(self):mutexB.acquire()print(self.name + "do2 up")time.sleep(1)mutexA.acquire()print(self.name + "do2 down")mutexA.release()mutexB.release()mutexA = threading.Lock()
mutexB = threading.Lock()def main():t1 = MyThread1()t2 = MyThread2()t1.start()t2.start()if __name__ == "__main__":main()
说明:
- 进入线程1,将mutexA锁定,然后休眠1秒
- 进入线程2,将mutexB锁定,然后休眠1秒
- 之后同时在线程1和2中各自获取mutexB,mutexA而进入相互等待阶段,即死锁。