线程
教学博客地址
线程后于进程出现,在线程出现之前,进程是最小资源和调度单位,在线程出现之后,实际上现代操作系统是多线程操作系统,CPU调度的最小单位,可执行的最小单元,都是线程了,而进程是分配资源的最小单元,是包含线程的数据结构.线程是动态的,在同一进程之内的所有线程,都可以共享该进程的内存和文件,包括主线程代码,主线程全局变量,打开的文件,信号量等组件.由于一个主线程的各个子线程都在同一个进程内,无需通过操作系统进行通信.
也就是说,从今天开始,针对可执行程序的概念需要全部更新,线程是最小的单元,进程是一堆线程的集合.说到执行了一个进程,实际上是执行了这个进程里的主线程(还可能有子线程).之前的多进程编程,实际上是为每个任务分配一个进程,而任务本身,是跑在每个进程的主线程内.
图解进程和线程
Threading模块的应用
Python内使用多线程,一般应用Threading模块,Threading模块的接口与multiprocessing模块的接口基本相同.先来看一个基本例子:
from threading import Thread
import time
import os
def func(n):
global g
time.sleep(1)
print(os.getpid())
print(n+g)
g = 10
for i in range(10):
t = Thread(target=func, args=(i,))
t.start()
从这个例子里可以看到,多线程编程在windows内无需再使用 if name == ""main""语句,线程并发执行,而且主线程定义的全局变量,是可以被子线程使用的.如果改用Process,会报错:name g is not defined.这就很明显的说明进程之间不能直接共享在主进程内的数据,而子线程和主线程都在一个进程内,可以直接共享定义好的全局变量.
实际上,支持各个线程运行的代码都在进程的内存内存储,各个线程存储各自的数据栈.上边代码里,g可以被共享,但是每个线程里内部的参数和其他形参是不能共享的.
此外,启动多线程还可以采用继承Thread类的方法,也需要实现.run方法,不再赘述.
在继续学习多线程之前,先要了解python的GIL机制.
全局解释器锁
GIL.
一句话解释GIL:Cpython解释器下的python程序,同一时间只能运行一个线程.
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程环境中,Python 虚拟机按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。
在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
GIL锁的不是数据,而是线程,也就是说,Cpython内部的解释器实际上只支持单线程工作,所以Cpython解释器的多线程效率并不高,无法充分利用CPU.
这个只是Cpython解释器的特性,并不是python语言的特性.如果用Jython等解释器,就没有GIL.当然,这个时候的进程内的数据安全,需要程序员额外关心.
Python cookbook 12.9 Python的全局锁问题 很好的解释了GIl的概念.
实际上,解释型语言到目前为止,由于并不是像编译型语言需要解释器环境,而是跑在虚拟机内,所以都不能很好的利用多核CPU.但是再次强调,这并非python语言的问题,而是解释器的问题.
解决多线程效率低下的问题,如果只工作在python环境下,可以采用multiprocessing的进程池模块,每个任务后台实际上分配了一个对应python解释器,也就是一个单独的进程(线程)来操作,可以实现真正的并行运行.另外的办法是采用C扩展,线程在遇到C扩展手工指令的时候,可以释放GIL,这个属于高端内容,以后再来学习.
Threading模块应用
对于外在表现来说,多线程和多进程对于无需共享数据的应用模式一样.
# 用多线程实现socketserver,原理一样,将接受的连接放入新线程内去执行,只需要修改一行代码
from socket import *
from threading import Thread
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))
server.listen(5)
def talk(conn, client_addr):
print(conn, client_addr)
while True:
try:
msg = conn.recv(1024)
if not msg: break
print('来自于{}的消息:{}'.format(client_addr,msg.decode('utf-8')))
data = input('输入想发送给{}的信息:'.format(client_addr)) # 多线程里可以使用input,因为是在一个进程内,没有冲突
conn.send(data.encode('utf-8'))
except Exception:
break
conn.close()
while True:
conn, client_addr = server.accept()
p = Thread(target=talk, args=(conn, client_addr))
p.start()
在进入线程各个组件之前,总结一下Thread模块的方法.
实例化 | group=None, target=可调用对象, name=线程名, args=传给target的参数元组, kwargs=传给target的关键字参数字典, *, daemon=如果是None,继承当前线程的类型,如果是True表示是守护线程) |
start() | 启动线程 |
run() | start方法就会调用run方法,如果继承Thread类,需要自己实现 |
join(timeout=None) | 同步化等待主线程,和进程的join方法类似 |
name | 返回线程名 |
is_alive() | 线程是否还生存 |
daemon | 设置是否为守护线程,跟随主线程结束而结束.主线程的结束和进程不同,是指所有子线程都执行完毕. |
ident | 最好是使用threading.get_ident() |
守护线程
守护线程和守护进程的定义类似,需要解决的是守护线程何时被结束.
from threading import Thread
import time
def func1():
print('this is func1')
time.sleep(10)
def func2():
for i in range(5):
print(i)
time.sleep(1)
t1 = Thread(target=func1,args=())
t2 = Thread(target=func2,args=())
t1.start()
t2.start()
可以看到,在普通情况下,主线程一直到func1睡完了10秒才结束,5秒的时候子线程func2就结束了.现在把func1线程设置为守护线程.然后修改一下代码
from threading import Thread
import time
def func1():
time.sleep(1)
print('this is func1')
def func2():
for i in range(5):
print(i)
time.sleep(1)
if __name__ == '__main__':
t1 = Thread(target=func1,args=())
t1.daemon = True
t2 = Thread(target=func2,args=())
t1.start()
t2.start()
可以发现,func1的内容得到了运行,如果将Thread改成multiprocessing.Process,会发现没有显示出this is func1,说明func1进程已经结束了,这是因为守护线程和守护进程何时结束的判定是不同的.
对于线程来讲,主线程的结束,是指主线程内所有非守护进程统统运行完毕.守护线程会等待主线程的结束而结束.例子里,func1的关闭实际上是在func2的5秒计数结束之后.
而如果改成进程,主进程执行到t2.start()之后就意味着主进程结束,守护进程会立刻跟着结束.主进程执行这些代码用时很少,所以func1守护进程刚启动就被结束了,根本无法显示this is func1.
一句话总结就是:进程之间互相独立,运行完了该收就收,谁先结束就回收谁;线程之间都是在一个进程里,要死一起死,所以要等到最后的结束了,主线程才能结束守护线程.