多任务复习
1.什么是多任务
简单的说,就是同时可以运行多个任务。
2.多任务执行原理
操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
3.线程
- 什么是线程
线程就是程序在执行代码的那个执行流 - 使用threading模块
- 单线程执行
import time def say_sorry(): print('我错了,我能吃饭么?') time.sleep(1) for i in range(5): say_sorry()
- 多线程执行
import threading import time def say_sorry(): print('我错了,我能吃饭了么?') time.sleep(1) for i in range(5): t = threading.Thread(target=say_sorry) t.start() # 启动线程,即让线程开始执行
- 说明
- 可以看出多线程并发操作,浪费的时间短
- 当调用start()时,才会真正的创建线程,并且开始执行
- 查看线程的数量
使用threading.enumerate()能够得到当前程序在运行时,所有的线程信息,以列表的方式返回 - 总结:
- 一个多任务线程中最少有一个线程(主线程)
- 单线程执行
4.创建线程传递参数
- 传递多个参数
from threading import Thread import time def work1(num1, num2): print("----in work1--num1=%d,num2=%d-" % (num1, num2)) def work2(num1, num2, num3): print("----in work1--num1=%d,num2=%d,num3=%d-" % (num1, num2, num3)) t1 = Thread(target=work1, args=(11, 22)) t1.start() t2 = Thread(target=work2, args=(33, 44, 55)) t2.start()
- 传递命名参数
from threading import Thread import time def work1(num1, num2, m): print("----in work1--num1=%d,num2=%d,m=%d-" % (num1, num2, m)) def work2(num1, num2, num3, n): print("----in work1--num1=%d,num2=%d,num3=%d,n=%d-" % (num1, num2, num3, n)) t1 = Thread(target=work1, args=(11, 22), kwargs={"m": 100}) t1.start() t2 = Thread(target=work2, args=(33, 44, 55), kwargs={"n": 200}) t2.start()
5. 案例:同时收发数据的UDP聊天程序
import socket
import threading
def send_msg(udp_socket):
"""获取键盘数据,并将其发送给对方"""
while True:
print("1: 发送数据")
print("2: 退出程序")
op = input("请输入操作序号:")
if op == "1":
# 1. 输入对方的ip地址
dest_ip = input("\n请输入对方的ip地址:")
# 2. 输入对方的port
dest_port = int(input("\n请输入对方的port:"))
while True:
# 3. 从键盘输入数据
msg = input("\n请输入要发送的数据:")
if msg:
# 4. 发送数据
udp_socket.sendto(msg.encode("utf-8"), (dest_ip, dest_port))
else:
# 要是没有输入内容则认为是要重新输入ip、port
break
elif op == "2":
break
udp_socket.close()
def recv_msg(udp_socket):
"""接收数据并显示"""
while True:
try:
# 1. 接收数据
recv_msg = udp_socket.recvfrom(1024)
except:
break
else:
# 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. 创建一个新的线程,用来接收数据
udp_r = threading.Thread(target=recv_msg, args=(udp_socket,))
# 4. 创建一个新的线程,用来发送数据
udp_s = threading.Thread(target=send_msg, args=(udp_socket,))
# 5. 运行创建的子线程
udp_r.start()
udp_s.start()
if __name__ == "__main__":
main()
总结:
- udp是全双工的,可以同时收发数据
- 创建一个udp套接字可以传递多个线程同时使用,一个收数据,一个发数据
- 如果某个线程将套接字close,那么意味着这个udp套接字不能在使用了
6.创建线程的另外一种方式
- 定义一个新的类,继承Thread类
- 在这个类中实现run方法
- 在run方法中写如要执行的代码
- 当使用这类创建一个对象后,调用对象的start方法就可以让这个线程执行,且会自动执行run方法的代码
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
msg = "I'm "+ self.name + ' @ '+str(i) # name属性中保存的是当前线程的名字
print(msg)
time.sleep(1)
if __name__ == '__main__':
t = MyThread()
t.start()
7.并发TCP服务器
import socket
import threading
class HandleData(threading.Thread):
def __init__(self, client_socket):
super().__init__()
self.client_socket = client_socket
def run(self):
# 接收/发送数据
while True:
recv_content = self.client_socket.recv(1024)
if len(recv_content) != 0:
print(recv_content)
self.client_socket.send(recv_content)
else:
self.client_socket.close()
break
def __del__(self):
self.client_socket.close()
class TCPServer(threading.Thread):
def __init__(self, port):
# 调用父类的初始化方法
# threading.Thread.__init__(self)
super().__init__()
# 创建套接字
self.server_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地信息
self.server_s.bind(("", port))
# 将套接字由默认的主动链接模式改为被动模式(监听模块)
self.server_s.listen(128)
def run(self):
# 等待客户端进行链接
while True:
new_s, client_info = self.server_s.accept()
print(client_info)
# t = HandleData(new_s)
# t.start()
HandleData(new_s).start()
def __del__(self):
# 关闭套接字
self.server_s.close()
def main():
tcp_server = TCPServer(7788) # 7788表示TCP要绑定的端口
tcp_server.start()
if __name__ == '__main__':
main()
8.全局变量
- 在一个进程中所有线程共享全局变量,很方便在多个线程间共享数据
- 缺点是:线程是对全局变量随意篡改可能造成多线程之间对全局变量的混乱(即线程非安全)
- 如果多个线程对同一个全局变量操作,会出现资源竞争(解决方法互斥锁)
9.互斥锁
- 为什么使用互斥锁?
防止多个线程出现资源竞争的问题 - 状态
锁定 或者 非锁定 - 使用互斥锁
- 创建锁 :mutex = threading.Lock()
- 锁定:mutex.acquire()
- 释放:mutex.release()
- 案例实现
完成2个线程对同一个变量各加100万次操作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("---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) # 创建一个互斥锁 # 默认是未上锁的状态 mutex = threading.Lock() # 创建2个线程,让他们各自对g_num加1000000次 p1 = threading.Thread(target=test1, args=(1000000,)) p1.start() p2 = threading.Thread(target=test2, args=(1000000,)) p2.start() # 等待计算完成 while len(threading.enumerate()) != 1: time.sleep(1) print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
- 好处
确保了某段关键代码同时只能由一个线程从头到尾完整地执行 - 坏处
- 阻止了线程并发执行,包含锁的某段代码实现上只能以单线程模式执行,效率就大大地下降了
- 由于可以存着多个锁,不同的线程有不同的锁,并试图获取对方持有的锁时,可能造成死锁
10.进程
- 什么是程序?
就是一堆代码的称呼 - 什么是程序?
一个程序运行起来后,代码+用到的资源 称之为进程,他是操作系统分配资源的基本单位 - 实现多任务的方式
- 创建Process对象
- 基础Process类,创建自己的对象,实现run重写
- 使用进程池
- 创建进程方式1
from multiprocessing import Process import time def test(): """子进程单独执行的代码""" while True: print('---test---') time.sleep(1) if __name__ == '__main__': p=Process(target=test) p.start() # 主进程单独执行的代码 while True: print('---main---') time.sleep(1)
- 进程PID(进程号,一个程序一个进程号)
from multiprocessing import Process import os import time def run_proc(): """子进程要执行的代码""" print('子进程运行中,pid=%d...' % os.getpid()) # os.getpid获取当前进程的进程号 print('子进程将要结束...') if __name__ == '__main__': print('父进程pid: %d' % os.getpid()) # os.getpid获取当前进程的进程号 p = Process(target=run_proc) p.start()
- 每个进程都有1个数字来标记,这个数字称之为进程号
- Linux系统中查看PID的命令是 ps
- Linux命令 kill pid 的方式结束一个进程,进程结束,表示程序结束
- Process创建实例对象的常用方法
- start():启动进程实例(创建子进程)
- is_alive():判断子进程是否还在或者
- join([timeout]):是否等待子进程执行结束,或等待多时秒
- terminate():不管任务是否完成,立即终止子进程
- 创建进程方式2
from multiprocessing import Process import time class MyNewProcess(Process): def run(self): while True: print('---1---') time.sleep(1) if __name__=='__mian__': p = MyNewProcess() # 调用p.start()方法,p会先去父类中寻找start(),然后在Process的start方法中调用run方法 p.start() while True: print('---Main---') time.sleep(1)
- 传递参数
from multiprocessing import Process import os from time import sleep def run_proc(name, age, **kwargs): for i in range(10): print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age, os.getpid())) print(kwargs) sleep(0.2) if __name__=='__main__': p = Process(target=run_proc, args=('test',18), kwargs={"m":20}) p.start() sleep(1) # 1秒中之后,立即结束子进程 p.terminate() p.join()
- 调用Process类创建进程对象时
- target指明 创建进程后,进程指定的代码是哪个函数
- args、kwargs用来给 那个函数指明传递的实参
1. args:元组
2. kwargs:字典
- 调用Process类创建进程对象时
- 进程之间不共享全局变量
11.进程间通信-Queue
- 进程间相互独立,数据不共享,但有时需要数据共享,就需要进程间通信
- 可以使用multiprocessing模块的Queue实现多进程之间的数据传递
from multiprocessing import Queue q = Queue(3) # 初始化一个Queue对象,最多可接收三条put消息 q.put("消息1") q.put("消息2") print(q.full()) # False q.put("消息3") print(q.full()) # True # 因为消息列队已满,所以会导致下面的try都会抛出异常, # 第一个try会等待2秒后再抛出异常 # 第二个Try会立刻抛出异常 try: q.put("消息4", True, 2) except: print("消息列队已满,现有消息数量:%s" % q.qsize()) try: q.put_nowait("消息4") except: print("消息列队已满,现有消息数量:%s" % q.qsize()) # 推荐的方式,先判断消息列队是否已满,再写入 if not q.full(): q.put_nowait("消息4") # 读取消息时,先判断消息列队是否为空,再读取 if not q.empty(): for i in range(q.qsize()): print(q.get_nowait())
- 说明
- 使用Queue()时,若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头)
- Queue的几个方法功能说明:
- Queue.qsize():返回当前队列包含的消息数量
- Queue.empty():如果队列为空,返回True,反之False
- Queue.full():如果队列满了,返回True,反之False
- Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True
- 如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出Queue.Empty异常
- 如果block值为False,消息列队如果为空,则会立刻抛出Queue.Empty异常
- Queue.get_nowait():相当Queue.get(False)
- Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True
- 如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出Queue.Full异常
- 如果block值为False,消息列队如果没有空间可写入,则会立刻抛出Queue.Full异常
- Queue.put_nowait(item):相当Queue.put(item, False)
12.创建进程方式3-进程池
from multiprocessing import Pool
import os
import random
import time
def worker(num):
for i in range(5):
print('===pid=%d==num=%d='%(os.getpid(),num))
time.sleep(1)
# 3表示进程池中最多有三个进程一起执行
pool=Pool(3)
for i in range(10):
print('---%d---'%i)
# 向进程中添加任务
# 注意:如果添加的任务数量超过了进程池中进程的个数的话,那么就不会接着往进程池中添加,
# 如果还没有执行的话,他会等待前面的进程结束,然后在往
# 进程池中添加新进程
pool.apply_async(worker,(i,))
pool.close() # 关闭进程池
pool.join() # 主进程在这里等待,只有子进程全部结束之后,在会开启主线程
- apply_async(func[, args[, kwds]]):使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
- close():关闭Pool,使其不再接受新的任务;
- terminate():不管任务是否完成,立即终止;
- join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
13.进程池中的Queue
# -*- coding:utf-8 -*-
# 修改import中的Queue为Manager
from multiprocessing import Manager,Pool
import os,time,random
def reader(q):
print("reader启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in range(q.qsize()):
print("reader从Queue获取到消息:%s" % q.get(True))
def writer(q):
print("writer启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in "itcast":
q.put(i)
if __name__=="__main__":
print("(%s) start" % os.getpid())
q = Manager().Queue() # 使用Manager中的Queue
po = Pool()
po.apply_async(writer, (q,))
time.sleep(1) # 先让上面的任务向Queue存入数据,然后再让下面的任务开始从中取数据
po.apply_async(reader, (q,))
po.close()
po.join()
print("(%s) End" % os.getpid())
运行结果:
(11095) start
writer启动(11097),父进程为(11095)
reader启动(11098),父进程为(11095)
reader从Queue获取到消息:i
reader从Queue获取到消息:t
reader从Queue获取到消息:c
reader从Queue获取到消息:a
reader从Queue获取到消息:s
reader从Queue获取到消息:t
(11095) End
14.案例:文件copy器(多进程版)
import multiprocessing
import os
import time
import random
def copy_file(queue, file_name,source_folder_name, dest_folder_name):
"""copy文件到指定的路径"""
f_read = open(source_folder_name + "/" + file_name, "rb")
f_write = open(dest_folder_name + "/" + file_name, "wb")
while True:
time.sleep(random.random())
content = f_read.read(1024)
if content:
f_write.write(content)
else:
break
f_read.close()
f_write.close()
# 发送已经拷贝完毕的文件名字
queue.put(file_name)
def main():
# 获取要复制的文件夹
source_folder_name = input("请输入要复制文件夹名字:")
# 整理目标文件夹
dest_folder_name = source_folder_name + "[副本]"
# 创建目标文件夹
try:
os.mkdir(dest_folder_name)
except:
pass # 如果文件夹已经存在,那么创建会失败
# 获取这个文件夹中所有的普通文件名
file_names = os.listdir(source_folder_name)
# 创建Queue
queue = multiprocessing.Manager().Queue()
# 创建进程池
pool = multiprocessing.Pool(3)
for file_name in file_names:
# 向进程池中添加任务
pool.apply_async(copy_file, args=(queue, file_name, source_folder_name, dest_folder_name))
# 主进程显示进度
pool.close()
all_file_num = len(file_names)
while True:
file_name = queue.get()
if file_name in file_names:
file_names.remove(file_name)
copy_rate = (all_file_num-len(file_names))*100/all_file_num
print("\r%.2f...(%s)" % (copy_rate, file_name) + " "*50, end="")
if copy_rate >= 100:
break
print()
if __name__ == "__main__":
main()
15.进程、线程对比
- 理解:
- 进程,能够完成多任务,比如一台电脑运行多个qq
- 线程,能够完成多任务,一个qq中的多个聊天窗口
- 定义
- 进程是系统进程资源分配和调度的一个独立单位
- 线程是进程的一个实体,是CUP调度和分派的基本单位,他是币进程跟小的独立运行的基本单位
- 区别
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程(资源比进程少),是的多线程程序的并发性高
- 进程在纸箱过程中拥有的独立内存单位,而多个线程共享内存,从而极大提高了程序的运行效率
- 线程不能够独立执行,必须依存在进程中
- 优缺点
线程和进程在使用上各有优缺点- 线程执行开销小,但不利于资源的管理和保护
- 进程执行开销大,但有利于资源的管理和保护