什么是进程
- 进程是执行中的程序。
- 拥有独立地址空间,内存,数据栈等。
- 操作系统统一管理。
- 派生(fork或spawn)新进程。
- 进程间通信(IPC)方式共享信息。
什么是线程
- 同进程下执行,并共享相同的上下文。
- 线程间的信息共享和通信更加容易。
- 多线程并发执行。
- 需要同步原语。
python与线程
- 解释器主循环
- 主循环中只有一个控制线程在执行。
- 使用全局解释器锁(GIL)。
GIL保证一个线程
- 设置GIL
- 切换进一个线程去运行
- 执行下面的操作之一
- 指定数量的字节码指令
- 线程主动让出控制权
- 把线程设置回睡眠状态(切换出线程)
- 解锁GIL
- 重复上述步骤
两种线程管理
Python提供了两种线程管理模式。
- _thread:提供了基本的线程和锁。
- threading: 提供了更高级别,功能更全的线程管理。threading底层使用了_thread。
- 支持同步机制
- 支持守护线程
_thread模块
函数/方法 | 描述 |
thread模块的函数 | |
start_new_thread(function,args,kwargs=None) | 派生一个新的线程,使用给定的args和可选的kwargs来执行function |
allocate_lock() | 分配LockType锁对象 |
LockType锁对象方法 | |
acquire(wait=None) | 尝试获取锁对象 |
locked() | 如果获取了锁对象则返回True,否则返回False |
release() | 释放锁 |
使用_thread模块案例1
代码:
import _thread
import logging
from time import sleep, ctime
# 配置日志收集器
logging.basicConfig(level=logging.INFO)
def loop0():
"""定义一个子方法1"""
logging.info("start loop0 at " + ctime())
sleep(4)
logging.info("end loop0 at " + ctime())
def loop1():
"""定义一个子方法2"""
logging.info("start loop1 at " + ctime())
sleep(2)
logging.info("end loop1 at " + ctime())
def main():
"""主方法"""
logging.info("start all at " + ctime())
# 运行子方法1
_thread.start_new_thread(loop0, ())
# 运行子方法2
_thread.start_new_thread(loop1, ())
sleep(6)
logging.info("end all at " + ctime())
if __name__ == "__main__":
main()
运行结果:
注意:这里主方法中必须加上time.sleep(6)
,否则主进程退出后,所有子进程都将强行退出,所有子线程将不被执行。这里也该模块的缺点之一,因为它没有守护线程的概念。
以下结果是不加time.sleep(6)
的效果。
使用_thread模块案例2-利用锁
上述案例中其实存在缺点。在实际应用中,我们并不确定主线程需要等待子线程几秒钟,子线程才能结束。所以本案例加入锁,可以有效的解决这一问题。
代码:
import _thread
import logging
from time import sleep, ctime
# 配置日志收集器
logging.basicConfig(level=logging.INFO)
loops = [2, 4]
def loop(nloop, nsec, lock):
"""
定义一个子方法
nloop: 用于标识当前loop处于第几个
nsec: 时间,loop循环时间
lock: 锁
"""
logging.info(f"start loop {nloop} at " + ctime())
sleep(nsec)
logging.info(f"end loop {nloop} at " + ctime())
# loop执行完成后释放锁
lock.release()
def main():
"""主方法"""
logging.info("start all at " + ctime())
# 定义一个锁列表,可以包含很多的锁
locks = []
# loop的名字
nloops = range(len(loops))
for i in nloops:
# 声明一个锁
lock = _thread.allocate_lock()
# 锁上锁
lock.acquire()
# 将这个锁追加到锁列表
locks.append(lock)
# 获取锁是需要时间的,因此获取锁和开启子线程不能放在同一个for循环中
# 可以有效避免获取第2个锁的时间,第一个线程已经执行完毕了
# _thread.start_new_thread(loop, (i, loops[i], locks[i]))
for i in nloops:
# 启动子线程
_thread.start_new_thread(loop, (i, loops[i], locks[i]))
for i in nloops:
# 检查锁是否是锁定,如果被锁定就什么都不做;如果锁是释放的则退出循环
# 也就是等待每个子线程结束后才退出主线程
while locks[i].locked(): pass
logging.info("end all at " + ctime())
if __name__ == "__main__":
main()
运行结果:
threading模块
对象 | 描述 |
Thread | 表示一个执行线程的对象 |
Lock | 锁原语对象(和thread模块中的锁一样) |
RLock | 可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁) |
Condition | 条件变量对象,使得一个线程等待另一个线程满足特定的“条件”,比如改变状态或某个数据值 |
Event | 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有线程将被激活 |
Semaphore | 为线程间共享的有限资源提供了一个“计数器”,如果没有可用资源时会被阻塞 |
BoundedSemaphore | 与Semaphore相似,不过它不允许超过初始值 |
Timer | 与Thread相似,不过它要在运行前等待一段时间 |
Barrier | 创建一个“障碍”,必须达到指定数量的线程后才可以继续 |
使用threading模块案例
threading自带锁,可以更有效解决上述案例【使用_thread模块案例2-利用锁】的问题。
代码:
import threading
import logging
from time import sleep, ctime
# 配置日志收集器
logging.basicConfig(level=logging.INFO)
loops = [2, 4]
def loop(nloop, nsec):
"""
定义一个子方法
nloop: 用于标识当前loop处于第几个
nsec: 时间,loop循环时间
"""
logging.info(f"start loop {nloop} at " + ctime())
sleep(nsec)
logging.info(f"end loop {nloop} at " + ctime())
def main():
"""主方法"""
logging.info("start all at " + ctime())
# 声明一个线程组
threads = []
# loop的名字
nloops = range(len(loops))
for i in nloops:
# 设置子线程
t = threading.Thread(target=loop, args=(i, loops[i]))
threads.append(t)
for i in nloops:
# 启动子线程
threads[i].start()
for i in nloops:
# 将子线程加入到主线程中
# 如果子进程执行完毕,主线程就会结束。
# 如果子线程没有执行完成, 主线程就会阻塞,一定会等子线程执行完成
threads[i].join()
logging.info("end all at " + ctime())
if __name__ == "__main__":
main()
运行结果:
注意:loop 0 和loop 1的执行先后顺序不是一定的。
Thread类
属性 | 描述 |
Thread对象数据属性 | |
name | 线程名 |
ident | 线程的标识符 |
daemon | 布尔标志,表示这个线程是否是守护线程 |
Thread对象方法 | |
init(group=None, target=None, name=None, args=(), kwargs={}, verbose=None, daemon=None) | 实例化一个线程对象,需要有一个可调用的target,以及其参数args或kwargs。还可以传递name或group参数,不过后者还未实现。此外,verbose标志也是可接受的。而daemon的值将会设定thread.daemon属性/标志 |
start() | 开始执行该线程 |
run() | 定义线程功能的方法(通常在子类中被应用开发者重写) |
join(timeout=None) | 直至启动的线程终止之前一直挂起;除非给出了timeout(秒),否则会一直阻塞 |
getName() | 返回线程名 |
setName(name) | 设定线程名 |
isAlivel/is_alive() | 布尔标志,表示这个线程是否还存活 |
isDaemon() | 如果是守护线程,则返回True,否则返回False |
setDaemon(daemonic) | 把线程的守护标志设定为布尔值daemonic(必须在线程start()之前调用) |
使用threading模块案例-重写threading.Thread
我们可以继续改进上述案例【使用threading模块案例】,使得代码更加符合面向对象编程的思想。
代码:
import threading
import logging
from time import sleep, ctime
# 配置日志收集器
logging.basicConfig(level=logging.INFO)
loops = [2, 4]
class MyThread(threading.Thread):
def __init__(self, func, args, name=""):
""" 主动调用threadding.Thread的初始构造方法 """
threading.Thread.__init__(self)
self.func = func
self.args = args
self.name = name
def run(self) -> None:
"""重写父类的run方法"""
self.func(*self.args)
def loop(nloop, nsec):
"""
定义一个子方法
nloop: 用于标识当前loop处于第几个
nsec: 时间,loop循环时间
"""
logging.info(f"start loop {nloop} at " + ctime())
sleep(nsec)
logging.info(f"end loop {nloop} at " + ctime())
def main():
"""主方法"""
logging.info("start all at " + ctime())
# 声明一个线程组
threads = []
# loop的名字
nloops = range(len(loops))
for i in nloops:
# 设置子线程
t = MyThread(loop, (i, loops[i]), loop.__name__)
threads.append(t)
for i in nloops:
# 启动子线程
threads[i].start()
for i in nloops:
# 将子线程加入到主线程中
# 如果子进程执行完毕,主线程就会结束。
# 如果子线程没有执行完成, 主线程就会阻塞,一定会等子线程执行完成
threads[i].join()
logging.info("end all at " + ctime())
if __name__ == "__main__":
main()
运行结果: