1、多任务
有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的
1.1 程序中模拟多任务
import time
def sing():
for i in range(3):
print('I am singing...%d' % i)
time.sleep(1)
def dance():
for i in range(3):
print('I am dancing...%d' % i)
time.sleep(1)
if __name__ == '__main__':
sing()
dance()
上面代码返回结果:
I am singing…0
I am singing…1
I am singing…2
I am dancing…0
I am dancing…1
I am dancing…2
通过返回结果可知,singing执行完成,dance才会开始
1.2 多任务的理解
并行:真的多任务 cpu大于当前执行的任务
并发:假的多任务 cpu小于当前执行的任务
线程完成多任务
threading.Thread() 有两个典型参数:
target : 指定 这个线程去哪个函数里面去执行代码
args: 指定将来调用 函数的时候 传递什么数据过去
args参数指定的一定是一个元组类型
import time
import threading
def sing():
for i in range(3):
print('I am singing...%d' % i)
time.sleep(1)
def dance():
for i in range(3):
print('I am dancing...%d' % i)
time.sleep(1)
if __name__ == '__main__':
# sing()
# dance()
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start() # 主线程会等子线程执行完之后,主线程才会结束
上面代码返回:
I am singing…0
I am dancing…0
I am dancing…1
I am singing…1
I am dancing…2
I am singing…2
说明:通过上面返回结果可知,singing 和 dance 同时进行
1.3 其它特性
查看线程数量
threading.enumerate() 查看当前线程的数量
验证子线程的执行与创建
当调用Thread的时候,不会创建线程。
当调用Thread创建出来的实例对象的start方法的时候,才会创建线程以及开始运行这个线程。
import time
import threading
def sing():
for i in range(3):
print('I am singing...%d' % i)
# time.sleep(1)
def dance():
for i in range(3):
print('I am dancing...%d' % i)
# time.sleep(1)
if __name__ == '__main__':
# sing()
# dance()
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start() # 主线程会等子线程执行结束之后,主线程继续执行
print(threading.enumerate())
运行上面程序返回结果:
I am singing…0
I am singing…1I am dancing…0
[<_MainThread(MainThread, started 8496)>, <Thread(Thread-1, started 2276)>, <Thread(Thread-2, started 3648)>]
I am singing…2
I am dancing…1
I am dancing…2
通过结果可知,一个主线程_MainThread,两个子线程:Thread-1 Thread-2
1.4 守护线程 setDaemon() 和 jion()
setDaemon() 守护线程 不会等子线程结束
jion() 等子线程执行结束,主线程继续执行
import time
import threading
def demo():
for i in range(3):
print("holle world")
time.sleep(0.1)
def main():
t = threading.Thread(target=demo)
# 守护线程 不会等子线程结束
t.setDaemon(True)
t.start()
#等子线程执行结束,主线程继续执行
t.join()
print('1')
t.setDaemon(True) #不等子线程打印三次 holle world ,主线程结束
t.join() 等子线程结束,主线程继续执行
1.5 验证子线程的创建与执行
import time
import threading
def demo():
for i in range(5):
print('执行demo函数')
time.sleep(1)
def main():
print('1',threading.enumerate())
t = threading.Thread(target=demo)
print('2',threading.enumerate())
#创建子线程,开始执行
t.start()
print('3',threading.enumerate())
if __name__ == '__main__':
main()
返回结果可以看到,1、2是主线程在运行,直到 t.start() 执行后才会看到主线程和子线程
2、继承Thread类创建线程
run() 函数是在主函数 a.start() 时候自动调用的
import time
import threading
class A(threading.Thread):
def run(self):
for i in range(5):
self.demo()
def demo(self):
print('我是demo')
def main():
a = A()
a.start()
if __name__ == '__main__':
main()
3 线程间通信
3.1 什么是线程间通信呢?
多个线程处理同一资源,但是任务不同
3.2 为什么要通信?
如果各个线程之间各干各的,确实不需要通信,这样的代码也十分的简单。但这一般是不可能的,至少线程要和主线程进行通信,不然计算结果等内容无法取回。而实际情况中要复杂的多,多个线程间需要交换数据,才能得到正确的执行结果。
3.3 多线程共享全局变量(线程间通信)
修改全局变量一定需要加global吗?
答案是:在一个函数中,对全局变量进行修改的时候,是否要加global要看是否对全局变量的指向进行了修改,如果修改了指向,那么必须使用global,仅仅是修改了指向的空间中的数据,此时不用必须使用global
看下面例子
import threading
import time
num = 100
lis = [2, 3]
def demo1():
# global 让 demo1 和 demo2 访问的是同一个 num
global num
#global lis
num += 50
#而list类型变量 lis 通过“append”函数添加对象就不用声明全局,因为lis的指向没有改变
lis.append(4)
print('demo1.num----%d' % num)
print('demo1.lis----{}'.format(lis))
#如果 lis 以“+”的方式添加元素,那么因为指向跟append不同,而必须用“global”声明全局变量,否则程序报错
# lis = lis + [7]
# print('demo1.lis----{}'.format(lis))
def demo2():
global lis
lis.append(5)
print('demo2.num----%d' % num)
print('demo2.lis----{}'.format(lis))
def main():
# global lis
t1 = threading.Thread(target=demo1)
t2 = threading.Thread(target=demo2)
t1.start()
time.sleep(0.1)
t2.start()
time.sleep(0.1)
print('main-----%d' % num)
if __name__ == '__main__':
main()
上面代码返回值
3.4多线程参数-args
threading.Thread(target=test, args=(num,))
import time
import threading
lst = [11, 22]
print('lst---:',id(lst))
def demo1(lst):
lst.append(33)
print('demo1.lst----:', lst,id(lst))
def demo2(lst):
print('demo2.lst----:', lst,id(lst))
def main():
t1 = threading.Thread(target=demo1, args=(lst, ))
t2 = threading.Thread(target=demo2, args=(lst, ))
# print(t1.__dict__)
t1.start()
time.sleep(0.1)
t2.start()
time.sleep(0.1)
print('mian.lst----:', lst,id(lst))
if __name__ == '__main__':
main()
返回值:
4、共享全局变量资源竞争
多线程是为了提高效率,让程序并发执行,但并发执行会引起资源竞争,看下面的程序,当nums参数很小时10万级不会出问题,但百万级时
4.1 一个资源竞争导致结果错误的例子
import threading
import time
num = 0
def demo1(nums):
global num
for i in range(nums):
num += 1
print('demo1-----num:{}'.format(num))
def demo2(nums):
global num
for i in range(nums):
num += 1
print('demo2-----num:{} \n'.format(num))
def main():
t1 = threading.Thread(target=demo1, args=(1000,))
t2 = threading.Thread(target=demo2, args=(1000,))
t1.start()
t2.start()
print('main-----num:{}'.format(num))
if __name__ == '__main__':
main()
当nums数字不大时,因为处理器处理速度快,不会出问题,当数字大了,就会出现demo1 和 demo2 资源竞争的问题。
nums = 10000时,返回值如下:
当nums= 100000时,返回值就会错乱,因为demo1 demo2 和主函数 是根据cpu分时间片运行的,变量num得不到很好的控制,要解决这个问题需要用到互斥锁
4.1.1 python字节码
'''
python 字节码 --> python 虚拟机来执行python字节码
'''
import dis
def add_num(a):
a += 1
print(dis.dis(add_num))
上面执行结果:能看出来a被加载,a加1,和返回a的过程
4.2 互斥锁
一个线程写入,一个线程读取,没问题,如果两个线程都写入呢?
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
某个线程要更改共享数据时,先将其锁定,此时资源的状态为"锁定",其他线程不能改变,只到该线程释放资源,将资源的状态变成"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
mutex = threading.lock() 创建一个互斥锁对象
mutex.acquire() 上锁
mutex.release() 解锁
import threading
import time
num = 0
# 创建一个互斥锁对象,默认是没有上锁的
mutex = threading.Lock()
def demo1(nums):
global num
#上锁共享变量 num
mutex.acquire()
for i in range(nums):
num += 1
#解锁共享变量 num
mutex.release()
print('demo1---num:', num)
def demo2(nums):
global num
#上锁共享变量 num
mutex.acquire()
for i in range(nums):
num += 1
#解锁共享变量 num
mutex.release()
print('demo2---num:', num)
def main():
t1 = threading.Thread(target=demo1, args=(1000000,))
t2 = threading.Thread(target=demo2, args=(1000000,))
t1.start()
t2.start()
#主进程睡眠1秒钟,给demo1 和 demo2 执行时间
#还可以用 t1.join() t2.join() 函数,等待子函数执行完之后,再执行主函数
time.sleep(1)
print('main-----num:%d' % num)
if __name__ == '__main__':
main()
返回值:
重入锁:mutex = threading.Rlock()
重入锁可以嵌套,但上锁与解锁必须成对出现
5 线程同步
如果想让多线程实现下面信息问答同步需要用到 threading.condition 方法函数
天猫精灵:小爱同学
小爱同学:在
天猫精灵:现在几点了?
小爱同学:你猜猜现在几点了
6 多任务版udp聊天
1 创建套接字
2 绑定本地信息
3 获取对方IP和端口
4 发送、接收数据
5 创建两个线程,去执行功能
import threading
import socket
def recv_msg(udp_socket):
while True:
recv_data = udp_socket.recvfrom(1024)
print('收到%s发来的消息:%s' % (recv_data[1], recv_data[0].decode('gbk')))
# print(recv_data)
def send_msg(udp_socket, dest_ip, dest_port):
while True:
send_data = input('清输入发送的内容:')
udp_socket.sendto(send_data.encode('gbk'), (dest_ip, dest_port))
def main():
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# bind_str = ("",8080)
udp_socket.bind(("", 8899))
# 对方ip 和 端口
dest_ip = '192.168.2.119'
dest_port = 8090
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()
网络调试助手网络设置为UDP