网络编程

网络编程

引子

假如有两个脚本,foo.py ,bar.py,分别运行,都可以正常运行。若现在想从两个程序间传递一个数据。

-同一台电脑

​ 创建一个文件,将foo.py的数据读入文件中,bar.py从中读取数据

-不同电脑

​ 该怎么做

软件的开发架构

C/S架构

c/s即Client和Server—>客户端和服务器端架构

B/S架构

即Browser和Server—>浏览器端和服务器端架构

什么是网络

网络就是一种辅助双方或多方能够连接在一起的工具

伴随着网络发展,人们使用了很多通信方法,有些已不再使用,现在使用最广泛的是

TCP/IP(Transmission Control Protocal/Internet Protocal)

TCP/IP事实上是一些协议(protocols)的合集。当前大多数使用中的通信都使用TCP协议。

Internet是在一些共享的线路上发送数据’的。例如:在您的计算机上也许同时运行着几个应用程序,如Web浏览器、通讯软件等程序,而您只须通过一条单一的线路来连接互联网。上面所有的程序都共享这个连接,简单地说,用户往往不会觉察到这个共享的发生。

TCPMP是标准的协议,其可以使世界范围的计算机通过internet或本地网络通信

目的

​ -使用网络把多方连接在一起然后进行数据传输

​ -为了让不同电脑的软件可以相互传递数据,借助网络的功能。

网络编程的定义

让不同电脑中软件能够进行数据传递,即网络中不同主机进程间的通信

IP地址分类

地址

1. 生活中的地址与标识

不同的网络中,采用唯一的标识来区分不同的主体,比如车牌号、建筑物地址、电话号码、员工编号等等

一台电脑怎么找到很多电脑中的一台呢?

警察怎么找到嫌疑犯的?

2. ip地址的作用

ip地址:用来在网络中标记一台电脑,比如192.168.1.1;在同一网络中是唯一的。

同一网络:好比班级编号,球队编号。

同一网络:例如同一个局域网, 一个教室里。

目前ip主要分为两种

​ -ipv4,32位二进制构成。分成四段,每段范围0-255(2的8次方,四个字节)

​ -ipv6,128位二进制构成

每个IP包含两部分:

​ -网络号

​ -主机号

类似电话号码由区号+电话主机号组成

以太网适配器 以太网:

连接特定的 DNS 后缀 . . . . . . . :
IPv6 地址 . . . . . . . . . . . . : 2001:250:6405:6005:1033:c65c:82c1:466b
临时 IPv6 地址. . . . . . . . . . : 2001:250:6405:6005:6c01:d71:e55f:3aac
本地链接 IPv6 地址. . . . . . . . : fe80::1033:c65c:82c1:466b%2
IPv4 地址 . . . . . . . . . . . . : 192.168.1.131
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : fe80::22a:6aff:fefa:ff3c%2

​ 192.168.1.1

开放式系统互联参考模型

(Open System Interconnection Reference Model)—>简称OSI

这个标准定义了网络的七层框架,试图使得计算机在整个世界范围内实现互联。
在OSI中,网络体系结构被分成下面的七层。

  • 物理层
    • 定义了通信设备的传输规范,规定了激活、维持和关闭通信节点之间的机械特性、电气特性和功能特性等。此层为上层协议提供了一个传输数据的物理媒介。
  • 数据链路层
    • 定义了数据封装以及传送的方式。这个层次的数据单位称为“帧”。数据链路层包括两个重要的子层:逻辑链路控制层(Logic Link Control,LLC)和介质访问控制层(Media Access Control,MAC)。LLC用来对节点间的通信链路进行初始化,并防止链路中断,确保系统的可靠通信。而MAC则用来检测包含在数据帧中的地址信息。这里的地址是链路地址或物理地址,是在设备制造的时候设置的。网络上的两种设备不能有相同的物理地址,否则会造成网络信息传送失败。
  • 网络层
    • 定义了数据的寻址和路由方式。这一层负责对子网间的数据选择路由,并实现网络互连等功能。
  • 传输层
    • 为数据提供端到端传输。这是比网络层更高的层次,是主机到主机的层次。传输层将对上层的数据进行分段并进行端到端传输。另外,还提供差错控制和流量控制机制。
  • 会话层
    • 用来为通信的双方制定通信方式,包括建立和拆除会话。另外,此层将会在数据中插入校验点来实现数据同步。
  • 表示层
    • 为不同的用户提供数据和信息的转换。同时还提供解压缩和加解密服务。这一层保证了两个主机的信息可以互相理解。
  • 应用层
    • 控制着用户绝大多数对于网络应用程序的访问,提供了访问网络服务的接口。

端口

整个网络通信通过IP地址+端口来标识不同的网络服务

端口号是用来表示区别网络中不同的应用,操作系统会对端口进行编号,即端口号

​ -端口号使用16位,也就是2个字节的数字来标识,范围是02^16-1(065535)

端口的分配是基于一定规则的,而不是随意分配的

知名端口

​ -80,分配给http服务的

​ -21,分配给FTP服务的

动态端口(Dynamic Ports)

一般不固定分配某种服务,动态分配。范围:1024-65535

所谓的动态分配,是指一个程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供其使用。关闭程序时,同时释放占用的端口

Socket简介

不同电脑上进程的标识与识别

用唯一标识来标记一个进程

在电脑上,可以通过用进程号(PID)来唯一标识进程。但是在网络上,不可以,需要利用TCP/IP协议族来帮助我们解决问题。

用IP可以唯一标识网络中的主机,协议+端口号唯一标识主机中的应用程序

进程指的是,运行的程序以及运行程序用到的资源的整体就称之为进程

什么是Socket

socket(套接字)是进程间通信的一种方式

与其他进程通信的一个主要不同是:

它能不同主机间的进程间的通信

现行大多数服务都是基于Socket来完成通信的。比如:浏览网页,QQ聊天,收发email

创建一个socket

import socket

socket.socket(AddressFamily,Type)

参数说明:

​ *AddressFamily

​ -AF_INET internet间进程间通信,实际工作中最常用。

​ -AF_UNIX 同一台机器进程间通信

​ *Type 套接字类型

​ -SOCK_DGRAM 数据报套接字,主要用于UDP协议

​ -SOCK_STREAM 流式套接字,主要用于TCP协议

创建一个TCP Socket

#先运行server.py
import socket

# 创建一个socket对象
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 主机名
host = socket.gethostname()
# 端口号
port = 9995

#绑定端口
serversocket.bind((host, port))
# 设置最大连接数,超过后排队
serversocket.listen(5)

while  True:
   # 简历客户端连接
   clientsocket, addr = serversocket.accept()
   print("连接地址:\t %s" % (str(addr)))
   msg = "网络编程测试。" + "\r\n"
   clientsocket.send(msg.encode("utf-8"))
   clientsocket.close()

#再运行client.py
import socket

# 创建一个socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# print(dir(s))
# 获取本地主机名
host = socket.gethostname()
# 设置端口号
port = 9995

# 连接服务,指定主机和端口
s.connect((host, port))

# 接收小于1024字节的数据
msg = s.recv(1024)
s.close()
print(msg.decode('utf-8'))

输出:连接地址:	 ('192.168.1.131', 51515)

UDP网络程序

UDP—>User Datagram Protocol(用户数据报协议)是一个无连接的简单的面向数据报的运输层协议。

优缺点

​ 优点:传输速度快(udp在传输时无需在客户端和服务端之间建立连接,也无超时重新发送机制)

​ 缺点:不能保证可靠性(udp是一种面向无连接的协议每个数据都是一个独立的信息,包含完整的源地址或目的地址,在网络上以任何可能的路径传往目的地。因此,能够到达目的地,以及到达目的地的时间和内容的正确性无法保证)

特点:

UDP是面向无连接的通讯协议;

UDP包括目的端口号和源端口号信息,通讯不需要连接,能够实现广播发送。

UDP数据大小有限制,每个被传输的数据报必须限定在64K以内。

UDP是一个不可靠的协议,发送出去的数据报不一定以相同次序到达接收方。

UDP一般多用于多点通信和实时的数据业务。比如:视屏、QQ、语音广播

发送数据

创建一个UDP客户端程序的流程:

1.创建一个客户端套接字

2.发送/接收数据

3.关闭套接字

socket和file的区别

​ -file针对指定模块进行“打开”,“读写”,“关闭”

​ -socket针对服务端和客户端socket进行“打开”,“读写”,“关闭”

import socket

udp_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

addr=('192.168.1.122',8234)

sendData=input('输入要发送的数据:')

udp_socket.sendto(sendData.encode('gbk'),addr)

udp_socket.close()

发送,数据

Socket 对象(内建)方法

函数描述
服务器端套接字
s.bind()绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
s.listen()开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept()被动接受TCP客户端连接,(阻塞式)等待连接的到来
客户端套接字
s.connect()主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex()connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv()接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
s.send()发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
s.sendall()完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvform()接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto()发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close()关闭套接字
s.getpeername()返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname()返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value)设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen])返回套接字选项的值。
s.settimeout(timeout)设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout()返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno()返回套接字的文件描述符。
s.setblocking(flag)如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile()创建一个与该套接字相关连的文件
import socket
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

Addr = ('192.168.1.112',8345)

sendData = input('请输入发送数据:')

udp_socket.sendto(sendData.encode('utf-8'),Addr)

recvData = udp_socket.recvfrom(1024)

print(recvData)

udp_socket.close()

用网络调试助手时,端口号会一直变动。

UDP端口号绑定

import socket
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
local_addr=('',7788)       #ip地址和端口号,ip一般不用写,表示本地ip
udp_socket.bind(local_addr)

recv_data=udp_socket.recvfrom(1024)
print(recv_data[0].encode('gbk'))
udp_socket.close()

总结:

​ -一个udp网络程序,可以不绑定端口,此时系统会自动分配一个端口。重新运行此程序,端口号可能会发生变化。

​ -一个udp网络程序,可以绑定信息(IP,Ports)。如果绑定成功,那么操作系统用这个端口号来进行区别收到的网络数据是否是此进程的

TCP简介

TCP协议,传输控制协议(transmission control protocol),是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP通信需要经过创建连接,传输数据,终止连接三个步骤。

TCP和UDP的不同点:

-面向连接

-有序的数据传输

-无差错的数据传输(重发丢失的数据包,舍弃重复的数据包)

-阻塞/流量控制

-TCP通信模型,类似打电话,在通信开始前,一定要先建立相关的连接,才能发送数据;而udp通信模型,类似写信,不需要建立相关的连接,只需要发送数据即可

TCP特点

面向连接

通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。

双方间的数据传输都可以通过这个连接进行。

完成数据交换后,双方断开此连接,以释放系统资源。

这种连接是一对一的

因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议

可靠传输

1)TCP采用发送应答机制

TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功

2)超时重传

发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。

TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。

3)错误校验

TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

  1. 流量控制和阻塞管理

流量控制用来避免主机发送得过快而使接收方来不及完全收下。

在python中,用TCP进行socket编程也比较简单

​ -客户端

​ 要主动连接服务器的IP和服务器的指定端口

​ -服务器

​ 监听指定端口

​ 对于每个新的连接,创建一个线程或进程

通常,服务器程序可以无限运行下去。要注意的是,一个端口不能同时被两个socket绑定。

TCP服务端和客户端格子socket的创建和交互。

TCP服务器

在python程序中,如果要完成一个tcp服务器的功能,需要的流程如下:

​ 1.socket创建一个套接字

​ 2.bind()绑定IP和port(插卡)

​ 3.listen()使套接字由主动变为被动连接,即开启监听模式(设置一个响铃模式)

​ 4.accept()等待客户端连接。

​ 5.recv/send 接收/发送数据

​ 6.关闭和客户端交互的套接字

​ 7.关闭监听套接字

#先运行tcp_client.py
import socket

