Python进阶教程m10–多线程

原文链接:http://www.juzicode.com/archives/841

《Python进阶教程m9–网络通信–socket通信》中我们实现了一个socket服务端和客户端通信的例子,这个例子中服务端需要等待客户端发送消息后才能返回消息给客户端,在客户端没有发送消息时,服务端一直在data = connet.recv(1024) 上被阻塞住,直到等到客户端发来消息才能做下一步的动作。但是在实际的应用中,这种阻塞是不能容忍的,当然可以通过修改接收消息的方法,在某个时间内如果没有收到对方消息则退出继续做下一步的动作 。另外也可以使用多线程的方法将接收消息和发送消息在不同的线程中实现,从而能实现发送消息和接收消息两不误。在这个例子中单线程的方式只能实现对讲机式的“半双工”通信,而使用多线程方式就可以做到“全双工”通信。

1、编程模型

通过一个例子我们先来看下多线程的基本编程模型,在这个例子中主线程定义和开启了一个子线程,在子线程中打印自己的线程名称和循环次数,循环次数达到上限后退出子线程:

print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   
 
import time, threading

def func1():
    print('进入线程:  ', threading.current_thread().name)
    loopcnt = 0
    while loopcnt < 5:
        loopcnt = loopcnt + 1
        print('线程: %s, loopcnt=%d' % (threading.current_thread().name, loopcnt))
        time.sleep(0.3)
    print('退出线程:  ' , threading.current_thread().name)

if __name__ == '__main__':
    print('进入主线程: ', threading.current_thread().name)
    t1 = threading.Thread(target=func1, name='func1')
    t1.start()
    print('退出主线程: ' ,threading.current_thread().name)

==========结果:
进入主线程:  MainThread
进入线程:   func1
退出主线程:  MainThread
线程: func1, n=1
线程: func1, n=2
线程: func1, n=3
线程: func1, n=4
线程: func1, n=5
退出线程:   func1

从上面的例子可以看出多线程编程的基本模型:首先定义一个或多个任务函数,这个例子中定义了一个func1任务(函数);在主线程中用threading.Thread()定义了这个任务的实例t1;然后使用t1.start()启动func1任务。 注意在Thread(target=func1, name=’func1′)定义任务实例时,target入参是函数名称func1,而不是执行函数“func1()”的写法,是不带“()”符号的

接下来我们来看一个稍微复杂的例子,这里定义了2个任务:

print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   
 
import time, threading

def func1():
    print('进入线程:  ', threading.current_thread().name)
    loopcnt = 0
    while loopcnt < 5:
        loopcnt = loopcnt + 1
        print('线程: %s, loopcnt=%d' % (threading.current_thread().name, loopcnt))
        time.sleep(0.2)
    print('退出线程:  ' , threading.current_thread().name)

def func2():
    print('进入线程:  ', threading.current_thread().name)
    loopcnt = 0
    while loopcnt < 5:
        loopcnt = loopcnt + 1
        print('线程: %s, loopcnt=%d' % (threading.current_thread().name, loopcnt))
        time.sleep(0.3)
    print('退出线程:  ' , threading.current_thread().name)
    
if __name__ == '__main__':
    print('进入主线程: ', threading.current_thread().name)
    t1 = threading.Thread(target=func1, name='func1')
    t2 = threading.Thread(target=func2, name='func2')
    t1.start()    
    t2.start()
    #t1.join()
    print('退出主线程: ' ,threading.current_thread().name)
==========结果:

进入主线程:  MainThread
进入线程:   func1
线程: func1, loopcnt=1
进入线程:   func2
退出主线程:  MainThread
线程: func2, loopcnt=1      -------这里func2和func1开始交替运行
线程: func1, loopcnt=2
线程: func2, loopcnt=2
线程: func1, loopcnt=3
线程: func1, loopcnt=4
线程: func2, loopcnt=3
线程: func1, loopcnt=5
线程: func2, loopcnt=4
退出线程:   func1
线程: func2, loopcnt=5
退出线程:   func2

