第十四篇:多任务系列之线程(一)
本篇先介绍多任务的概念、随后介绍关于threading模块下thread类创建子线程的流程以及理解,最后关于互斥锁。死锁以及如何避免死锁等知识。而关于进程在下一篇进行介绍。
一、概念
在了解多任务或者线程等知识之前,我们首先需要对一些概念性知识有一定的了解。
1、时间片轮转
假如终端为单核CPU,而同时我又想同时运行多个程序,通常我们能看到单核的CPU也能同时运行多个程序,那是由于操作系统对CPU调用进行分配时,每个程序占用CPU的时间很短很短,接着便开始下一个程序的运行,周而复始的运行这些程序,由于每个程序占用CPU的时间特别短故呈现给我们的现象是:多个程序同时运行的假象。
2、并发
当CPU的数量小于运行程序的数量时,无法是实现真正意义上的每个程序的同时运行,故还是利用时间片轮转原理制造每个程序同时运行的假象,即为并发。
3、并行
当CPU的数量大于等于运行程序的数量时,可以实现每个程序一直均占有一个CPU,实现真正意义上的每个程序同时运行,即为并行。
4、主线程
当我们执行一段程序时,会有一条类似流水线的流程,按照程序的执行顺序不断执行,而在执行的过程主线程可以创建子线程,而通常而言主线程会等所有子线程均运行完才会结束运行,若主线程死掉,所有的子线程也会被杀死。
5、子线程
子线程可以通过主线程创造,同时子线程运行结束或者被杀死,不会影响主线程的执行。
二、线程
1、为什么要有线程?我们从例子开始查看:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding:utf-8 -*- import time def sing():# 唱歌 for i in range(3): print("he is singing song..") time.sleep(1) def dance(): #跳舞 for i in range(3): print("he is dancing..") time.sleep(1) def main(): sing() dance() if __name__=="__main__": main()
注:从上面的例子可以看出:在没有多线程的情况下,只能先唱完歌再进行跳舞,然而我们要实现的是边唱歌边跳舞
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding:utf-8 -*- import time import threading def sing(): """唱歌""" for i in range(3): print("he is singing song..") time.sleep(1) def dance(): """跳舞""" for i in range(3): print("he is dancing..") time.sleep(1) def main(): # 1.创建子线程对象,并且指定子线程的工作函数 # 即相当于 公司招了个员工,告诉他需要干的活 t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) # 2、子线程运行,并工作函数开始工作 # 相当于该员工开始工作 t1.start() t2.start() # 3、主线程等子线程运行完才关闭 t1.join() t2.join() if __name__=="__main__": main()
注:通过上述例子可以得出:通过多任务的实现了唱歌的同时也进行了跳舞。
2、创建线程的流程是怎么样的?
import time,threading def sing(): for i in range(3): print("he is singing song..") time.sleep(1) def dance(): for i in range(3): print("he is dancing..") time.sleep(1) def main(): # 1.创建子线程对象,并且指定子线程的工作函数 # 即相当于 公司招了个员工,告诉他需要干的活 t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) # 2、子线程运行,并工作函数开始工作 # 相当于该员工开始工作 t1.start() t2.start() # 3、主线程等子线程运行完才关闭 t1.join() t2.join() #注:通过多任务的实现了唱歌的同时也进行了跳舞。 if __name__=="__main__": main()
注:其实主线程和子线程的关系我们可以理解为:
1、主线程相当于一个公司的运转,而子线程相当于该公司下的一个员工正在进行的工作;
2、创建子线程的过程即t1=threading.Thread(target=func_name,args=(para1,para2..),我们可以理解为该公司雇佣了新的员工;而target=func_name,相当于告诉该员工他需要干的活是什么即传入功能函数;
3、而t1.start(),则表示该子线程真正运行,功能函数被调用,该过程可以理解为员工开始干活了,即员工的工作才是子线程,而员工本身仅仅是子线程对象。
3、如何查看当前线程的数量?
查看当前线程的数量主要通过threading模块下的 enumerate()方法,返回一个元组,该元组内为线程对象;同时也可以通过 isalive()方法 查看该线程是否存活,例如:
#!/usr/bin/env python # -*- coding:utf-8 -*- import time,threading def test1(): for i in range(5): print("in the test1 --> %s" %str(i)) time.sleep(1) def test2(): for i in range(5): print("in the test2 --> %d" %i) time.sleep(1) def main(): t1 = threading.Thread(target=test1) t2 = threading.Thread(target=test2) t1.start() t2.start() #查看哪个子线程先结束: while True: if not t1.is_alive(): print("in the t1",time.asctime()) break elif not t2.is_alive(): print("in the t2",time.asctime()) #查看当前线程的数量: while True: print("当前线程的数量为-->",threading.enumerate()) if len(threading.enumerate())==1: break time.sleep(0.5) t1.join() t2.join() if __name__ == "__main__": main()
4、通过继承类的进行封装的方式
这种方式的应用主要用于该子线程的运行过程较为复杂,可以将封装在继承了类threading.Thread类的自定义内中的 run()函数中,当调用 start()方法,将自动运行run()函数,例如:
import threading,time class MyThread1(threading.Thread): def sing(self): # 将函数sing封装在类中 for i in range(3): print("She is singing .") time.sleep(1) def run(self): self.sing() class MyThread2(threading.Thread): def dance(self): # 将函数dance封装在类中 for i in range(3): print("She is dancing .") time.sleep(1) def run(self): self.dance() def main(): mt1= MyThread1() # 创建子线程对象,每个子线程单独负责一个流程 mt1.start() mt2 = MyThread2() mt2.start() if __name__ == "__main__": main()
5、关于多线程共享全局变量的问题
当多个线程共享全局变量的时候,会出现下面的问题,例如:
#!/usr/bin/env python # -*- coding:utf-8 -*- import threading g_num =0 def cal1(n): """计算1--不断循环+1执行num次 """ global g_num for i in range(n): g_num += 1 print("--in the cal1-->",g_num) # 循环完查看全局变量结果 def cal2(n): """计算2--不断循环+1执行num次""" global g_num for i in range(n): g_num +=1 print("__in the cal2 -->",g_num) def main(): t1 = threading.Thread(target=cal1,args=(1000000,)) t2 = threading.Thread(target=cal2,args=(1000000,)) t1.start() t2.start() print("------->",g_num) # 查看最终全局变量修改的结果 if __name__=="__main__": main() #最终结果为: -------> 195920 --in the cal1--> 1409931 __in the cal2 --> 1449753
从上述结果可以看出最中得到全局变量的结果并不是我们意想的那个2000000,而是一个小于2000000的结果,这是由于:
6、互斥锁
既然我们知道了线程共享全局变量的时候容易出现问题,那么我们该如何解决这些问题呢?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding:utf-8 -*- import threading g_num =0 def cal1(n): global g_num mutex.acquire() # 加锁 for i in range(n): g_num += 1 mutex.release() # 释放锁 print("--in the cal1-->",g_num) def cal2(n): global g_num mutex.acquire() for i in range(n): g_num +=1 mutex.release() print("__in the cal2 -->",g_num) mutex = threading.Lock() #创建互斥锁对象 # 注:加上互斥锁的好处 --即加上锁则哪个子线程抢到该锁先执行,一直执行完成直到锁被释放才会执行下一个子线程。 def main(): t1 = threading.Thread(target=cal1,args=(1000000,)) t2 = threading.Thread(target=cal2,args=(1000000,)) t1.start() t2.start() print("------->",g_num) if __name__=="__main__": main()
通过加上互斥锁的好处 --即加上锁则哪个子线程抢到该锁先执行,一直执行完成直到锁被释放才会执行下一个子线程。这样的就能够保证上锁到释放锁中的这段代码运行完成才操作系统才会将CPU分配给其他线程。
7、死锁
死锁通常需要两个或者两个以上的锁才会发生,死锁的发生会导致程序永久性堵塞,发生死锁的程序是无法通过其他方法进行解决的,只能避免。例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#coding=utf-8 import threading import time class MyThread1(threading.Thread): def run(self): # 对mutexA上锁 mutexA.acquire() # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁 print("next need MyThread2 release mutexB.....") 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("next need MyThread1 release mutexA.....") time.sleep(1) # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了 mutexA.acquire() print(self.name+'----do2---down----') mutexA.release() # 对mutexB解锁 mutexB.release() mutexA = threading.Lock() mutexB = threading.Lock() if __name__ == '__main__': t1 = MyThread1() t2 = MyThread2() t1.start() t2.start()
那么死锁我们可以通过怎样的方式进行避免呢?
1、程序设计时要尽量避免(银行家算法)
2、添加超时时间等(即我给线程设置一个时间限定,若时间到了该线程还没有抢到锁执行代码,则要么直接终止另外一个线程,直接运行,要么避开。)
8、多任务版的UDP聊天器
在上一篇中的随笔中,我们实现了基本的UDP聊天器的,但是发送完了自能等着接收消息,接收完消息就只能等着发送消息,无法实现随时发送消息和接收消息,接下这个是升级版:
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket,threading def send_msg(socket_obj,aim_addr): """发送消息""" while True: send_data =input("请输入发送的内容:") socket_obj.sendto(send_data.encode("utf-8"),aim_addr) if send_data =="exit": break def recv_msg(socket_obj): """接收消息""" while True: recv_data = socket_obj.recvfrom(1024) recv_content = recv_data[0].decode("gbk") recv_addr = recv_data[1] print("%s : %s !" % (recv_addr, recv_content)) def main(): # 1.创建socket对象 udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 2.绑定本地信息 udp_socket.bind(("",6969)) # 3.发送信息 desk_ip =input("enter desk_ip -->:") desk_port =int(input("enter desk_port-->:")) desk_addr=(desk_ip,desk_port) # 4.接收消息 #创建两个子线程 t1 = threading.Thread(target=send_msg,args=(udp_socket,desk_addr)) t2 = threading.Thread(target=recv_msg,args=(udp_socket,)) t1.start() t2.start() # 5.关闭套接字 udp_socket.close() if __name__ == "__main__": main()
Over....................................,
本文来自博客园,作者:Little_five,转载请注明原文链接:https://www.cnblogs.com/littlefivebolg/p/9291919.html