#创建套接字
tcp_client_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#目标服务器信息
server_ip = input("请输入服务器ip:")
server_port = int(input("请输入服务器port:"))

#连接服务器
tcp_client_socket.connect((server_ip,server_port))

#提示用户输入数据
send_data = input("请输入要发送的数据:")
tcp_client_socket.send(send_data.encode('utf-8'))

#接收服务器端发来的信息
recv_data = tcp_client_socket.recv(1024)
print("接收到的数据为",recv_data.decode("gbk"))

#关闭套接字
tcp_client_socket.close()

设置监听的目的:

socket默认为主动模式,listen()能将主动模式改为被动模式,被动了才能接收信息

listen(5),5是指可以同时接收到的客户端申请的最大数,超过则拒绝连接

创建新套接字的原因:

服务器接收到请求后,将生成一个新的套接字,专门给新来的客户端提供一对一服务。此时,服务器套接字的任务就是等待新的客户端套接字连接它

#再运行tcp_server.py
import socket

#创建套接字
tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#绑定ip
tcp_socket.bind(("",8081)) #port_num>1024的随便用,小于1024的一般制定了用途

#设置监听
tcp_socket.listen(5)

#创建套接字
new_socket,addr = tcp_socket.accept()

#收/发数据
content = new_socket.recv(1024)

print("接收到数据",content.decode("gbk"))

data = input("请输入服务器发送的数据:")

new_socket.send(data.encode('gbk'))

#关闭通信的socket
tcp_socket.close()

#关闭用于监听的套接字
new_socket.close()

TCP协议

当应用程序希望通过TCP与另一个应用程序通信时,他会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP将在两个应用程序之间建立一个全双工(full-duplex)的通信。这个全双工的通信将占用两个计算机之间的通信线路,直到他被一方或双方关闭为止。

标志位,TCP的6个标志位https://www.cnblogs.com/-zyj/p/5730445.html

所谓标志位,是一种二进制的指令

​ -SYN:发起一个新连接

​ -FIN:释放一个新连接

​ -ACK:确认

TCP三次握手

TCP是因特网的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方的回答SYN+ACK,并最终对对方的SYN执行ACK确认。这种建立连接的方法可以防止产生错误的连接。

TCP三次握手过程

-客户端发送SYN(seq=x)报文交给服务器端,进入SYN_SEND状态;

-服务器端收到SYN报文,会因一个SYN(seq=y)+ACK(x+1)报文,进入SYN_RECV状态;

-客户端收到服务器端的SYN报文,回应一个ACK(y+1)报文,进入Established状态。

三次握手完成,TCP客户端和服务器端成功建立连接,可以开始传输数据。

TCP四次挥手

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这个是由TCP的半关闭状态(half-close)造成的

​ 1.某个应用进程首先调用close。称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN字节,表示数据发送完毕。

​ 2.接收到这个FIN的对端执行“被动关闭”(passive close),这个FIN由确认。

注意:

FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相关的连接上再无额外数据可接受

​ 3.一段时间后,接收到这个文件结束符的应用进程将会调用close关闭它的套接字。这导致它的TCP也发送一个FIN

​ 4.接收这个最终端FIN的原发送端TCP(即执行主动关闭的那一段)确认这个FIN。(既然每端都需要一个FIN和一个ACK,因此,通常需要4个字节)

-广播

-Mac地址

-arp协议

-路由器

-局域网

-子网掩码

多任务介绍

计算机中的多任务是指操作系统同时完成多项任务处理。(同时是指同一个时间段,而非某个瞬时时间点)

多任务处理是指,用户在同一时间段内运行多个应用程序,每个应用程序就可以称为一个任务。

现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?

答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

并发与并行
  • 并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行
  • 并行处理(Parallel Processing):是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集

进程与线程

进程

进程:一个程序的执行实例。每个进程提供执行程序所需的所有资源。

进程本质上是资源的集合。

一个进程有虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要至少有一个线程

在Unix/Linux中,提供了fork()系统函数。

-普通的函数调用,调用一次,返回一次

