网络编程
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 |
| |
| udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| |
| |
| udp_socket.sendto(bytes型数据, 对方的ip以及端口号) |
| |
| |
| udp_socket.close() |
发送数据
| import socket |
| def main(): |
| |
| 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(): |
| |
| udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| |
| local_daar = ("192.168.1.100", 8081) |
| udp_socket.bind(local_daar) |
| |
| while True: |
| recv_data = udp_socket.recvfrom(1024) |
| recv_msg = recv_data[0] |
| send_addr = recv_data[1] |
| |
| |
| print("%s:%s" % (str(send_addr), recv_msg.decode("gbk"))) |
| |
| 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 |
| |
| tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| |
| tcp_socket.connect((服务器ip,服务器port)) |
| |
| tcp_socket.send(bytes类型数据) |
| |
| tcp_socket.close |
tcp服务端
| |
| |
| |
| |
| |
| |
| from socket import * |
| |
| |
| tcp_server_socket = socket(AF_INET, SOCK_STREAM) |
| |
| |
| address = ('', 7788) |
| |
| |
| tcp_server_socket.bind(address) |
| |
| |
| tcp_server_socket.listen(128) |
| |
| |
| |
| |
| client_socket, clientAddr = tcp_server_socket.accept() |
| |
| |
| recv_data = client_socket.recv(1024) |
| print('接收到的数据为:', recv_data.decode('gbk')) |
| |
| |
| client_socket.send("thank you !".encode('gbk')) |
| |
| |
| client_socket.close() |
tcp客户端
| import socket |
| |
| def main(): |
| |
| tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| |
| |
| server_ip = input('请输入需要链接的服务器ip:') |
| server_port = int(input('请输入需要链接的服务器port:')) |
| tcp_socket.connect((server_ip, server_port)) |
| |
| tcp_socket.send(b'hahaha') |
| |
| tcp_socket.close() |
| |
| if __name__ == "__main__": |
| main() |
| |
tcp程序案例
循环为一个客户端服务
| import socket |
| |
| |
| def main(): |
| |
| tcp_server_scoket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| |
| |
| tcp_server_scoket.bind(("", 7890)) |
| |
| |
| tcp_server_scoket.listen(128) |
| while True: |
| print("等待一个新的客户端的到来") |
| |
| 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")) |
| |
| 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): |
| |
| |
| file_name = new_clinet_scoket.recv(1024).decode("utf-8") |
| print("客户端(%s)需要下载文件是:%s " % (str(client_addr), file_name)) |
| file_content = None |
| |
| try: |
| f = open(file_name, "rb") |
| file_content = f.read() |
| f.close() |
| except Exception as ret: |
| print("没有要下载的文件(%s)" % file_name) |
| |
| |
| if file_content: |
| |
| new_clinet_scoket.send(file_content) |
| |
| |
| def main(): |
| |
| tcp_server_scoket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| |
| |
| tcp_server_scoket.bind(("", 7890)) |
| |
| |
| tcp_server_scoket.listen(128) |
| while True: |
| |
| new_clinet_scoket, client_addr = tcp_server_scoket.accept() |
| |
| send_file_2_client(new_clinet_scoket, client_addr) |
| |
| 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.多任务-线程
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() |
| 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()' |
| |
| |
| |
| |
| |
| |
| """ |
| 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 线程执行代码的封装
| |
| |
| 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) |
| 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 总结
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,)) |
| 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() |
| |
| |
| |
| |
| 产生该结果的原因:程序最终是被CPU执行,执行时一句编程语言会被翻译成多句指令,并且这两个函数存在资源竞争(你也可以理解为时间片轮转,每个程序都执行一段时间,然后换另一个程序执行) |
| 该程序出的问题在程序执行 g_num+=1 |
| |
| 我们看一个函数: |
| import dis |
| g_num = 0 |
| def func(): |
| global g_num |
| g_num += 1 |
| print(dis.dis(func)) |
| |
| |
| |
| |
| |

3.6 同步的概念
| 同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。 |
| |
| "同"字从字面上容易理解为一起动作 |
| |
| 其实不是,"同"字应是指协同、协助、互相配合。 |
| |
| 如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。 |
3.6.1 线程同步解决资源竞争问题
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 |
| |
| |
| 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 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.acquire() |
| |
| |
| print(self.name+'----do1---up----') |
| time.sleep(1) |
| |
| |
| mutexB.acquire() |
| print(self.name+'----do1---down----') |
| mutexB.release() |
| |
| |
| mutexA.release() |
| |
| class MyThread2(threading.Thread): |
| def run(self): |
| |
| mutexB.acquire() |
| |
| |
| print(self.name+'----do2---up----') |
| time.sleep(1) |
| |
| |
| mutexA.acquire() |
| print(self.name+'----do2---down----') |
| mutexA.release() |
| |
| |
| 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): |
| |
| 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聊天器的整体控制""" |
| |
| udp_scoket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| |
| udp_scoket.bind(("192.168.1.100", 7890)) |
| |
| 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(): |
| break |
| if count > 3: |
| print('连接超时') |
| return |
| print('第%s个人正在尝试第%s次连接!'%(i,count)) |
| e.wait(0.5) |
| count+=1 |
| print('第%s个人连接成功'%i) |
| |
| def check_mysql(e): |
| print('\033[45m 数据库正在维护 \033[0m') |
| time.sleep(random.randint(1,2)) |
| e.set() |
| |
| if __name__ == '__main__': |
| e = Event() |
| t = Thread(target=check_mysql,args=(e,)) |
| t.start() |
| for i in range(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 = 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中的多个聊天窗口 |
| '-------------------------如下图解释⬇-------------------------' |

再举个栗子

4.4 进程之间的通信: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() |
| |
| waitting_analy_data.append(data) |
| if q.empty(): |
| break |
| print(waitting_analy_data) |
| |
| |
| def main(): |
| |
| 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())) |
| |
| 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) |
| for i in range(0, 10): |
| |
| |
| po.apply_async(worker, (i,)) |
| |
| print("....开始....") |
| po.close() |
| |
| po.join() |
| 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. |
| |
| 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() |
| po = Pool() |
| po.apply_async(writer, (q,)) |
| |
| time.sleep(1) |
| |
| 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(): |
| |
| old_floder_name = input("请输入要copy的文件夹的名字:") |
| |
| |
| try: |
| new_floder_name = old_floder_name + "[复件]" |
| os.mkdir(new_floder_name) |
| except: |
| pass |
| |
| file_names = os.listdir(old_floder_name) |
| print(file_names) |
| |
| |
| po = multiprocessing.Pool(5) |
| |
| |
| q = multiprocessing.Manager().Queue() |
| |
| for file_name in file_names: |
| po.apply_async(copy_file, args=(q, file_name, old_floder_name, new_floder_name)) |
| |
| |
| po.close() |
| |
| all_file_num = len(file_names) |
| copy_ok_num = 0 |
| while True: |
| file_name = q.get() |
| |
| 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)) |
| |
| |
| from collections import Iterator |
| print(isinstance(classmate_iterator,Iterator)) |
for...in...循环的本质
| for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。 |
| |
| |
| |
| import collections |
| import time |
| |
| |
| class Classmate(object): |
| def __init__(self): |
| self.names = list() |
| |
| def add(self, name): |
| self.names.append(name) |
| |
| def __iter__(self): |
| |
| """如果想要一个对象称为一个可以迭代的对象,即可以使用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("王二") |
| |
| |
| |
| |
| |
| for name in classmate: |
| print(name) |
| time.sleep(1) |
| |
| """ |
| # 1.当for循环时,解释器会查看被循环的对象是否是可迭代的(也就是查看该对象是否含有__iter__方法) |
| # 2.若对象是可迭代,for循环每访问一次数据,就会调用可迭代对象中__iter__所返回对象中的__next__方法 |
| # 3.该返回对象中必须拥有__iter__和__next__方法,而该返回对象如果含有这些方法,则该对象称为迭代器 |
| # 4.而for循环每访问一次数据,都会调用迭代器中的__next__方法 |
| # 5.当数据全部访问完成时,主动抛出异常,结束代码 |
| """ |
| |
| |
| |
| |
| |
| 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): |
| |
| """如果想要一个对象称为一个可以迭代的对象,即可以使用for,那么必须使用__iter__方法""" |
| |
| return self |
| |
| 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 |
| a, b = b, a + b |
| current_num += 1 |
| |
| |
| |
| obj = create_num(10) |
| |
| |
| |
| print(next(obj)) |
| print(next(obj)) |
| print(next(obj)) |
| |
| |
| |
| |
| 总结: |
| |
| |
| 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起 |
| 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用 |
| |
| |
5.2.2 生成器send唤醒
| def create_num(all_num): |
| a, b = 0, 1 |
| current_num = 0 |
| while current_num < all_num: |
| ret = yield a |
| print(ret) |
| a, b = b, a + b |
| current_num += 1 |
| |
| |
| |
| |
| obj = create_num(10) |
| print('obj',obj) |
| |
| |
| |
| |
| |
| |
| |
| |
| obj.send(None) |
| |
| ret = next(obj) |
| print(ret) |
| |
| |
| 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() |
| |
| |
5.3.2 协程-greenlet
| from greenlet import greenlet |
| import time |
| |
| def test1(): |
| while True: |
| print "---A--" |
| |
| 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.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) |
| |
| |
| 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 打补丁
| 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核数不一样可能是并行的,但是协程是在一个线程中 所以是并发 |
6.GIL
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)