上面的例子中用到了2个线程,2个线程的任务函数中使用到不同时长的延时,模拟线程中周期性任务所需的不同运行时长,2个线程就会交替在命令行窗口打印出各自的信息。其中func1延时的时长短,对应的t1线程结束的更早。

2、threading、线程对象的属性方法

2.1、 threading 属性、方法

通过threading的属性和方法可以获取线程列表、活动线程数、线程ID等:

threading.enumerate() 枚举线程对象列表
threading.active_count() 获取活动线程对象数目 等同于 enumerate() 返回列表的长度
threading.current_thread() 返回当前线程对象
threading.get_ident()获取当前线程id获取当前线程的id,如果要获取其他线程的id号,需要通过线程实例的ident属性获取

2.2、线程对象的属性、方法

通过threading.Thread()创建的线程对象常用的属性、方法有:

is_alive()判断线程对象是否是活动的在使用start()启动线程前和线程结束后,调用该方法返回False
ident线程对象ID
start()开启线程线程创建后只是创建了对象,需要使用start()方法开启线程。
join()等待线程结束调用线程会被阻塞,直到被启动线程结束,如果被启动线程中使用无限循环时需要小心

print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   
 
import time, threading

def func1():
    thread_name = threading.current_thread().name
    print('进入线程:  ', thread_name)
    print(thread_name, 'get_ident():  ', threading.get_ident())
    time.sleep(1)
    print('退出线程:  ' , thread_name)

if __name__ == '__main__':
    print('进入主线程: ', threading.current_thread().name)
    t1 = threading.Thread(target=func1, name='func1-1')
    t2 = threading.Thread(target=func1, name='func1-2')
    
    print('before t1.start() t1.is_alive():',t1.is_alive())
    t1.start()
    print('after t1.start() t1.is_alive():',t1.is_alive())
    print('t1.ident:  ', t1.ident)
    
    print('active_count():  ', threading.active_count() )        
    t2.start()
    print('t2.ident:  ', t2.ident)
    print('active_count():  ', threading.active_count() )
    print('enumerate():  ', threading.enumerate())
    
    time.sleep(3)
    print('t1.is_alive():',t1.is_alive())
    print('退出主线程: ' ,threading.current_thread().name)
===========结果:
-----欢迎来到www.juzicode.com
-----公众号: 桔子code/juzicode

进入主线程:  MainThread
before t1.start() t1.is_alive(): False  #启动前的状态为False
进入线程:   func1-1
func1-1 get_ident():   708
after t1.start() t1.is_alive(): True    #启动后的状态为True
t1.ident:   708
active_count():   2
进入线程:   func1-2
t2.ident:   11796        
func1-2 get_ident():   11796            #在子线程中使用threading.get_ident()方法和在线程外用该线程对象的ident属性获取的线程id是一样的
active_count():   3                     #启动2个子线程后的活动线程数目为3,其中含包含了1个主线程
enumerate():   [<_MainThread(MainThread, started 18284)>, <Thread(func1-1, started 708)>, <Thread(func1-2, started 11796)>]
退出线程:   func1-1
退出线程:   func1-2
t1.is_alive(): False                 #结束后的状态为False
退出主线程:  MainThread

3、创建线程的其他参数

3.1 入参args

当任务函数带参数时,可以在创建线程对象的时候用args参数指定任务函数的参数列表,该参数列表是一个元组,比如下面的例子中,func函数带para1和para2入参,在创建任务对象时使用args=(para1 ,para2):

#任务函数定义
def func(para1,para2):
    pass

#创建线程对象
t1 = threading.Thread(target=func, name='func-1',args=(para1 ,para2))

需要注意的是如果任务函数只有一个参数也必须表示成tuple的形式:args=(para1, ) ,其中组成tuple中的逗号是不能少的 ,如果像args=(para1)这样使用就会抛TypeError异常,这一点和《Python进阶教程m7b–混合编程–C语言接口ctypes(1)》中用Python封装C函数定义入参类型argtypes是一样的,下面是一个错误示例:

import time, threading
def func_one_para(para1):
    print(para1)