-fork()函数调用一次,返回两次。

​ 因为操作系统自动把当前的进程(父进程)复制一份(子进程),然后在父进程和子进程内返回。

fork()子进程永远返回0,而父进程返回子进程的ID。

一个父进程可以fork出很多子进程。父进程可以记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID

-getpid()返回当前进程标识

-getppid()返回父进程标识

import os

pid=os.fork()
if pid <0:
    print('fork调用失败')
elif pid ==0:
    print('我是子进程:\t%s,我的父进程是:\t%s' % (os.getpid(),os.getppid()))
else:
    print('我是子进程:\t%s,我的父进程是:\t%s' %(os.getpid(),pid))
print('父子进程都可以执行这里')

线程

线程是操作系统能够运算调用的最小单位。线程被包含在进程之中,是进程中的实际运作单位。

一条线程是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程并行执行的不同的任务。

一条线程是一个execution context(执行上下文),即一个CUP执行时所需要的一串指令。

线程的工作方式

假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。

线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。

进程与线程的区别

​ 同一进程中的线程共享同一内存空间,但是进程之间是独立的。

​ -同一个进程中的所有线程的数据共享,进程之间的数据是独立的

​ -对主线程的修改可能影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程

​ -线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。

​ -同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。

​ -创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。

​ -一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。

​ -线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。

python中多线程的实现

1.创建一个函数传入Thread对象中

import time
import threading


def download_music():
    for i in range(5):
        time.sleep(1)
        print('---正在下载歌曲%d---' % i)


def play_music():
    for i in range(5):
        time.sleep(1)
        print('---正在播放歌曲%d---' % i)


def main():
    # 创建两个线程对象,target指向新开启的线程要执行的函数
    t1 = threading.Thread(target=download_music)
    t2 = threading.Thread(target=play_music)

    t1.start()
    t2.start()


if __name__ == '__main__':
    main()
    
out:
---正在下载歌曲0---
---正在播放歌曲0---
---正在下载歌曲1---
---正在播放歌曲1---
---正在下载歌曲2---
---正在播放歌曲2---
---正在下载歌曲3---
---正在播放歌曲3---
---正在下载歌曲4---
---正在播放歌曲4---

1.可以明显看出使用多线程并发的操作,花费时间要短很多

2.当我们调用start()时,才会真正的执行线程,执行线程中的代码

-继承Thread类,创建一个新的class,将要执行的代码写到run函数中

import threading
import time

#自定义类,threading.Thread

class MyThread(threading.Thread):

    def run(self):
        for i in range(5):
            time.sleep(1)
            msg='I am '+self.name+'@'+str(i)   #name保存的是当前线程的名字
            print(msg)

if __name__=='__main__':
    t1=MyThread()
    t2=MyThread()
    t1.start()
    t2.start()
    
out:
I am Thread-1@0
I am Thread-2@0
I am Thread-1@1
I am Thread-2@1
I am Thread-1@2
I am Thread-2@2
I am Thread-1@3
I am Thread-2@3
I am Thread-1@4
I am Thread-2@4

python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread的start()方法,可以启动该线程。当该线程获得执行的机会时,就会调用run()方法执行

注:

threading.currentThread(): 返回当前的线程变量。 
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

线程何时开启,何时结束

​ -子线程何时开始,何时运行

​ 当调用thread.start()时,开启线程,再运行线程代码。

​ -子线程何时结束

​ 子线程把target指向的函数中的语句执行完毕后,或者线程中run函数代码执行完毕后,立即结束

​ 当前子进程。

​ -查看当前的线程数量

​ 通过threading.enumerate()可枚举当前运行的所有进程

​ -主线程何时结束

​ 所有子线程执行完毕后,主线程才结束

import threading
import time

def test1():
    for i in range(5):
        time.sleep(1)
        print('---子线程1---%d'%i)
        print('子线程1中查看线程情况',threading.enumerate())

def test2():
    for i in range(10):
        time.sleep(1)
        print('---子进程2---%d'%i)
        print('子线程2中查看线程情况',threading.enumerate())

