目录
1.网络通信的相关概念
2.Socket(简单版)
2.1 不同电脑上的进程之间如何通信
2.2 什么是scoket
2.3 创建socket
2.4 写代码
2.4.1 UDP发送信息
2.4.2 UDP接收信息
2.4.3 TCP客户端
2.4.4 TCP服务端
2.4.5 TCP的注意事项
3.文件下载
3.1 文本文件下载服务器
3.2 文本文件下载客户端
3.3 图片下载服务器
3.4 图片下载客户端
4.多线程
4.1 多任务
4.2 多线程版聊天
4.3 多线程共享全局变量
4.3.1 互斥锁
4.3.2 多线程解决买票问题
4.3.3 互斥锁的使用
4.4 线程间通信(生产者消费者问题)
4.4.1 Queue的原理
4.4.2 生产消费面包
5.多进程
5.1 创建进程
5.2 进程与线程的区别
5.3 进程间通信
5.3.1 共享全局变量
5.3.2 进程间通信
5.4 队列的使用
5.4.1 队列常用方法
5.5 进程池
5.6 join方法
简单来说,网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。
使用网络的目的,就是为了联通多方然后进行通信,即把数据从一方传递给另外一方。
前面的学习编写的程序都是单机的,即不能和其他电脑上的程序进行通信。为了让在不同的电脑上运行的软件,之间能够互相传递数据,就需要借助网络的功能。
- 使用网络能够把多方链接在一起,然后可以进行数据传递
- 所谓的网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信
IP地址:
生活中的地址指的就是,找到某人或某机关或与其通信的指定地点。在网络编程中,如果一台主机想和另一台主机进行沟通和共享数据,首先要做的第一件事情就是要找到对方。在互联网通信中,我们使用IP地址来查询到各个主机。
首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在1台电脑上可以通过进程号(PID)来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址"可以唯一标识网络中的主机,而传输层的“协议+端口"可以唯一标识主机中的应用进程(进程)。这样利用ip地址,协议,端口就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
注意:
所谓进程指的是:运行的程序以及运行时用到的资源这个整体称之为进程
所谓进程间通信指的是:运行的程序之间的数据共享
socket(简称套接字)是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于Socket 来完成通信的例如我们每天浏览网页、QQ聊天、收发email 等等。
作用:
socket可以在不同的电脑间通信;还可以在同一个电脑的不同程序之间通信。
2.3.1 在Python中使用socket模块的函数socket就可以完成:
import socket
socket.socket(AddressFamily,Type)
说明:
函数socket.socket创建一个socket,该函数带有两个参数:
- AddressFamily:可以选择AF_INET(用于Internet进程间通信)或者AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET。
- Type:套接字类型,可以是SOCK_STREAM(流式套接字,主要用于TCP协议)或者soCK_DGRAM(数据报套接字,主要用于UDP协议)。
2.3.2 创建一个tcp socket (tcp套接字) :
import socket
#创建tcp套接字
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#...这里是使用套接字的功能...
#不用的时候关闭套接字
s.close()
2.3.3 创建一个udp socket (udp套接字) :
import socket
#创建udp的套接字
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#...这里是使用套接字的功能...
#不用的时候关闭套接字
s.close()
说明:
套接字使用流程与文件的使用流程很类似:
1.创建套接字
2.使用套接字收/发数据3.关闭套接字
UDP是User Datagram Protocol的简称,中文名是用户数据报协议。在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中,"写信"。
#不同电脑之间的通信需要使用socket
import socket
#1.创建socket,并连接
#AF_INET:表示这个socket用于网络连接,SOCK_DGRAM:表示UDP连接
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#2.发送数据
#data:要发送的数据,是一个二进制的数据
#address:发送给谁,参数是一个元组,元组里有两个元素(目标IP地址,程序端口号)——>套接字
#("127.0.0.1",8080) 端口号0-1024不要用,找一个空闲的端口号
#'gbk'是因为我的'网络调试助手'安装在windows上
s.sendto('未来的梦,你好'.encode('gbk'),("127.0.0.1",8080))
#3.关闭socket
s.close()
import socket
#创建一个基于UDP的网络socket
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#绑定端口号和IP地址
s.bind(("127.0.0.1",8080))
#recvfrom 接收数据
data,addr=s.recvfrom(1024)#content=s.recvfrom(1024)
print('从{}地址{}端口号接收到了信息,内容是:{}'.format(addr[0],addr[1],data.decode('gbk')))
#print(content)#接收到的是一个元组,(接收到的数据,(发送方的IP地址,端口号))
s.close()
服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。
客户端(Client)也被称为用户端,是指与服务器相对应,为客户提供本地服务的程序。
客户端服务器架构又被称为主从式架构,简称C/S结构,是一种网络架构,它把客户端与服务器分开来,一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。
#客户端的发送信息代码
import socket
#基于TCP的socket连接
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#在发送数据之前必须要先和服务器建立连接
s.connect(('127.0.0.1',8080))#服务器('127.0.0.1',8080)
s.send('no'.encode('gbk'))
s.close()
在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:
1. socket创建一个套接字
2. bind绑定ip和port
3. listen使套接字变为可以被动链接4. accept等待客户端的链接
5. recv/send接收发送数据
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(128)#把socket变成一个被动监听器的socket
#接收到的结果是一个元组,(客户端的socket连接,客户端的ip和端口号,
client_socket,client_addr=s.accept()#接收客户端的请求
data=client_socket.recv(1024)#tcp里面用recv获取数据,而udp里面是recvfrom获取,1024指的是每次读取数据的大小
print('接收到{}客户端{}端口号发送的数据,内容是:{}'.format(client_addr[0],client_addr[1],data.decode('gbk')))
s.close()
运行结果:
1.tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器。
2.tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机。
3.tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做白4.当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信。
5.当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务。
6. listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的。
7.关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
8.关闭accept返回的套接字意味着这个客户端已经服务完毕。
9.当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线。
import socket,os
#文件下载是基于TCP协议的
server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_socket.bind(('127.0.0.1',8080))
server_socket.listen(128)#超过128的会连接不上
client_socket,client_addr=server_socket.accept()
data=client_socket.recv(1024).decode('gbk')
#print('接收到了来自{}地址{}端口号的信息,内容是:{}'.format(client_addr[0],client_addr[1],data))
path='./'+data
if os.path.isfile(path):
#print('读取文件,返回给客户端!')
with open(path,'r',encoding='utf8') as file:
content=file.read()
client_socket.send(content.encode('gbk'))
else:
print('文件不存在!')
server_socket.close()
import socket
client_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1',8080))
data=input('请输入您要下载的文件名:')
client_socket.send(data.encode('gbk'))
content=client_socket.recv(1024).decode('gbk')
file_name=input('请输入您将要存取的文件名:')
file_name='./'+file_name
with open(file_name,'w',encoding='utf8') as file:
file.write(content)
client_socket.close()
import socket,os
#文件下载是基于TCP协议的
server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_socket.bind(('127.0.0.1',8080))
server_socket.listen(128)#超过128的会连接不上
client_socket,client_addr=server_socket.accept()
data=client_socket.recv(1024).decode('gbk')
#print('接收到了来自{}地址{}端口号的信息,内容是:{}'.format(client_addr[0],client_addr[1],data))
path='./'+data
if os.path.isfile(path):
#print('读取文件,返回给客户端!')
with open(path,'rb') as file:
content=file.read()
client_socket.send(content)
else:
print('文件不存在!')
server_socket.close()
import socket
client_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1',8080))
data=input('请输入您要下载的文件名:')
client_socket.send(data.encode('gbk'))
file_name=input('请输入您将要存取的文件名:')
file_name = './' + file_name
with open(file_name, 'ab') as file:
while True:
content = client_socket.recv(1024)
if content is None:
break
file.write(content)
client_socket.close()
在现实生活中,有很多的场景中的事情是同时进行的,比如跳舞和唱歌是同时进行的。
Python中执行多任务:多线程,多进程,多线程+多进程
import threading
from time import sleep
def sing():
for i in range(5):
print('正在唱歌...%d'%i)
sleep(0.2)
def dance():
for i in range(5):
print('正在跳舞...%d'%i)
sleep(0.2)
#Python中执行多任务:多进程,多线程,多进程+多线程
#target需要的是一个函数,用来指定线程需要执行的任务
t1=threading.Thread(target=dance)#创建了线程1
t2=threading.Thread(target=sing)#创建了线程2
#启动线程
t1.start()
t2.start()
运行结果:
import socket,threading
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.bind(('192.168.1.223',10005))
def send_msg():
while True:
msg=input('请输入您要发送的信息(exit结束聊天):')
s.sendto(msg.encode('gbk'),('192.168.1.223',8080))
if msg=='exit':
break
def recv_msg():
while True:
data,addr=s.recvfrom(1024)
if data.decode('gbk')=='exit':
break
file=open('./消息记录.txt','a',encoding='utf8')
print('接收到了{}地址{}端口号的消息,内容是:{}'.format(addr[0],addr[1],data.decode('gbk')),file=file)
t1=threading.Thread(target=send_msg)
t2=threading.Thread(target=recv_msg)
t1.start()
t2.start()
运行结果:
同步:
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。同步就是协同步调,按预定的先后次序进行运行。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁:
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为"锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成"非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
#创建锁
mutex=threading.Lock()
#锁定
mutex.acquire()
#释放
mutex.release()
注意:
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞
- 如果在调用acquire对这个锁上锁之前它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止。
- 和文件操作一样,Lock也可以使用with语句快速的实现打开和关闭操作。
多个线程共享同一个全局变量,会出现安全问题。
import time,threading
ticket=10
def sell_ticket():
global ticket
while True:
if ticket > 0:
time.sleep(0.3)
ticket -= 1
#threading.current_thread().name 拿到线程的名字
print('{}卖出一张票,还剩{}张!\t'.format(threading.current_thread().name,ticket))
else:
print('票卖完了!')
break
#创建两个线程买票
t1=threading.Thread(target=sell_ticket,name='线程1')
t2=threading.Thread(target=sell_ticket,name='线程2')
t1.start()
t2.start()
运行结果:
加锁之后,速度会变慢,效率会变低。
import time,threading
ticket=10
lock=threading.Lock()
def sell_ticket():
global ticket
while True:
lock.acquire()#加同步锁
if ticket > 0:
time.sleep(0.3)#这里就是模拟现实中真正售出票前的一系列操作
ticket -= 1
lock.release()#不操作数据时就释放锁
#threading.current_thread().name 拿到线程的名字
print('{}卖出一张票,还剩{}张!\t'.format(threading.current_thread().name,ticket))
else:
lock.release()
print('票卖完了!')
break
#创建两个线程买票
t1=threading.Thread(target=sell_ticket,name='线程1')
t2=threading.Thread(target=sell_ticket,name='线程2')
t1.start()
t2.start()
运行结果:
上锁过程:
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked"状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为""blocked"状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked"状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)。
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行。
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
线程之间有时需要通信,操作系统提供了很多机制来实现进程间的通信,其中我们使用最多的是队列Queue。
Queue是一个先进先出(First In First Out)的队列,主进程中创建一个Queue对象,并作为参数传入子进程,两者之间通过put( )放入数据,通过get( )取出数据,执行了get( )函数之后队列中的数据会同时被删除,可以使用multiprocessing模块的Queue实现多进程之间的数据传递。
import threading,queue,time
def produce():
for i in range(3):
time.sleep(0.1)
print('{}生产了++++++++面包{}!\t\t'.format(threading.current_thread().name,i))
q.put('{}品牌{}'.format(threading.current_thread().name,i))
def consumer():
while True:
time.sleep(0.3)
#q.get()是一个阻塞的方法
print('{}买到了--------面包{}!\t\t'.format(threading.current_thread().name,q.get()))
q=queue.Queue() #创建一个q
#三条生产线
p_a=threading.Thread(target=produce,name='p_a')
p_b=threading.Thread(target=produce,name='p_b')
p_c=threading.Thread(target=produce,name='p_c')
#两条消费线
c_a=threading.Thread(target=consumer,name='c_a')
c_b=threading.Thread(target=consumer,name='c_b')
p_a.start()
p_b.start()
p_c.start()
c_a.start()
c_b.start()
运行结果:
程序:例如xxx.py这是程序,是一个静态的。
进程:一个程序运行起来后,“代码+用到的资源+PCB”称之为进程,它是操作系统分配资源的基本单元。
不仅可以通过线程完成多任务,进程也是可以的。工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态。
- 就绪态:运行的条件都已经满足,正在等在cpu执行。
- 执行态::cpu正在执行其功能。
- 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态。
multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。
示例:创建两个进程,唱歌跳舞。
import multiprocessing
import time,os
def dance(m):
for i in range(m):
time.sleep(0.3)
print('正在跳舞!pid={}'.format(os.getpid()))
def sing(n):
for i in range(n):
time.sleep(0.2)
print('正在唱歌!pid={}'.format(os.getpid()))
if __name__=='__main__':#windows必须加这个,否者报错
print('主进程的pid={}'.format(os.getpid()))
# 创建了两个进程
# target:需要的是一个函数, 可以传参,args需要的是一个元组
p1 = multiprocessing.Process(target=dance,args=(100,))
p2 = multiprocessing.Process(target=sing,args=(100,))
# 启动
p1.start()
p2.start()
运行结果:
任务管理器中的显示:
功能:
- 进程,能够完成多任务,比如在一台电脑上能够同时运行多个QQ。
- 线程,能够完成多任务,比如一个QQ中的多个聊天窗口。
定义的不同:
- 进程是系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈);但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
区别:
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率·线线程不能够独立执行,必须依存在进程中。
- 可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人。
优缺点:
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
5.3.1.1 进程不能共享全局变量
进程之间是持有全局变量,但是相互不影响。
import multiprocessing,time
import os
n=100
def test():
time.sleep(0.1)
global n
n+=1
print('{}里n的值为:{}'.format(os.getpid(),n))
def demo():
time.sleep(1.8)
global n
n+=1
print('{}里n的值为:{}'.format(os.getpid(),n))
if __name__=='__main__':
print('主进程的pid={}'.format(os.getpid()))
p1=multiprocessing.Process(target=test)
p2=multiprocessing.Process(target=demo)
p1.start()
p2.start()
运行结果:
5.3.1.2 线程共享全局变量
而线程不一样,线程的pid是一样的。
线程可以共享同一进程里的全局变量。
import threading,time
import os
n=100
def test():
time.sleep(0.5)
global n
n+=1
print('{}里n的值为:{}\t\t'.format(os.getpid(),n))
def demo():
time.sleep(0.6)
global n
n+=1
print('{}里n的值为:{}\t\t'.format(os.getpid(),n))
if __name__=='__main__':
print('主进程的pid={}'.format(os.getpid()))
t1=threading.Thread(target=test)
t2=threading.Thread(target=demo)
t1.start()
t2.start()
运行结果:
import os,multiprocessing
import time
#由于进程不能共享全局变量,所以应该使用传参的方式
def producer(q):
for i in range(10):
time.sleep(0.5)
print('pid={}生产了+++++++{}\t\t\t'.format(os.getpid(),i))
q.put('pid={} {}'.format(os.getpid(),i))
def consumer(q):
for i in range(10):
time.sleep(0.5)
print('pid={}消费了-------{}\t\t\t'.format(os.getpid(),q.get()))
if __name__=='__main__':
print('主进程的pid={}'.format(os.getpid()))
q = multiprocessing.Queue()#区分线程中的q=queue.Queue()
p1=multiprocessing.Process(target=producer,args=(q,))
c2=multiprocessing.Process(target=consumer,args=(q,))
p1.start()
c2.start()
运行结果:
进程所用的队列和线程用的队列差不多。
#q1=multiprocessing.Queue() 进程间通信
#q2=queue.Queue() 线程间通信
def __init__(self, maxsize: int = ..., *, ctx: Any = ...) -> None: ...、 def get(self, block: bool = ..., timeout: Optional[float] = ...) -> _T: ... def put(self, obj: _T, block: bool = ..., timeout: Optional[float] = ...) -> None: ... def qsize(self) -> int: ... def empty(self) -> bool: ... def full(self) -> bool: ... def put_nowait(self, item: _T) -> None: ... def get_nowait(self) -> _T: ... def close(self) -> None: ... def join_thread(self) -> None: ... def cancel_join_thread(self) -> None: ...
程序一:
import multiprocessing,queue
#q1=multiprocessing.Queue() 进程间通信
#q2=queue.Queue() 线程间通信
#创建队列时,可指定最大长度。默认为0,表示不限制长度;若是n,表示最长n
q=multiprocessing.Queue(5)
q.put('hello')
q.put('good')
q.put('yes')
q.put('ok')
q.put('hi')
q.put('how')#程序走到这里会一直阻塞,不会结束
运行状态:
程序二:
import multiprocessing,queue
#q1=multiprocessing.Queue() 进程间通信
#q2=queue.Queue() 线程间通信
#创建队列时,可指定最大长度。默认为0,表示不限制长度;若是n,表示最长n
q=multiprocessing.Queue(5)
q.put('hello')
q.put('good')
q.put('yes')
q.put('ok')
q.put('hi')
print(q.get())
print(q.get())
print(q.get())
q.put('how')
运行结果:
程序三:
import multiprocessing,queue
#创建队列时,可指定最大长度。默认为0,表示不限制长度;若是n,表示最长n
q=multiprocessing.Queue(5)
q.put('hello')
q.put('good')
# q.put('yes')
q.put('ok')
q.put('hi')
print('队列是否满了:',q.full())
print('首先出来的元素是:',q.get())#get()也是一个阻塞的方法,如果队列空,则一直等待
q.put('how')
#block=True 表示,如果队列满了,就等待
#timeout=1 单位是秒,超时等待多久以后,程序报错
q.put([1,2,3],block=True,timeout=1)
#q.get(block=True,timeout=1)
#q.put_nowait('ooooo') #等价于q.put('ooooo',blocks=False) 放不进去立马报错
#q.get_nowait() #相当于q.get(block=False) 拿不到立马报错
print('当前队列大小:',q.qsize())
运行结果:
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务。
import multiprocessing,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(msg,'执行完毕,耗时%0.2f'%(t_stop-t_start))
if __name__=='__main__':
#3个进程7个任务
po = multiprocessing.Pool(3) # 定义一个进程池,最大进程数为3个
for i in range(7):
# apply_async(要调用的目标,(传递给目标的参数元组,))
# 每次循环将会用空闲出来的子进程去调用目标
po.apply_async(worker, (i,))
print('----------start----------')
po.close() # 关闭进程池,关闭后po不再接受新的请求
po.join() # 等待po中所有子进程执行完成,必须放在close语句之后
print('----------end----------')
运行结果:
join方法是让主线程等待子线程
import threading,time
x=2
def play():
time.sleep(1)
global x
x=10
t=threading.Thread(target=play)
t.start()
print('x=',x)
运行结果:
import threading,time
x=2
def play():
time.sleep(1)
global x
x=10
t=threading.Thread(target=play)
t.start()
t.join()#让主线程等待子线程
print('x=',x)
运行结果: