多线程编程 - thread 模块

多线程编程 - thread 模块

除了派生线程外,thread 模块还提供了基本的同步数据结构,称为锁对象 (lock object,亦称原语锁、简单锁、互斥锁、互斥和二进制信号量)。这个同步原语和线程管理是密切相关的。

thread 模块的核心函数是 start_new_thread()。它的参数包括函数 (对象)、函数的参数以及可选的关键字参数。将专门派生新的线程来调用这个函数。

thread 模块和锁对象
在这里插入图片描述

#!/usr/bin/env python3

import _thread
from time import sleep, ctime


def loop0():
    print('start loop 0 at:', ctime())
    sleep(4)
    print('loop 0 done at:', ctime())


def loop1():
    print('start loop 1 at:', ctime())
    sleep(2)
    print('loop 1 done at:', ctime())


def main():
    print('starting at:', ctime())
    _thread.start_new_thread(loop0, ())
    _thread.start_new_thread(loop1, ())
    sleep(6)
    print('all DONE at:', ctime())


if __name__ == '__main__':
    main()

Python2

import thread
from time import sleep, ctime

start_new_thread() 必须包含开始的两个参数,于是即使要执行的函数不需要参数,也需要传递一个空元组。

与之前的代码相比,本程序执行后的输出结果有很大不同。原来需要运行 6~7 秒的时间,而现在的脚本只需要运行 4 秒,也就是最长的循环加上其他所有开销的时间之和。

/usr/bin/python3.5 /home/strong/workspace/******.py
starting at: Sat May 11 14:25:25 2019
start loop 0 at: Sat May 11 14:25:25 2019
start loop 1 at: Sat May 11 14:25:25 2019
loop 1 done at: Sat May 11 14:25:27 2019
loop 0 done at: Sat May 11 14:25:29 2019
all DONE at: Sat May 11 14:25:31 2019

Process finished with exit code 0

睡眠 4 秒和睡眠 2 秒的代码片段是并发执行的,这样有助于减少整体的运行时间。你甚至可以看到 loop 1 是如何在 loop 0 之前结束的。

这个应用程序中剩下的一个主要区别是增加了一个 sleep(6) 调用。这是因为如果我们没有阻止主线程继续执行,它将会继续执行下一条语句,显示“all done”然后退出,而 loop0() 和 loop1() 这两个线程将直接终止。

我们没有写让主线程等待子线程全部完成后再继续的代码,即我们所说的线程需要某种形式的同步。在这个例子中,调用 sleep() 来作为同步机制。将其值设定为 6 秒是因为我们知道所有线程 (用时 4 秒和 2 秒的) 会在主线程计时到 6 秒之前完成。

肯定会有比在主线程中额外延时 6 秒更好的线程管理方式。由于这个延时,整个程序的运行时间并没有比单线程的版本更快。像这样使用 sleep() 来进行线程同步是不可靠的。我们可能会过早或过晚退出主线程。这就是引出锁的原因。

通过使用锁,我们可以在所有线程全部完成执行后立即退出。

#!/usr/bin/env python3

import _thread
from time import sleep, ctime

loops = [4, 2]


def loop(nloop, nsec, lock):
    print('start loop', nloop, 'at:', ctime())
    sleep(nsec)
    print('loop', nloop, 'done at:', ctime())
    lock.release()


def main():
    print('starting threads...')
    locks = []
    nloops = list(range(len(loops)))

    for i in nloops:
        lock = _thread.allocate_lock()
        lock.acquire()
        locks.append(lock)

    for i in nloops:
        _thread.start_new_thread(loop, (i, loops[i], locks[i]))

    for i in nloops:
        while locks[i].locked():
            pass

    print('all DONE at:', ctime())


if __name__ == '__main__':
    main()

/usr/bin/python3.5 /home/strong/workspace/******.py
starting threads...
start loop 0 at: Sat May 11 14:27:32 2019
start loop 1 at: Sat May 11 14:27:32 2019
loop 1 done at: Sat May 11 14:27:34 2019
loop 0 done at: Sat May 11 14:27:36 2019
all DONE at: Sat May 11 14:27:36 2019

Process finished with exit code 0

逐行解释
第 1~6 行
在 UNIX 启动行后,导入了 time 模块的几个熟悉属性以及 thread 模块。我们不再把 4 秒和 2 秒硬编码到不同的函数中,而是使用了唯一的 loop() 函数,并把这些常量放进列表 loops 中。

第 9~13 行
loop() 函数代替了之前例子中的 loop*() 函数。我们必须在 loop() 函数中做一些修改,以便它能使用锁来完成自己的任务。其中最明显的变化是我们需要知道现在处于哪个循环中,以及需要睡眠多久。最后一个新的内容是锁本身。每个线程将被分配一个已获得的锁。当sleep() 的时间到了的时候,释放对应的锁,向主线程表明该线程已完成。

第 16~34 行
大部分工作是在 main() 中完成的,这里使用了 3 个独立的 for 循环。首先创建一个锁的列表,通过使用 thread.allocate_lock() 函数得到锁对象,然后通过 acquire() 方法取得 (每个锁)。取得锁效果相当于“把锁锁上”。一旦锁被锁上后,就可以把它添加到锁列表 locks 中。下一个循环用于派生线程,每个线程会调用 loop() 函数,并传递循环号、睡眠时间以及用于该线程的锁这几个参数。那么为什么我们不在上锁的循环中启动线程呢?有两个原因:其一,我们想要同步线程,以便“所有的马同时冲出围栏”;其二,获取锁需要花费一点时间。如果线程执行得太快,有可能出现获取锁之前线程就执行结束的情况。

在每个线程执行完成时,它会释放自己的锁对象。最后一个循环只是坐在那里等待 (暂停主线程),直到所有锁都被释放之后才会继续执行。因为我们按照顺序检查每个锁,所有可能会被排在循环列表前面但是执行较慢的循环所拖累。这种情况下,大部分时间是在等待最前面的循环。当这种线程的锁被释放时,剩下的锁可能早已被释放 (也就是说,对应的线程已经执行完毕)。结果就是主线程会飞快地、没有停顿地完成对剩下锁的检查。最后,你应该知道只有当我们直接调用这个脚本时,最后几行语句才会执行 main() 函数。

使用 thread 模块只是为了介绍多线程编程。多线程应用程序应当使用更高级别的模块,比如threading 模块。

References

Python 核心编程 (第 3 版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值