if __name__ == '__main__':
    para1 =11
    t0 = threading.Thread(target=func_one_para, name='func-0',args=(para1))
    t0.start()

==========结果:
Exception in thread func-0:
Traceback (most recent call last):
  File "D:\Python\Python38\lib\threading.py", line 932, in _bootstrap_inner
    self.run()
  File "D:\Python\Python38\lib\threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
TypeError: func_one_para() argument after * must be an iterable, not int

3.2 入参kwargs

当任务函数带有关键字参数时,使用kwargs传入关键字参数列表:

#任务函数定义
def func(para1,para2,**kw):
    pass

#创建线程对象
t1 = threading.Thread(target=func, name='func-1',args=(para1 ,para2),kwargs={'age':10} )

下面是一个args和kwargs的简单例子:

print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   
 
import time, threading

def func_one_para(para1):
    print(para1)
    pass
    
def func(para1,para2,**kw):
    thread_name = threading.current_thread().name
    print('进入线程:  ', thread_name)
    print(thread_name, 'para1:  ', para1)
    print(thread_name, 'para2:  ', para2)
    print(thread_name, 'kw:  ', kw)
    print('退出线程:  ' , thread_name)

if __name__ == '__main__':
    print('进入主线程: ', threading.current_thread().name)
    para1 =1 
    t0 = threading.Thread(target=func_one_para, name='func-0',args=(para1,))

    para1 =11 
    para2 =12    
    t1 = threading.Thread(target=func, name='func-1',args=(para1 ,para2),kwargs={'age':10} )
    
    para1 =21
    para2 =22
    t2 = threading.Thread(target=func, name='func-2',args=(para1 ,para2),kwargs={'age':20}, )
    
    t0.start()
    t1.start()
    t2.start()
 
    time.sleep(1)
    
    print('退出主线程: ' ,threading.current_thread().name)
========执行结果:
-----欢迎来到www.juzicode.com
-----公众号: 桔子code/juzicode

进入主线程:  MainThread
1
进入线程:   func-1
func-1 para1:   11
func-1 para2:   12
func-1 kw:   {'age': 10}
退出线程:   func-1
进入线程:   func-2
func-2 para1:   21
func-2 para2:   22
func-2 kw:   {'age': 20}
退出线程:   func-2
退出主线程:  MainThread

3.3、daemon 参数

daemon参数用来表示线程是否属于看护线程,True表示是看护线程,必须在启动线程之前设置。如果是看护线程在主线程退出时,子线程也会自动跟着退出。下面的例子就是是否设置为看护线程时的表现,


print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   
 
import time, threading

def func1():
    thread_name = threading.current_thread().name
    print('进入线程:  ', thread_name)
    loop_cnt=0
    while True:
        loop_cnt+=1
        print(thread_name,':loop_cnt:',loop_cnt)
        time.sleep(1)
    print('退出线程:  ' , thread_name)

if __name__ == '__main__':
    print('进入主线程: ', threading.current_thread().name)
    t1 = threading.Thread(target=func1, name='func1-1',daemon=False)
    #t1 = threading.Thread(target=func1, name='func1-1',daemon=True)
    t1.start()
    while True:
        inp = input('----->')
        if inp.lower() == 'quit':
            break
    print('退出主线程: ' ,threading.current_thread().name)

当设置daemon=False时,在主线程输入quit退出后,子线程仍然还在运行:

-----欢迎来到www.juzicode.com
-----公众号: 桔子code/juzicode

进入主线程:  MainThread
进入线程:   func1-1
func1-1 :loop_cnt: 1
----->func1-1 :loop_cnt: 2
func1-1 :loop_cnt: 3
quitfunc1-1 :loop_cnt: 4

退出主线程:  MainThread   #这里主线程已经退出
func1-1 :loop_cnt: 5      #子线程仍然在继续运行
func1-1 :loop_cnt: 6
func1-1 :loop_cnt: 7
func1-1 :loop_cnt: 8
func1-1 :loop_cnt: 9
func1-1 :loop_cnt: 10
func1-1 :loop_cnt: 11
func1-1 :loop_cnt: 12

