网络编程
ip地址
ip地址:用来在网络中标记一台电脑,比如192.168.1.1;在本地局域网上是唯一的。
端口
端口是通过端口号来标记的,端口号只有整数,范围是从0到65535,其中包含知名端口和动态端口
知名端口:0~1023 与我们熟知的110,120一样
动态端口:范围是从1024到65535,动态分配是指当一个系统程序或应用程序程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用
socket
import socket
socket.socket(AddressFamily, Type) # 带有两个参数
AddressFamily:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET,也 就是ipv4
Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)
"""
单工 :收音机
半双工:对讲机 ---- 可以收又可以发 我在发的时候收不了,我在收的时候发不了
全双工:相当于 打电话
"""
'socket属于全双工'
网络--udp
udp的基础通信
import socket
# 1.创建套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2.发送数据
udp_socket.sendto(bytes型数据, 对方的ip以及端口号)
# 3.关闭套接字
udp_socket.close()
发送数据
import socket
def main():
# 创建一个udp套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
local_daar = ("192.168.1.100", 8080)
udp_socket.bind(local_daar)
# 从键盘输入数据
while True:
send_data = input("请输入要发送的数据:")
if send_data == "exit":
break
# 可以使用套接字收发数据
udp_socket.sendto(send_data.encode("utf-8"), ("192.168.1.100", 8081))
# 关闭套接字
udp_socket.close()
if __name__ == "__main__":
main()
接收数据
import socket
def main():
# 1.创建套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2.绑定一个本地信息
local_daar = ("192.168.1.100", 8081)
udp_socket.bind(local_daar)
# 3.接收数据
while True:
recv_data = udp_socket.recvfrom(1024)
recv_msg = recv_data[0]
send_addr = recv_data[1]
# 打印接收到的数据
# print(recv_data)
print("%s:%s" % (str(send_addr), recv_msg.decode("gbk")))
# 4.关闭套接字
udp_socket.close()
if __name__ == "__main__":
main()
udp简单的聊天器
import socket
def send_msg(udp_socket):
# 获取要发送的内容
send_data = input("请输入要发送的消息:")
dest_ip = input("请输入对方的ip:")
dest_port = int(input("请输入对方的port:"))
udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))
def recv_msg(udp_socket):
recv_data = udp_socket.recvfrom(1024)
print("%s:%s" % (str(recv_data[1], recv_data[0].decode("utf-8"))))
def main():
# 创建套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定信息
udp_socket.bind(("", 8008))
# 循环来进行处理事情
while True:
print("----xxx聊天器-----")
print("1:发送消息")
print('2:接收消息')
print("0:退出系统")
op = input("请输入功能:")
if op == "1":
# 发送
send_msg(udp_socket)
elif op == "2":
# 接收并显示
recv_msg(udp_socket)
elif op == "0":
break
else:
print("输入有误请重新输入.....")
if __name__ == "__main__":
main()
网络--tcp
tcp的基础通信
import socket
# 1.创建套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.链接服务器
tcp_socket.connect((服务器ip,服务器port))
# 3.发送数据
tcp_socket.send(bytes类型数据)
# 4.关闭套接字
tcp_socket.close
tcp服务端
# 1.socket创建一个套接字
# 2.bind绑定ip和port
# 3.listen使套接字变为可以被动链接
# 4.accept等待客户端的链接
# 5.recv/send接收发送数据
from socket import *
# 创建socket
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 本地信息
address = ('', 7788)
# 绑定
tcp_server_socket.bind(address)
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcp_server_socket.listen(128)
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务
# client_socket用来为这个客户端服务
# tcp_server_socket就可以省下来专门等待其他新客户端的链接
client_socket, clientAddr = tcp_server_socket.accept()
# 接收对方发送过来的数据
recv_data = client_socket.recv(1024) # 接收1024个字节
print('接收到的数据为:', recv_data.decode('gbk'))
# 发送一些数据到客户端
client_socket.send("thank you !".encode('gbk'))
# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
client_socket.close()
tcp客户端
import socket
def main():
# 1.创建套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.链接服务器
server_ip = input('请输入需要链接的服务器ip:')
server_port = int(input('请输入需要链接的服务器port:'))
tcp_socket.connect((server_ip, server_port))
# 3.发送接收数据
tcp_socket.send(b'hahaha')
# 4.关闭套接字
tcp_socket.close()
if __name__ == "__main__":
main()
tcp程序案例
循环为一个客户端服务
import socket
def main():
# 1.买个手机(创建套接字 scoket)
tcp_server_scoket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.插入手机卡(绑定本地信息)
tcp_server_scoket.bind(("", 7890))
# 3.将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
tcp_server_scoket.listen(128)
while True:
print("等待一个新的客户端的到来")
# 4.等待比尔的电话到来(等待客户端的链接 accept)
new_clinet_scoket, client_addr = tcp_server_scoket.accept()
print("一个新的客户端已经到来%s" % str(client_addr))
print(client_addr)
# 循环目的:为同一个客户端服务
while True:
# 接收客户端发送过来的请求
recv_data = new_clinet_scoket.recv(1024)
print("客户端发送过来的请求是:%s" % recv_data.decode("utf-8"))
# 如果recv堵塞那么有两种方式:1客户端发送过来数据 2客户端调用close导致堵塞
if recv_data:
# 回送一部分数据给客户端
new_clinet_scoket.send("hhhh".encode("utf-8"))
else:
break
# 关闭套接字
new_clinet_scoket.close()
print("已经服务完毕")
tcp_server_scoket.close()
if __name__ == "__main__":
main()
下载文件
import socket
def send_file_2_client(new_clinet_scoket, client_addr):
# 1.接收客户端需要下载的文件名
# 接收客户端发送过来的要下载的文件名
file_name = new_clinet_scoket.recv(1024).decode("utf-8")
print("客户端(%s)需要下载文件是:%s " % (str(client_addr), file_name))
file_content = None
# 2.打开这个文件,读取数据
try:
f = open(file_name, "rb")
file_content = f.read()
f.close()
except Exception as ret:
print("没有要下载的文件(%s)" % file_name)
# 3.发送文件的数据给客户端
if file_content:
# 发送文件给客户端
new_clinet_scoket.send(file_content)
def main():
# 1.买个手机(创建套接字 scoket)
tcp_server_scoket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.插入手机卡(绑定本地信息)
tcp_server_scoket.bind(("", 7890))
# 3.将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
tcp_server_scoket.listen(128)
while True:
# 4.等待比尔的电话到来(等待客户端的链接 accept)
new_clinet_scoket, client_addr = tcp_server_scoket.accept()
# 5.调用函数,完成为客户端的服务
send_file_2_client(new_clinet_scoket, client_addr)
# 6.关闭套接字
new_clinet_scoket.close()
tcp_server_scoket.close()
if __name__ == "__main__":
main()
tcp 三次握手
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1]
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
tcp 四次挥手
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。[1]
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2]
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。[2]
tcp的四次挥手
多任务
1.多任务的介绍
现实生活中
有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的;
试想,如果把唱歌和跳舞这2件事情分开依次完成的话,估计就没有那么好的效果了(想一下场景:先唱歌,然后在跳舞,O(∩_∩)O哈哈~)
1.1程序表达
import time
def sing():
"""唱歌"""
for i in range(5):
print("***起风了***")
time.sleep(1)
def dance():
"""跳舞"""
for i in range(5):
print("***正在跳舞***")
time.sleep(1)
def main():
sing()
dance()
if __name__ == "__main__":
main()
# 运行结果如下:
***起风了***
***起风了***
***起风了***
***起风了***
***起风了***
***正在跳舞***
***正在跳舞***
***正在跳舞***
***正在跳舞***
***正在跳舞***
"""
注意:
很显然刚刚的程序并没有完成唱歌和跳舞同时进行的要求
如果想要实现唱歌跳舞同时进行,那么就需要一个新的方法,叫做:多任务
"""
2.多任务的概念
多任务的概念:什么叫多任务呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。这也称作时间片轮转
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
"""
注意:
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
"""
3.多任务-线程
# 利用python中的threading模块可以完成多任务
3.0 守护线程
# 主线程结束子线程也要结束
方法:
在启动线程前将线程属性 Deamon = true
3.1 单线程
import time,threading
def sing():
"""唱歌"""
for i in range(5):
print("***起风了***")
time.sleep(1)
def main():
t1 = threading.Thread(target=sing) # 实例化一个对象
t1.start() # 将会调用实例化对象中的run方法,创建一个子线程,子线程将执行target指向的函数中的代码,主线程继续向下执行
if __name__ == "__main__":
main()
3.2多线程
import time
import threading
def sing():
"""唱歌"""
for i in range(5):
print("***起风了***")
time.sleep(1)
def dance():
"""跳舞"""
for i in range(5):
print("***正在跳舞***")
time.sleep(1)
def main():
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
if __name__ == "__main__":
main()
"""
注意:
当threading.Thread(target=函数名称)时会实例化一个对象,这个实例化对象调用start时会创建一个子线程,子线程将会执行target指向的函数,主线程将继续执行下面的代码,即t2.start(),这也将继续创建一个子线程,执行target指向的函数。但这两个子线程执行的顺序是不确定的(可以说根据操作系统心情而定,哈哈哈。你也可以认为这两个子线程在争抢),所以不一定谁先调用start()谁就先会去执行
"""
3.3查看线程数(附加一些方法)
'threading.enumerate()'
# 当我们不确定线程是在何时创建时,可以利用查看线程数去知道线程会在什么时候被创建,调用threading.enumerate()方法
# 该方法返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程
# 另外一些内置方法:
"""
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
# join() : 可以让主线程等待子线程执行完毕再继续向下执行 相当于堵塞
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程对象。
# threading.current_thread():返回当前的线程对象,和上述方法一样。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
# threading.active_count():返回正在运行的线程数量,和上述方法一样
"""
import threading
import time
def test1():
for i in range(5):
print("***test1***%d" % i)
def test2():
for i in range(5):
print("***test2***%d" % i)
def main():
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
print(threading.enumerate())
t2.start()
print(threading.enumerate())
time.sleep(1)
print("2")
print(threading.enumerate())
if __name__ == "__main__":
main()
'由此可知,线程创建是在调用start时被创建'
# 循环查看线程数
import threading
import time
def test1():
for i in range(5):
print("***test1***%d" % i)
time.sleep(1)
def test2():
for i in range(10):
print("***test2***%d" % i)
time.sleep(1)
def main():
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
while True:
print(threading.enumerate())
time.sleep(1)
if len(threading.enumerate()) <= 1:
break
if __name__ == "__main__":
main()
3.4线程-注意点
通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法(与java的多线程实现方法有异曲同工之妙哈哈哈)
3.4.1 线程执行代码的封装
# 利用threading.getName()可以获取当前线程的名字,该名字是python解释器自动设置,自己也可以设置线程名-->threading.setName()
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()
说明:'python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。'
3.4.2 线程的执行顺序
mport 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()
说明:'从代码和执行结果我们可以看出,多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。'
3.4.3 总结
# 1.每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
# 2.当线程的run()方法结束时该线程完成。
# 3.无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
3.5 多线程-共享全局变量
多个线程操作同一个全局变量,会出现数据不安全
对于共享的数据的操作
如果是 += *= /= -= 都存在数据不安全的问题 l[0] += 1
如果是 append extend pop remove不会存在数据不安全的问题
3.5.1 共享全局变量
import time
import threading
g_num = 100
def test1():
global g_num
g_num += 1
print("---in test1 g_num=%d" % g_num)
def test2():
print("---in test2 g_num=%d" % g_num)
def main():
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
time.sleep(1)
t2.start()
time.sleep(1)
print("in main g_num=%d" % g_num)
if __name__ == "__main__":
main()
'结论:线程之间可以共享全局变量'
3.5.2 线程共享传递参数
import time
import threading
g_num = [11, 12]
def test1(temp):
temp.append(33)
print("---in test1 temp=%s" % str(temp))
def test2(temp):
print("---in test2 temp=%s" % str(temp))
def main():
t1 = threading.Thread(target=test1, args=(g_num,)) # args为一个元组,里面的元素为传递给函数的实参
t2 = threading.Thread(target=test2, args=(g_num,))
t1.start()
time.sleep(1)
t2.start()
time.sleep(1)
print("---in test1 g_num=%s" % str(g_num))
if __name__ == "__main__":
main()
3.5.3 共享全局变量的问题 --- 资源竞争
import time
import threading
import dis
g_num = 0
def test1(num):
global g_num
for i in range(num):
g_num += 1
print("---in test1 g_num=%d" % g_num)
def test2(num):
global g_num
for i in range(num):
g_num += 1
print("---in test1 g_num=%d" % g_num)
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test2, args=(1000000,))
t1.start()
t2.start()
time.sleep(2)
print(dis.dis(test1))
print(dis.dis(test2))
print("in main g_num=%d" % g_num)
if __name__ == "__main__":
main()
# 该程序为两个函数对一个全局变量各进行100万次加的操作,但结果最终不为200万
产生该结果的原因:程序最终是被CPU执行,执行时一句编程语言会被翻译成多句指令,并且这两个函数存在资源竞争(你也可以理解为时间片轮转,每个程序都执行一段时间,然后换另一个程序执行)
该程序出的问题在程序执行 g_num+=1
我们看一个函数: # dis中的dis可以看见操作系统具体做了什么 -----看下面的图⬇-----
import dis
g_num = 0
def func():
global g_num
g_num += 1
print(dis.dis(func))
# 1.当执行g_num+=1时,先加载g_num
# 2.随后进行加载常量1
# ....中间就不再看了,不太懂
# (重点) STORE_GLOBAL 这一句大概是存储全局变量g_num,问题就在这,两个函数存在资源竞争,时间片轮转,如果正好函数1执行到存储就停下来,g_num此时还是0。函数2开始执行,进行相同的操作,这个函数执行了存储这个指令,则此时g_num=1,然后时间片轮转,到原来函数1继续向下执行。而此时函数1执行存储指令,g_num=1。出现这种情况,函数虽然都执行了,但是全局变量真正只是加了一次。
3.6 同步的概念
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
"同"字从字面上容易理解为一起动作
其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。
3.6.1 线程同步解决资源竞争问题
# 通过线程同步来解决
思路:
# 1.系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
# 2.t1对g_num的值进行+1
# 3.t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了,而且是g_num的值不是0而是1
# 4.同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性
3.7 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
3.7.1 上锁解锁过程
threading模块中定义了Lock类,可以方便的处理锁定:
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire() # 当该锁已被上锁,在此会被堵塞,直到该锁被释放
# 释放
mutex.release()
"""
当一个线程调用锁的acquire()方法获得锁时,锁就进入locked状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为blocked状态,称为阻塞,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入unlocked状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
"""
3.7.2 互斥锁解决资源竞争问题
import time
import threading
g_num = 0
def test1(num):
global g_num
# 上锁
# mutex.acquire()
for i in range(num):
mutex.acquire()
g_num += 1
# 解锁
# mutex.release()
mutex.release()
print("---in test1 g_num=%d" % g_num)
def test2(num):
global g_num
# mutex.acquire()
for i in range(num):
mutex.acquire()
g_num += 1
mutex.release()
# mutex.release()
print("---in test1 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()
time.sleep(2)
print("in main g_num=%d" % g_num)
if __name__ == "__main__":
main()
3.7.3 总结
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
3.8 死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子:
import threading
import time
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()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
3.9 案例 - 多任务udp聊天器
import socket
import threading
def recv_msg(udp_scoket):
# 4.接收数据
while True:
reve_data = udp_scoket.recv(1024).decode('gbk')
print(reve_data)
def send_msg(udp_scoket, dest_ip, dest_port):
# 发送数据
while True:
send_data = input("请输入要发送的数据;")
udp_scoket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))
def main():
"""完成udp聊天器的整体控制"""
# 1.创建套接字
udp_scoket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2.绑定本地信息
udp_scoket.bind(("192.168.1.100", 7890))
# 3.获取对方的ip及port
dest_ip = input("请输入ip:")
dest_port = int(input("请输入port:"))
# 创建两个线程
t_recv = threading.Thread(target=recv_msg, args=(udp_scoket,))
t_send = threading.Thread(target=send_msg, args=(udp_scoket, dest_ip, dest_port))
t_recv.start()
t_send.start()
if __name__ == "__main__":
main()
3.9.1 信号量
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
from threading import Thread,Semaphore
import time
import random
def func(i):
sem.acquire()
print('第%s个客人进入屋子'%i)
time.sleep(random.randint(1,3))
print('第%s个客人离开屋子' % i)
sem.release()
sem = Semaphore(5)
for i in range(20):
t = Thread(target=func,args=(i,))
t.start()
3.9.2 事件
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
from threading import Thread,Event
import time
import random
def conn_mysql(e,i):
count = 1
while 1:
if e.is_set():# 如果为True,就是可以连接上数据库
break
if count > 3:
print('连接超时')
return
print('第%s个人正在尝试第%s次连接!'%(i,count))
e.wait(0.5)# 在这里阻塞等待0.5秒,模拟用户连接时的等待
count+=1
print('第%s个人连接成功'%i)
def check_mysql(e):
print('\033[45m 数据库正在维护 \033[0m')# 让数据库初始状态处于维护状态,默认所有用户连接不上
time.sleep(random.randint(1,2))# 随机1秒或2秒,如果随机1秒的话,用户就可以连接上,2秒就连接不上
e.set()# 将e.is_set()设为True
if __name__ == '__main__':
e = Event()
t = Thread(target=check_mysql,args=(e,))
t.start()
for i in range(10):# 产生10个线程都去尝试连接数据库
t1 = Thread(target=conn_mysql,args=(e,i))
t1.start()
模拟连接数据库操作
4.多任务-进程
程序:例如一个xxx.py的文件,没有执行是是静态的,不可以调动任何资源
进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。
4.1 进程的创建 -multiprocessing
import time
import threading
import multiprocessing
def test1():
while True:
print("1.....")
time.sleep(1)
def test2():
while True:
print("2.....")
time.sleep(1)
def main():
# t1=threading.Thread(target=test1)
# t2=threading.Thread(target=test2)
# t1.start()
# t2.start()
t1 = multiprocessing.Process(target=test1)
t2 = multiprocessing.Process(target=test2)
t1.start()
t2.start()
if __name__ == "__main__":
main()
'与线程类似,导入模块,创建对象,之后start创建子进程'
4.2 进程的执行
与线程一样,主进程中主线程会一步一步向下执行,创建对象,然后对象.start()创建子进程,与子线程不一样,子进程会对主进程进行复刻,然后执行target指向的函数,所以,当用多任务-进程时会消耗大量的资源
4.3 进程与线程的对比
功能:
进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
线程,能够完成多任务,比如 一个QQ中的多个聊天窗口
'-------------------------如下图解释⬇-------------------------'
再举个栗子
# 流水线 --- 比如组装
# 1.一个流水线就是一个进程
# 2.流水线上的零件就是资源,资源是共享的(进程与进程之间的资源是不共享的)
# 3.流水线旁的工人就是线程,线程完成组装任务 --- 也就是完成所需的功能函数
4.4 进程之间的通信:Queue
'因为进程与进程之间是相互独立的,所以之间无法共享全局变量'
# 利用队列来实现进程间的通信:multiprocessing模块的Queue实现多进程之间的数据传递,Queue本身是一个消息列队程序
初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头);
Queue.qsize():返回当前队列包含的消息数量;
Queue.empty():如果队列为空,返回True,反之False ;
Queue.full():如果队列满了,返回True,反之False;
Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True;
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
2)如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
Queue.get_nowait():相当Queue.get(False);
Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;
import multiprocessing
def download_from_web(q):
"""下载数据"""
# 模拟从网上下载的数据
data = [11, 22, 33, 44]
# 向队列中写入数据
for temp in data:
q.put(temp)
print("下载器已经下载完毕....")
def analyz_data(q):
"""数据处理"""
waitting_analy_data = list()
# 从队列中获取数据
while True:
data = q.get() # 当队列中没有数据可以获取时,会卡在这
# data = q.get_nowait() # 当队列中没有数据可以获取时,会主动抛出异常
waitting_analy_data.append(data)
if q.empty():
break
print(waitting_analy_data)
def main():
# 1.创建一个队列
q = multiprocessing.Queue()
p1 = multiprocessing.Process(target=download_from_web, args=(q,))
p2 = multiprocessing.Process(target=analyz_data, args=(q,))
p1.start()
p2.start()
if __name__ == "__main__":
main()
4.5 进程池Poll
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,并且会消耗大量资源,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务,请看下面的实例:
import multiprocessing
import os, time, random
def worker(msg):
t_start = time.time()
print("%s开始执行,进程号为%d " % (msg, os.getpid()))
# random.randrom()随机生成0-1之间的浮点数
time.sleep(random.random() * 2)
t_stop = time.time()
print("%s 执行完毕,耗时%0.2f" % (msg, t_stop - t_start))
if __name__ == "__main__":
po = multiprocessing.Pool(3) # 定义一个进程池,最大进程数为3
for i in range(0, 10):
# Pool().apply_async(要调用的目标,(传递给目标的参数元组,))
# 每次循环将会用空闲出来的子进程去调用目标
po.apply_async(worker, (i,))
print("....开始....")
po.close() # 关闭进程池,关闭后po不再接收新的请求
# 在利用进程池做多任务时,主进程不会等待子进程执行完毕再继续向下执行,而是直接向下执行后面的代码,所以需要使用join()使主进程等待子进程执行完毕再继续向下执行
po.join() # 等待po所有的子进程执行完成,必须放在close语句之后
print("....结束....")
multiprocessing.Pool常用函数解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
close():关闭Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
4.6 进程池中的Queue
如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
# 修改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())
4.6 文件夹copy器
import os
import multiprocessing
def copy_file(q, file_name, old_floder_names, new_floder_name):
"""完成文件的复制"""
print("模拟copy文件:从%s---->到%s文件名是:%s" % (old_floder_names, new_floder_name, file_name))
old_f = open(old_floder_names + "/" + file_name, "rb")
content = old_f.read()
old_f.close()
new_f = open(new_floder_name + "/" + file_name, "wb")
new_f.write(content)
new_f.close()
# 如果拷贝完了文件,那么就向队列中写入一个消息,表示已完成
q.put(file_name)
def main():
# 1.获取用户要copy的文件夹的名字
old_floder_name = input("请输入要copy的文件夹的名字:")
# 2.创建一个新的文件夹
try:
new_floder_name = old_floder_name + "[复件]"
os.mkdir(new_floder_name)
except:
pass
# 3.获取文件夹的所有的待copy的文件名字 listdir()
file_names = os.listdir(old_floder_name)
print(file_names)
# 4.创建进程池
po = multiprocessing.Pool(5)
# 5.创建一个队列
q = multiprocessing.Manager().Queue()
# 6.向进程池添加copy文件的任务
for file_name in file_names:
po.apply_async(copy_file, args=(q, file_name, old_floder_name, new_floder_name))
# 复制原文件夹中的文件,到新的文件夹的文件去
po.close()
# po.join() 下面有个while循环,不需要join了
all_file_num = len(file_names)
copy_ok_num = 0
while True:
file_name = q.get()
# print("已经完成copy:%s" % file_name)
copy_ok_num += 1
print("\r拷贝进度为:%.2f%%" % (copy_ok_num * 100 / all_file_num), end='')
if copy_ok_num >= all_file_num:
break
if __name__ == "__main__":
main()
5. 多任务-协程
5.1 迭代器
迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
# 判断一个对象是否可以迭代:
from collections import Iterable
print(isinstance(对象,Iterable)) # 返回为True说明对象是一个可迭代对象,False则不可迭代
# 判断一个对象是否是迭代器:
from collections import Iterator
print(isinstance(classmate_iterator,Iterator)) # 返回为True说明对象是一个迭代器
for...in...循环的本质
for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。
# 不多说,直接看代码:
# from collections import Iterable
# from collections import Iterator
import collections
import time
class Classmate(object):
def __init__(self):
self.names = list()
def add(self, name):
self.names.append(name)
def __iter__(self):
# 必须返回一个对象的引用,这个对象且必须有__iter__和__next__方法
"""如果想要一个对象称为一个可以迭代的对象,即可以使用for,那么必须使用__iter__方法"""
return ClassIterator(self)
class ClassIterator(object):
def __init__(self, obj):
self.obj = obj
self.num = 0
def __iter__(self):
pass
def __next__(self):
if self.num < len(self.obj.names):
ret = self.obj.names[self.num]
self.num += 1
return ret
else:
raise StopIteration # 主动抛出异常,结束代码
classmate = Classmate()
classmate.add("老王")
classmate.add("张三")
classmate.add("王二")
# print("判断classmate是否是可以迭代的对象:",isinstance(classmate,Iterable))
# classmate_iterator=iter(classmate)
# print("判断classmate_iterator是否是迭代器:",isinstance(classmate_iterator,Iterator))
# iter(classmate)
for name in classmate:
print(name)
time.sleep(1)
"""
# 1.当for循环时,解释器会查看被循环的对象是否是可迭代的(也就是查看该对象是否含有__iter__方法)
# 2.若对象是可迭代,for循环每访问一次数据,就会调用可迭代对象中__iter__所返回对象中的__next__方法
# 3.该返回对象中必须拥有__iter__和__next__方法,而该返回对象如果含有这些方法,则该对象称为迭代器
# 4.而for循环每访问一次数据,都会调用迭代器中的__next__方法
# 5.当数据全部访问完成时,主动抛出异常,结束代码
"""
# 一个对象是迭代器,则该对象一定有__iter__方法,而一个对象有__iter__方法,则不一定是迭代器
# 代码改良版:
# from collections import Iterable
# from collections import Iterator
import collections
import time
class Classmate(object):
def __init__(self):
self.names = list()
self.num = 0
def add(self, name):
self.names.append(name)
def __iter__(self):
# 必须返回一个对象的引用,这个对象且必须有__iter__和__next__方法
"""如果想要一个对象称为一个可以迭代的对象,即可以使用for,那么必须使用__iter__方法"""
return self # 可以返回自己,自己中含有__next__方法,自己就是一个迭代器,迭代时调用自己的__next__方法
def __next__(self):
if self.num < len(self.names):
ret = self.names[self.num]
self.num += 1
return ret
else:
raise StopIteration
classmate = Classmate()
classmate.add("老王")
classmate.add("张三")
classmate.add("王二")
for name in classmate:
print(name)
time.sleep(1)
5.2 生成器
'生成器是一类特殊的迭代器'
5.2.1 创建生成器
# 直接上代码:
def create_num(all_num):
a, b = 0, 1
current_num = 0
while current_num < all_num:
yield a # 如果一个函数中有yield语句,那么这个就不再是函数,而是一个生成器的模板
a, b = b, a + b
current_num += 1
# 如果在调用create_num的时候,发现这个函数中有yield,那么此时,不是调用函数,而是创建一个生成器的对象
obj = create_num(10)
# for num in obj:
# print(num)
print(next(obj))
print(next(obj))
print(next(obj))
# 1.当程序执行时,调用函数时发现函数中有yield,则不会执行该函数,而是返回一个生成器对象
# 2.当循环迭代该生成器时开始执行函数,碰到yield返回调用位置(若yield后面带有返回值,则将该值返回),并且该函数暂停执行
# 3.继续循环迭代时,该次执行从暂停时的地方继续执行
总结:
# 1.使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
# 2.yield关键字有两点作用:
保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
# 3.可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
# 4.Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)
5.2.2 生成器send唤醒
# 当我们使用next(obj)时,第一次可以从函数开头执行,第二次可以从yield处继续向下执行 ---> obj为生成器对象
# 如果我们想要传递参数,怎么办?
def create_num(all_num):
a, b = 0, 1
current_num = 0
while current_num < all_num:
ret = yield a # 如果一个函数中有yield语句,那么这个就不再是函数,而是一个生成器的模板
print(ret)
a, b = b, a + b
current_num += 1
# return "ok...."
# 如果在调用create_num的时候,发现这个函数中有yield,那么此时,不是调用函数,而是创建一个生成器的对象
obj = create_num(10)
print('obj',obj)
# while True:
# try:
# ret=next(obj)#生成器可直接调用next方法
# print(ret)
# except Exception as ret:
# print(ret.value)
# break
obj.send(None) # send一般不会放到第一次启动生成器,如果非要这么做,那么传递None
ret = next(obj)
print(ret)
# send里面的数据会传递给第5行,当作yield a的结果,然后ret保存结果
# send的结果是下一次调用yield时,yield后面的值
ret = obj.send("hahahha")
print(ret)
5.3 协程
协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
5.3.1 协程 - yield
import time
def task1():
while True:
print("----1----")
time.sleep(0.1)
yield
def task2():
while True:
print("----2----")
time.sleep(0.1)
yield
def main():
t1 = task1()
t2 = task2()
while True:
next(t1)
next(t2)
if __name__ == "__main__":
main()
# 使用yield实现交替执行任务,完成多任务
5.3.2 协程-greenlet
from greenlet import greenlet
import time
def test1():
while True:
print "---A--"
# 切换到gr2中执行
gr2.switch()
time.sleep(0.5)
def test2():
while True:
print "---B--"
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
#切换到gr1中运行
gr1.switch()
# 基本不用,当作了解即可
5.3.3 协程-gevent
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
import gevent
def f1(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(0.5)
def f2(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(0.5)
def f3(n):
for i in range(n):
print(gevent.getcurrent(), i) # 打印当前执行的协程
gevent.sleep(0.5) # sleep 0.5秒
print("1")
g1 = gevent.spawn(f1, 5)
print("2")
g2 = gevent.spawn(f2, 5)
print("3")
g3 = gevent.spawn(f3, 5)
g1.join()
g2.join()
g3.join()
5.3.4 打补丁
# 使用协程完成多任务时,寻常的time.sleep()不管用,无法完成多任务,需要用到gevent中的模块 gevent.sleep()
# 当如果我们使用协程完成多任务实现socket间的通信时,也需要换成相应的gevent模块
# 我们可以使用打补丁的发送,不需要我们更改模块,而是程序运行时内置程序自己去替换需要替换的模块
from gevent import monkey
import gevent
import random
import time
monkey.patch_all()
def coroutine_work(coroutine_name):
for i in range(10):
print(coroutine_name, i)
time.sleep(random.random())
gevent.joinall([
gevent.spawn(coroutine_work, "work1"),
gevent.spawn(coroutine_work, "work2")
])
5.4 图片下载器
import urllib.request
import gevent
from gevent import monkey
monkey.patch_all()
def downloader(img_name, img_url):
rep = urllib.request.urlopen(img_url)
print(rep)
img = rep.read()
with open(img_name, "wb") as f:
f.write(img)
def main():
gevent.joinall([gevent.spawn(downloader, "1.jpg",
"https://rpic.douyucdn.cn/live-cover/roomCover/2020/08/10/22640da60349d70e35202a1730938ed0_big.jpg"),
gevent.spawn(downloader, "2.jpg",
"https://anchorpost.msstatic.com/cdnimage/anchorpost/1051/02/0fb8f99042076281dd9df8c13b513b_2168_1607525405.jpg")])
if __name__ == "__main__":
main()
5.5 进程、线程、协程对比
有一个老板想要开个工厂进行生产某件商品(例如剪子)
他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程
这个老板为了提高生产率,想到3种办法:
1.在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式
2.老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式
3.老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式
5.6 简单总结
1.进程是资源分配的单位
2.线程是操作系统调度的单位
3.进程切换需要的资源很最大,效率很低
4.线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
5.协程切换任务资源很小,效率高
6.多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发