【python高级基础 笔记】多任务-线程
目录
一些基础点:
- thread模块是比较底层的模块,python的 threading模块 对thread做了一些包装,使用threading模块能完成多任务的程序开发。如 :t1 = threading.Thread(target=函数名,args=元祖) ,再通过 t1.start() 创建线程。(注:args参数非必须,是作为函数的参数使用)
- 注意:当调用Thread的时候,不会创建线程,当调用Thread创建出来的实例对象的start方法时,才会创建线程以及让这个线程开始运行。
- 使用 threading.enumerate() 可以查看线程数量: length = len(threading.enumerate()) ,一般要配合时延,不然线程执行太快,无法查看。
- 只有子线程执行完毕,主线程才能结束。
- 为了让每个线程的封装性更好,所以使用threading模块时,往往会定义一个新的子类class,只要继承
threading.Thread
就可以了,然后重写run
方法 (想干什么在run方法中定义)。通过 对象.start() 自动调用run方法。(适合于一个线程要做的事情比较复杂,且分成多个函数来做) - 注:一个对象.start() 只能启动一个线程。
1.通过继承Thread方法创建线程 小实例
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'm Thread-1 @ 0
# I'm Thread-1 @ 1
# I'm Thread-1 @ 2
2. 多线程共享全局变量、同步、互斥锁
- 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
- 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
- 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
- 同步就是协同步调,按预定的先后次序进行运行。"同"字是指协同、协助、互相配合。 如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。
- 资源竞争可以通过线程同步来进行解决。最简单的同步机制即引入互斥锁。
- 互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞
- 如果在调用acquire对这个锁上锁之前 ,它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
上锁解锁过程:
- 当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
- 每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
- 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
优点:确保了某段关键代码只能由一个线程从头到尾完整地执行
缺点:阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了;
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。造成应用的停止响应。
避免死锁:银行家算法(待续)、添加超时时间等。
2.1. 用互斥锁解决资源竞争 小实例
上锁:
- 如果之前 没有被上锁,那么此时上锁成功;
- 如果上锁之前 已经被上锁了,那么此时会堵塞在这里,直到这个锁被解开为止。
注:本例也可以将整个for循环上锁,但效率会低些。( g_num += 1 该语句执行时有多个过程,加锁后保证+1的动作执行完毕)
import threading
import time
# 定义一个全局变量
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("-----in 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("-----in test2 g_num=%d----" % g_num)
# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test2, args=(1000000,))
t1.start()
t2.start()
# 等待上面的2个线程执行完毕....
time.sleep(2)
print("-----in main Thread g_num = %d---" % g_num)
if __name__ == "__main__":
main()
执行结果:
-----in test1 g_num=1957071----
-----in test2 g_num=2000000----
-----in main Thread g_num = 2000000---
2.2 UDP聊天器 (多任务版)
线程1:接收数据然后显示
线程2:检测键盘数据然后通过udp发送数据
import socket
import threading
def recv_msg(udp_socket):
"""接收数据"""
# 接收数据
while True:
recv_data = udp_socket.recvfrom(1024)
print(recv_data)
def send_msg(udp_socket, dest_ip, dest_port):
"""发送数据"""
while True:
send_data = input("please input message:")
udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))
def main():
# 1 创建套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2 绑定本地信息
udp_socket.bind(("", 8890))
# 3 获取对方ip
dest_ip = input("plseae input destination ip:")
dest_port = int(input("plseae input destination port:"))
# 4 创建2个线程,执行相应功能
t_recv = threading.Thread(target=recv_msg, args=(udp_socket, ))
t_send = threading.Thread(target=send_msg, args=(udp_socket, dest_ip, dest_port))
t_recv.start()
t_send.start()
if __name__ == "__main__":
main()