Python多任务—线程
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
一、线程的概念
线程是一个进程内部的一条执行路径,是程序执行的最小单位
线程有自己的栈和局部变量,多个线程共享同一进程的地址空间
多线程就是在一个进程中创建多个线程,每个线程完成一个任务
二、线程的实现
1. 使用threading模块
(1)单线程执行
import time def saySorry(): print('亲爱的,我错了,我能吃饭了吗') time.sleep(1) if __name__ == '__main__': for i in range(5): saySorry()
运行会发现,每隔一秒打印一句“亲爱的,我错了,我能吃饭了吗”
(2)多线程执行
实例化Thread对象,通过target参数指定线程将要执行的函数名,args参数通过元组来指定函数的位置实参,kwargs参数通过字典来指定函数的关键字实参。
import time import threading def saySorry(): print('亲爱的,我错了,我能吃饭了吗') time.sleep(1) if __name__ == '__main__': for i in range(5): t = threading.Thread(target=saySorry) t.start()
可以明显看出来,使用了多线程并发的操作,花费的时间要短很多
当调用start()时,才会真正创建线程,并且开始执行
2、主线程会等待所有子线程结束后才结束
import threading from time import ctime, sleep def sing(): for i in range(5): print('正在唱歌。。。%d' %i) sleep(1) def dance(): for i in range(5): print('正在跳舞。。。%d' %i) sleep(1) if __name__ == '__main__': print('---开始---:%s' %ctime()) t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) t1.start() t2.start() sleep(5) print('---结束---%s' %ctime())
运行结果:
---开始---:Mon Sep 16 20:02:46 2019 正在唱歌。。。0 正在跳舞。。。0 正在跳舞。。。1 正在唱歌。。。1 正在跳舞。。。2 正在唱歌。。。2 正在跳舞。。。3 正在唱歌。。。3 正在跳舞。。。4 正在唱歌。。。4 ---结束---Mon Sep 16 20:02:51 2019
import threading from time import ctime, sleep def sing(): for i in range(5): print('正在唱歌。。。%d' %i) sleep(1) def dance(): for i in range(5): print('正在跳舞。。。%d' %i) sleep(1) if __name__ == '__main__': print('---开始---:%s' %ctime()) t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) t1.start() t2.start() # sleep(5) # 通过屏蔽延时,程序是否会马上停止 print('---结束---%s' %ctime())
输出结果:
---开始---:Mon Sep 16 20:04:45 2019 正在唱歌。。。0 正在跳舞。。。0 ---结束---Mon Sep 16 20:04:45 2019 正在唱歌。。。1 正在跳舞。。。1 正在跳舞。。。2 正在唱歌。。。2 正在跳舞。。。3 正在唱歌。。。3 正在唱歌。。。4 正在跳舞。。。4
3、查看线程数量
import threading from time import sleep, ctime def sing(): for i in range(5): print('正在唱歌。。。%d' %i) sleep(1) def dance(): for i in range(5): print('正在跳舞。。。%d' %i) sleep(1) if __name__ == '__main__': print('---开始---:%s' %ctime()) t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) t1.start() t2.start() while 1: length = len(threading.enumerate()) print('当前运行的线程数为:%d' %length) if length<=1: break sleep(0.5)
运行结果:
---开始---:Mon Sep 16 20:12:37 2019 正在唱歌。。。0 正在跳舞。。。0 当前运行的线程数为:3 当前运行的线程数为:3 正在跳舞。。。1 正在唱歌。。。1 当前运行的线程数为:3 当前运行的线程数为:3 正在唱歌。。。2正在跳舞。。。2 当前运行的线程数为:3 当前运行的线程数为:3 正在跳舞。。。3正在唱歌。。。3 当前运行的线程数为:3 当前运行的线程数为:3 正在跳舞。。。4正在唱歌。。。4 当前运行的线程数为:3 当前运行的线程数为:3 当前运行的线程数为:1
4、线程执行代码的封装
通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread
就可以了,然后重写run
方法。
import threading import time class MyThread(threading.Thread): def run(self): for i in range(3): time.sleep(1) msg = "I'm" + self.name + '@' + str(i) # name属性中保存的是当前线程的名字 print(msg) if __name__ == '__main__': t = MyThread() t.start()
运行结果:
I'mThread-1@0 I'mThread-1@1 I'mThread-1@2
说明:
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
5、线程的执行顺序
import threading import time class MyThread(threading.Thread): def run(self): for i in range(3): time.sleep(1) msg = "I'm" + self.name + '@' + str(i) print(msg) def test(): for i in range(5): t = MyThread() t.start() if __name__ == '__main__': test()
执行结果:(运行的结果可能不一样,但是大体是一致的)
I'mThread-3@0 I'mThread-1@0 I'mThread-2@0 I'mThread-4@0 I'mThread-5@0 I'mThread-1@1 I'mThread-3@1 I'mThread-2@1 I'mThread-5@1I'mThread-4@1 I'mThread-1@2 I'mThread-2@2I'mThread-3@2 I'mThread-5@2I'mThread-4@2
从代码和执行结果来看,多线程程序的执行顺序是不确定的,当执行到sleep语句时候,线程将被阻塞,到sleep结束,线程进入就绪状态,等待调度。而线程调度将自动选择一个线程执行。上面代码只能保证每个线程都运行完整的run函数,但是线程的启动顺序,run函数中每次循环的执行顺序都不能确定。
当线程的run()方法结束时该线程完成。
无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
三、多线程共享全局变量
1、共享全局变量
import time import threading g_num = 100 def work1(): global g_num for i in range(3): g_num += 1 print('---in work1, g_num is %d----'%g_num) def work2(): global g_num print('---in work2,g_num is %d---'%g_num) print('---线程创建之前g_num is %d---'%g_num) t1 = threading.Thread(target=work1) t1.start() time.sleep(3) # 延时,保证t1线程中的事情做完 t2 = threading.Thread(target=work2) t2.start()
运行结果:
---线程创建之前g_num is 100--- ---in work1, g_num is 103---- ---in work2,g_num is 103---
2、列表当作实参传递到线程中
import time import threading def work1(nums): nums.append(44) print('---in work1---', nums) def work2(nums): time.sleep(1) # 延时保证t1线程中的事情做完 print('---in work2---',nums) g_nums = [11,22,33] t1 = threading.Thread(target=work1, args=(g_nums,)) t1.start() t2 = threading.Thread(target=work2,args=(g_nums,)) t2.start()
运行结果
---in work1--- [11, 22, 33, 44] ---in work2--- [11, 22, 33, 44]
在一个进程内,所有线程共享全局变量,很方便多个线程间共享数据
缺点就是,线程对全局变量随意更改,可能造成多线程之间对全局变量的混乱(即线程非安全)
3、多线程全局共享变量开发可能会遇到的问题
假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加10次,g_num的最终的结果应该为20。
但是由于是多线程同时操作,有可能出现下面情况:
a、在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
b、然后t2对得到的值进行加1并赋给g_num,使得g_num=1
c、然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
d、这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
测试1:
import time import threading g_num = 0 def word1(num): global g_num for i in range(num): g_num += 1 print('---in work1,g_num is %d---'%g_num) def work2(num): global g_num for i in range(num): g_num += 1 print('---in work2, g_num is %d---'%g_num) print('---线程创建之前g_num is %d---'%g_num) t1 = threading.Thread(target=word1, args=(100,)) t1.start() t2 = threading.Thread(target=work2, args=(100,)) t2.start() while len(threading.enumerate()) != 1: time.sleep(1) print('2个线程对同一个全局变量操作之后的最终结果是:%s'%g_num)
运行结果:
---线程创建之前g_num is 0--- ---in work1,g_num is 100--- ---in work2, g_num is 200--- 2个线程对同一个全局变量操作之后的最终结果是:200
测试2:
import time import threading g_num = 0 def word1(num): global g_num for i in range(num): g_num += 1 print('---in work1,g_num is %d---'%g_num) def work2(num): global g_num for i in range(num): g_num += 1 print('---in work2, g_num is %d---'%g_num) print('---线程创建之前g_num is %d---'%g_num) t1 = threading.Thread(target=word1, args=(1000000,)) t1.start() t2 = threading.Thread(target=work2, args=(1000000,)) t2.start() while len(threading.enumerate()) != 1: time.sleep(1) print('2个线程对同一个全局变量操作之后的最终结果是:%s'%g_num)
运行结果:
---线程创建之前g_num is 0--- ---in work2, g_num is 1331011--- ---in work1,g_num is 1382473--- 2个线程对同一个全局变量操作之后的最终结果是:1382473
结论:如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确。
四、同步
1、同步的概念
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
“同”字从字面上容易理解为一起动作。
其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。
2、 解决线程同时修改全局变量的方式
对于上一小节提出的那个计算错误的问题,可以通过线程同步来进行解决
思路,如下:
a、系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
b、t1对g_num的值进行+1
c、t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了,而且是g_num的值不是0而是1
d、同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性
五、互斥锁
当多个线程同时修改某一个共享的数据时候,需要进行同步控制
线程同步能保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,现将其锁定,此时资源的状态为“锁定”,其他线程不能更改,直到该线程释放资源,将资源状态变为“非锁定”,其他的线程才能再次锁定该资源,互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下,数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
# 创建锁 mutex = threading.Lock() # 锁定 mutex.acquire() #释放 mutex.release()
注意:
如果这个锁之前是没有上锁的,那么acquire不会堵塞
如果在调用acquire对这个锁上锁之前,它已经被其他线程上了锁,那么此时的acquire会堵塞,直到这个锁被解锁为止。
使用互斥锁完成2个线程对同一全局变量各加100万次操作
import time import threading g_num = 0 # 床架你一个互斥锁,默认是未上锁状态 mutex = threading.Lock() def test1(num): global g_num for i in range(num): mutex.acquire() # 上锁 g_num += 1 mutex.release() # 解锁 print('---test1----g_num=%d'%g_num) def test2(num): global g_num for i in range(num): mutex.acquire() # 上锁 g_num += 1 mutex.release() # 解锁 print('---test2---g_num=%d'%g_num) t1 = threading.Thread(target=test1, args=(1000000,)) t1.start() t2 = threading.Thread(target=test1, args=(1000000,)) t2.start() # 等待计算完成 while len(threading.enumerate()) != 1: time.sleep(1)
运行结果:
---test1----g_num=1888007
---test1----g_num=2000000
2个线程对同一全局变量操作之后最终结果为:2000000
可以看到最后的结果,加入互斥锁后,其结果与预期相符
上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁,如果此时另外一个线程试图获得这个锁,该线程就会变为“blocked”,成为阻塞。直到拥有锁的线程调用锁的release()方法,释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个获得锁,并使得该线程进入运行(running)状态。
总结:
锁的好处:确保某段关键代码只能由一个线程从头到尾的完整执行
锁的坏处:阻止了多线程并发执行,包含锁的某段代码只能以单线程模式执行,效率降低。
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方的锁时,可能会造成死锁。
六、死锁
现实社会中,男女双方都等待对方先道歉。
如果双方都这样固执的等待对方先开口,弄不好就分手咯。
1、死锁
在线程共享多个资源的时候,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但是发生了就是造成应用停止响应。下面看一个死锁的例子:
import time import threading mutexA = threading.Lock() mutexB = threading.Lock() class MyThread1(threading.Thread): def run(self): # 对mutexA上锁 mutexA.acquire() # mutexA上锁后,延时1秒,等待另外那个线程,把mutexB上锁 print(self.name + '-----do1-----up-----') time.sleep(1) # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了 mutexB.acquire() print(self.name + '----do1----down----') mutexB.release() # 对mutexA解锁、 mutexA.release() class MyThread2(threading.Thread): def run(self): # 对mutexB上锁 mutexB.acquire() # mutexB上锁后,延时1秒,等待另外那个线程把mutexA上锁 print(self.name + '---do2---up----') time.sleep(1) # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁 mutexA.acquire() print(self.name + '----do2---down---') mutexA.release() # 对mutexB解锁 mutexB.release() if __name__ == '__main__': t1 = MyThread1() t2 = MyThread2() t1.start() t2.start()
运行结果:
Thread-1-----do1-----up-----
Thread-2---do2---up----
此时已经进入了死锁,程序一直在等待。
2、避免死锁
程序设计时要避免“银行家算法”
添加超时时间等。
七、补充
1、多线程优点:
多线程技术使程序的响应速度更快,提高资源利用率,并发与并行。
必须注意:GIL(全局解释锁)的存在,使得Python不支持同一个进程中多线程在多核CPU上运行。
2、线程的生命周期
a、创建线程实例化对象,
b、就绪状态,线程对象调用了start()方法,可以参与竞争CPU
c、运行状态,获取CPU,执行线程代码
d、阻塞状态,由于某种阻塞原因,线程发生阻塞,当阻塞原因接触,回到就绪状态
e、终止状态,run()方法执行完毕,或其他终止情况
3、线程本地变量
threading.local()实现线程局部变量
import threading
thread_local = threading.local()
针对thread_local对象的相同属性名,不同的线程有不同的局部变量。
import time from threading import local, currentThread, Thread class TestDemo: pass lc = local() # 创建local对象 test = TestDemo() # 创建TestDemo对象 def func(): test.nums = [] time.sleep(3) for i in range(5): test.nums.append(i) #lc.nums.append(i) print(currentThread().name+"中的test.nums=",test.nums) if __name__ == '__main__': for i in range(3): t = Thread(target=func) t.start()
运行结果:
Thread-2中的test.nums= [0, 1, 2, 3, 4] Thread-1中的test.nums= [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] Thread-3中的test.nums= [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
import time from threading import local, currentThread, Thread class TestDemo: pass lc = local() # 创建local对象 test = TestDemo() # 创建TestDemo对象 def func(): lc.nums = [] time.sleep(3) for i in range(5): lc.nums.append(i) print(currentThread().name + "中的lc.nums=", lc.nums) if __name__ == '__main__': for i in range(3): t = Thread(target=func) t.start()
运行结果:
Thread-1中的lc.nums= [0, 1, 2, 3, 4] Thread-3中的lc.nums= [0, 1, 2, 3, 4] Thread-2中的lc.nums= [0, 1, 2, 3, 4]
4、条件变量Condition
条件变量通过允许线程阻塞和等待线程发送信号的方式弥补了锁机制中的锁状态不足的问题
condition = threading.Condition() # 实例化线程条件变量对象 condition.acquire() # 获取该Condition对象上的锁(加锁) condition.release() # 释放该Condition对象上的锁(放锁) condition.wait() # wait()必须在加锁的情况下调用,wait()会释放已经获取的锁 condition.notifyAll() # notifyAll()必须在加锁的情况下调用,唤醒所有在该Condition对象上等待的线
生产者消费者模型
import time from threading import Thread,currentThread MAX_STORAGE = 5 # 最大库存量 class ProducerThread(Thread): # 生产者线程类 def __init__(self,tname,bread,condition): Thread.__init__(self,name=tname) self.bread = bread # 接收从外部传递来的bread对象,要确保与消费者线程是同一个Bread对象 self.condition = condition # 接收Condition对象,要确保与消费者线程是同一个Condition对象 def run(self): while True: self.condition.acquire() # 添加该Condition对象上的锁 if self.bread.count < MAX_STORAGE: # 判断当前面包数量是否已经到达了最大值 time.sleep(3) # 休眠3秒,不释放锁 self.bread.produce() # 生产面包 print(currentThread().name+"生产了一个面包,当前面包总数为:"+str(self.bread.count)) self.condition.notifyAll() # 唤醒在self.condition上等待的线程,即使没有等待的线程,调用也不错 self.condition.release() # 释放锁 else: print(currentThread().name+":库存量已满,停止生产!") self.condition.wait() # 挂起当前线程,并释放CPU与锁
import time from threading import Thread,currentThread class ConsumerThread(Thread): # 消费者线程类 def __init__(self,tname,bread,condition): Thread.__init__(self,name=tname) self.bread = bread # 接收从外部传递来的bread对象,要确保与消费者线程是同一个Bread对象 self.condition = condition # 接收Condition对象,要确保与消费者线程是同一个Condition对象 def run(self): while True: self.condition.acquire() # 加锁 if self.bread.count > 0: # 还有面包,就消费 time.sleep(3) # 休眠3秒,不释放锁 self.bread.consume() # 消费面包 print(currentThread().name + "消费了一个面包,当前面包总数为:" + str(self.bread.count)) else: print(currentThread().name+"说:没面包啦,快来生产~~~") self.condition.notifyAll() # 唤醒在self.condition上等待的线程,即使没有等待的线程,调用也不错 self.condition.wait() # 当前线程挂起,知道其他线程唤醒该线程
class Bread: # 面包商品 def __init__(self): self.count = 0 # 初始情况,面包数量为0 def produce(self): # 生产面包 self.count += 1 def consume(self): # 消费面包 self.count -= 1
from threading import Condition from goods import Bread from producer import ProducerThread from consumer import ConsumerThread if __name__ == '__main__': bread = Bread() con = Condition() for i in range(1): pro = ProducerThread("生产者-"+str(i),bread,con) pro.start() for i in range(10): consumer = ConsumerThread("消费者-"+str(i),bread,con) consumer.start()
5、其他
setDaemon(True) 将线程设置为后台线程。(在线程启动之前调用)
join(timeout=等待秒数) 当前线程挂起,等待线程对象对应的线程timeout秒,如果没有设置timeout,则等待执行完毕。(在线程启用之后调用用)
八、多任务版UDP聊天
import socket import threading def send_msg(udp_socket): """获取键盘数据,并将其发送给对方""" while True: # 1. 从键盘输入数据 msg = input("\n请输入要发送的数据:") # 2. 输入对方的ip地址 dest_ip = input("\n请输入对方的ip地址:") # 3. 输入对方的port dest_port = int(input("\n请输入对方的port:")) # 4. 发送数据 udp_socket.sendto(msg.encode("utf-8"), (dest_ip, dest_port)) def recv_msg(udp_socket): """接收数据并显示""" while True: # 1. 接收数据 recv_msg = udp_socket.recvfrom(1024) # 2. 解码 recv_ip = recv_msg[1] recv_msg = recv_msg[0].decode("utf-8") # 3. 显示接收到的数据 print(">>>%s:%s" % (str(recv_ip), recv_msg)) def main(): # 1. 创建套接字 udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 2. 绑定本地信息 udp_socket.bind(("", 7890)) # 3. 创建一个子线程用来接收数据 t = threading.Thread(target=recv_msg, args=(udp_socket,)) t.start() # 4. 让主线程用来检测键盘数据并且发送 send_msg(udp_socket) if __name__ == "__main__": main()