当设置为True时,主线程退出后子线程也跟着退出:

-----欢迎来到www.juzicode.com
-----公众号: 桔子code/juzicode

进入主线程:  MainThread
进入线程:   func1-1
func1-1 :loop_cnt: 1
----->func1-1 :loop_cnt: 2
func1-1 :loop_cnt: 3
func1-1 :loop_cnt: 4
func1-1 :loop_cnt: 5
quit
退出主线程:  MainThread
# 主线程退出后,子线程也跟着退出。

4、多线程实现服务端和客户端通信

回到本文开始提到的 《Python进阶教程m9–网络通信–socket通信》一文中socket服务端和客户端通信的例子,我们用多线程方式进行改造,使其能满足“全双工”通信的需求。

首先是实现服务端,在服务端的主线程中,仍然是创建socket实例、绑定端口、监听端口、等待连接的“标准流程”,当等待到客户端发来的连接后创建2个线程,一个为发送消息线程,一个为接收消息线程。在发送消息线程中用input()函数接收输入的消息,然后发送出去。在接收消息线程中,recv()方法阻塞等待客户端发来消息,接收到消息后显示出来。

print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   

import os,sys,socket,time,threading

def server_rec(connet):
    thread_name=threading.current_thread().name
    print(thread_name, ':进入线程')     
    while True:
        data = connet.recv(1024)#阻塞方式接收数据
        message = data.decode('utf8')#转换为string数据
        print('\n%s: 接收到消息:%s' %(thread_name, message) )
        
def server_send (connet):
    thread_name=threading.current_thread().name
    print(thread_name, ':进入线程')     
    while True:
        message = input('\n%s: 输入要发送的消息: \n'%thread_name)#输入要发送的消息
        data = bytes(message,encoding='utf8')#转换为bytes数据
        connet.send(data)#发送数据
 
if __name__ == '__main__':
    print('开启服务端.....')
    #创建socket服务端实例
    skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #绑定端口
    skt.bind(('127.0.0.1',19200))
    #监听端口
    skt.listen(100)
    #等待连接
    print('\n等待连接......')
    connet, address = skt.accept()
    print('客户端地址:%s,建立连接' % str(address))

    #开启接收和发送线程
    t1 = threading.Thread(target=server_send, name='server_send',args=(connet,)  )
    t2 = threading.Thread(target=server_rec, name='server_rec',args=(connet,) )
    t1.start()
    t2.start()
    
    while True:pass
    #关闭连接
    #connet.close()

在客户端中,主线程创建socket实例,连接服务端成功后创建2个线程,一个为发送消息线程,一个为接收消息线程,在发送消息和接收消息线程内部的处理方式和服务端类似:


print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   

import os,sys,socket,time,threading

def client_send(skt):
    thread_name=threading.current_thread().name
    print(thread_name, ':进入线程')     
    while True:
        message = input('\n%s: 输入要发送的消息: \n'%thread_name)#输入要发送的消息
        data = bytes(message,encoding='utf8')#转换为bytes数据
        skt.send(data)#发送数据

        
def client_rec(skt):
    thread_name=threading.current_thread().name
    print(thread_name, ':进入线程')     
    while True: 
        data = skt.recv(1024)#阻塞方式接收数据
        message = data.decode('utf8')#转换为string数据
        print( '\n%s: 接收到消息:%s'%(thread_name,message))
 
 
if __name__ == '__main__':
    print('开启客户端.....')
    #创建socket实例
    skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 建立连接,重试连接服务端
    connected = False
    for x in range(5):
        try:
            skt.connect(('127.0.0.1', 19200))
            connected = True
            break
        except:
            print('等待重连服务端.....')
    if not connected:
        print('服务端未开启')
        sys.exit(-1)
        
    #开启接收和发送线程
    t1 = threading.Thread(target=client_send, name='client_send',args=(skt ,))
    t2 = threading.Thread(target=client_rec, name='client_rec',args=(skt,  ) )
    t1.start()
    t2.start()
 
    while True:pass

最后的效果是这样的:

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注