def main():
    # threading.enumerate()  枚举当前的所有进程
    print('创建线程之前的线程情况',threading.enumerate())

    #创建线程对象
    t1=threading.Thread(target=test1)
    t2= threading.Thread(target=test2)

    time.sleep(1)
    print('创建线程之后的线程情况',threading.enumerate())

    t1.start()
    t2.start()

    time.sleep(1)
    print('调用了thread.start()之后的线程情况',threading.enumerate())
    t2.join()  #当t2线程执行完后,再执行后续的代码
    print('查看当前线程',threading.enumerate())

if __name__ == '__main__':
    main()

多线程-共享全局变量问题

多线程可能遇到的问题

假设有两个线程t1和t1,都要对全局变量g_num进行运算(+1运算),两个线程t1和t2分别对g_num各加10次,g_num的最终结果?

import threading
import time

g_num = 0
def work1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print('in work1:',g_num)


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print('in work2:', g_num)

print('---线程创建之前:%d' % g_num)

t1 = threading.Thread(target=work1,args=(10,))
t2 = threading.Thread(target=work2,args= (10,))
t1.start()
t2.start()

while len(threading.enumerate())!=1:
    time.sleep(1)
print('2个线程对全局变量操作之后的结果:%d' %g_num)
out:
---线程创建之前:0
in work1: 10
in work2: 20
2个线程对全局变量操作之后的结果:20

在num=0时,t1取得num=0,此时系统把t1调度为“sleeping”的状态,t2转换为“running”状态,t2也获得num=0,t2对得到的值进行加1并赋给num,num=1。然后,系统又将t2调度为“sleeping”状态,把t1转换为“running”。线程t1又把它之前得到的0加1后赋值给num。这种情况,明明两个线程都完成了一次+1工作,但结果还是num=1

如果我们将两个线程的参数调整为1000000,每次运行,结果都不同。说明如果多个线程同时对一个变量全局变量进行操作,会出现资源竞争问题,从而使数据结果不准确,造成线程安全问题

同步

同步,就是协同步调。按照预定的先后次序进行运行。好比交流,一个说完,另外一个再说。

进程和线程的同步,可以理解为进程或者线程A和B一块配合,A执行一定程度时需要依赖B的某个结果,于是停下来,让B运行,B开始运行,再将结果给A,A再继续操作。如此往复,直到程序结束

计算错误的解决

通过线程同步进行解决

思路:

​ -系统调度t1,获取num=0,此时上一把锁,即不允许其他线程操作num

​ -对num的值加1

​ -解锁,此时num的值为1,其他的线程就可以使用num了,此时num=1

​ -同理,其他线程在对num修改时,也要先上锁,处理完后再解锁。在上锁过程中,不允许其他线程访问,就保证了数据的正确性

互斥锁

当多个线程几乎同时修改某个共享数据时,需要进行同步控制

线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制就是引入互斥锁。

互斥锁为我们的资源引入一个状态:锁定/非锁定

某个线程要更改共享数据时,先将其锁定,此时,资源的状态为锁定,其他线程不能对其更改;

直到该线程释放资源,资源状态变为非锁定状态,其他线程才能再次锁定该资源。

互斥锁,保证了每次只有一个线程进行写入操作,从而保证了多线程情况下的数据的正确性

在threading模块中,定义了lock类, 可以方便的处理锁定。

mutex =threading.Lock()     #创建锁
mutex.acquire([blocking])   #锁定  bool类型
mutex.release()             #释放

参数说明:

​ -blocking

​ True,则当前线程堵塞,直到获取这个锁为止(若未指定,则默认为True)

​ False,则线程不会被堵塞

import threading
import time
import  threading
g_num = 0
def work1(num):
    global g_num
    for i in range(num):
        if mutex.acquire(True):
            g_num += 1
            mutex.release()
    print('in work1:',g_num)


def work2(num):
    global g_num
    for i in range(num):
        if mutex.acquire(True):
            g_num += 1
            mutex.release()
    print('in work2:', g_num)

print('---线程创建之前:%d' % g_num)

mutex=threading.Lock()
t1 = threading.Thread(target=work1,args=(100000,))
t2 = threading.Thread(target=work2,args= (100000,))
t1.start()
t2.start()

while len(threading.enumerate())!=1:
    time.sleep(1)
print('2个线程对全局变量操作之后的结果:%d' %g_num)


死锁(了解)

在线程间共享多个资源时,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁。

死锁一般很少发生,但一旦发生就会造成应用停止响应

import threading
import time

printer_mutex = threading.Lock() # 打印机锁

paper_mutext = threading.Lock() # 纸张锁


class ResumeThread(threading.Thread):
    """编写个人简历任务的线程"""

    def run(self):
        print("ResumeThread:编写个人简历任务")
        # 使用打印机资源,先对打印机加锁
        printer_mutex.acquire()
        print("--ResumeThread:正在使用打印机资源--")
        time.sleep(1)  # 休眠1秒

        # 使用纸张耗材,先对纸张耗材加锁
        paper_mutext.acquire()
        print("--正在使用纸张资源--")
        time.sleep(1)
        paper_mutext.release()  # 释放纸张锁

        # 释放打印机锁
        printer_mutex.release()


class PaperListThread(threading.Thread):
    """盘点纸张耗材任务的线程"""

    def run(self):
        print("PaperListThread:盘点纸张耗材任务")
        # 使用纸张耗材,先对纸张耗材加锁
        paper_mutext.acquire()
        print("--PaperListThread:正在盘点纸张耗材--")
        time.sleep(1)  # 休眠1秒

        # 使用打印机资源,打印清单
        printer_mutex.acquire()
        print("--正在使用打印机资源--")
        time.sleep(1)
        printer_mutex.release()  # 释放打印机锁

        # 释放纸张耗材锁
        paper_mutext.release()


if __name__ == '__main__':
    t1 = ResumeThread()
    t2 = PaperListThread()

    t1.start()
    t2.start()

同步的应用

让多个线程有序的执行

import threading
import time

class Task1(threading.Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print('-------Task1-------')
                time.sleep(0.5)
                lock2.release()

class Task2(threading.Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print('-------Task2-------')
                time.sleep(0.5)
                lock3.release()

class Task3(threading.Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print('-------Task3-------')
                time.sleep(0.5)
                lock1.release()


#使用Lock创建锁,默认没锁上
lock1 = threading.Lock()
#创建锁2,并且锁上
lock2 = threading.Lock()
lock2.acquire()
#创建锁3,并且锁上
lock3 = threading.Lock()
lock3.acquire()

t1 = Task1()
t2 = Task2()
t3 = Task3()

t1.start()

t2.start()

t3.start()

生产者消费问题也就是有限缓冲问题,是一个多线程同步的经典问题。描述了一个两个固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题

生产者的主要作用,生成一定量的数据放在缓冲区,然后,重复此过程

与此同时,消费者也在缓冲区消耗这些数据

整个问题的关进是,生产者不会再缓冲区满时加数据,消费者也不会在缓冲区空时消耗数据

解决办法
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形

1.队列,先进先出

2.栈,先进后出

python3中queue,在python2中Queue,模块提供了一个同步的、线程安全的队列类,包括先入先出(FIFO)

队列Queue,和后入先出(LIFO)队列LifoQueue和优先级队列PriorityQueue。

这些队列实现了锁原语(原子操作,要么不做,要么做完),可以在线程中直接使用。

可以使用队列来实现线程间的同步。

FIFO队列实现生产者消费者问题

import threading
import time
from queue import Queue

class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count +1
                    msg = '生成产品'+str(count)
                    queue.put(msg)
                    print(msg)
            time.sleep(0.5)

class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消费了 '+queue.get()
                    print(msg)
            time.sleep(1)


if __name__ == '__main__':
    queue = Queue()

    for i in range(500):
        queue.put('初始产品'+str(i))
    for i in range(2):
        p = Producer()
        p.start()
    for i in range(5):
        c = Consumer()
        c.start()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值