Python中的线程模块之_thread模块(第2节)


在Python程序中,线程模块是一种广泛应用于软件开发的技术,引入线程可以提高程序并发执行的程度,并进一步提高系统效率。我们可以通过“_thread” 和threading (推荐使用)这两个模块来处理线程。thread模块在Python3.8版本中已经完全移除,转而使用threading模块。所以,在Python3.8版本中不能再使用thread模块,但是为了兼容Python3.8以前的程序,已经将thread模块更名为“_thread”模块。

_thread模块

_thread模块是Python早期的线程模块,它提供了一些基本的线程操作函数,用法十分简单,它的核心其实就是start_new_thread()函数。

_thread.start_new_thread(函数名, (参数))函数会直接开启一个线程,该函数的第一个参数需要指定一个函数(也就是我们想要加入多线程的方法),可以把这个函数称为线程函数,当线程启动时会自动调用这个函数。第二个参数是给线程函数传递参数,必须是元组类型。

为了对比单线程和多线程处理数据的速度,下面我们首先使用单线程执行程序:

动手练一练:

# 导入模块
import _thread
import time

def fun1():
    print("开始运行fun1:", time.ctime())
    time.sleep(2)
    print("运行fun1结束:", time.ctime())

def fun2():
    print("开始运行fun2:", time.ctime())
    time.sleep(1)
    print("运行fun2结束:", time.ctime())

def main():
    print("开始运行")
    fun1()
    fun2()
    print("运行结束")

if __name__=='__main__':
    main()

执行以上代码,输出结果为:

开始运行
开始运行fun1 Tue Apr 23 10:07:44 2024
运行fun1结束 Tue Apr 23 10:07:46 2024
开始运行fun2 Tue Apr 23 10:07:46 2024
运行fun2结束 Tue Apr 23 10:07:47 2024
运行结束

上面的例子中,从程序运行结果可以看出,程序的运行时间前后花了3秒,因为单线程中的函数不是“同步”执行的,必须等到fun1函数执行完,才能切换执行fun2函数,如果想要让fun1函数和fun2函数同时执行,这就需要使用_thread模块创建多线程来缩短执行时间,例如:

动手练一练:

# 导入模块
import _thread
import time

def fun1():
    print("开始运行fun1:", time.ctime())
    time.sleep(2)
    print("运行fun1结束:", time.ctime())

def fun2():
    print("开始运行fun2:", time.ctime())
    time.sleep(1)
    print("运行fun2结束:", time.ctime())

def main():
    print("开始运行")
    # 创建线程
    _thread.start_new_thread(fun1,())
    _thread.start_new_thread(fun2,())
    time.sleep(3)
    print("运行结束")

if __name__=='__main__':
    main()

执行以上代码,输出结果为:

开始运行
开始运行fun1 Tue Apr 23 10:23:37 2024
开始运行fun2 Tue Apr 23 10:23:37 2024
运行fun2结束 Tue Apr 23 10:23:38 2024
运行fun1结束 Tue Apr 23 10:23:39 2024
运行结束

上面的例子中,从程序运行结果可以看出,_thread模块的start_new_thread方法提供了简单的多线程机制,程序的运行时间前后只花了2秒。在fun1线程执行时,fun2线程也在“同步”地执行,因为两个线程开始执行的时间一模一样。并且两个线程会根据自己线程内的time.sleep()休眠时间执行,每个线程互相独立运行,互不影响。所以引入多线程可以明显提高程序的运行速度。注意,由于fun1线程休眠时间多了一秒,所以fun1线程结束时间会晚一秒。

我们还可以为线程函数传递参数,通过start_new_thread()函数的第二个参数可以为线程函数传递参数,该参数必须是元组,例如:

动手练一练:

import _thread
import time

def fun1(a, b):
    print("开始运行fun1,线程的名:", a)
    time.sleep(b)
    print("运行fun1结束")

def fun2(a, b):
    print("开始运行fun2,线程的名:", a)
    time.sleep(b)
    print("运行fun2结束")

def main():
    print("开始运行")
    # 创建线程
    _thread.start_new_thread(fun1,("第一个线程",2))
    _thread.start_new_thread(fun2,("第二个线程",2))
    time.sleep(4)
    print("运行结束")

if __name__=='__main__':
    main()

执行以上代码,输出结果为:

开始运行
开始运行fun1线程的名 第一个线程
开始运行fun2线程的名 第二个线程
运行fun1结束
运行fun2结束
运行结束

在上面的代码中,我们为线程函数传递参数,让程序产生多个线程。这里需要注意的是,main()函数为主线程,用来控制整个进程从开始到结束的全部操作。在main()函数的最后必须使用time.sleep(4)函数让程序处于休眠状态,目的是为了在所有线程执行完之前,让程序暂停4秒钟,阻止程序退出。如果main()函数里面没有使用time.sleep(4)函数让程序暂停,主线程执行完就会立马退出,所有的线程也会直接关闭,因为main()函数无法判断是否有线程正在执行,以及是否所有线程函数都执行完毕,所以我们只能采用估算时间的方法让程序暂停4秒,直到所有的线程函数都执行完毕再退出。

在复杂的Python程序开发中,我们有时候很难估算线程的实际执行时间,主线程过早或者过晚的退出都不是我们所期盼的,这时候我们就需要使用线程锁。线程锁是通过锁让程序了解是否还有线程函数没执行完,而且可以做到当所有的线程函数执行完后,程序会立即退出,无需任何等待,提高程序运行的效率。

线程锁的使用比较简单,主要分为4个步骤:创建锁、获取锁、释放锁、检查锁。首先,使用_thread.allocate_lock()函数创建锁对象,然后使用锁对象的acquire()方法获取锁,如果不需要锁,可以使用锁对象的release()方法释放锁,最后通过使用锁对象的locked()方法检查锁是否被释放,如果锁被某个线程锁定,则返回True,否则返回False。例如:

动手练一练:

import time
import _thread

def fun(n, s, lock):
    print("第", n, "个线程开始执行时间:", time.ctime())
    time.sleep(s)
    print("第", n, "个线程结束执行时间:", time.ctime())
    lock.release()

def main():
    # 创建第一个锁对象(分配锁)
    a = _thread.allocate_lock()
    # 获取锁,相当于加锁
    a.acquire()
    # 启动第一个线程,并传入a锁对象
    _thread.start_new_thread(fun, (1, 2, a))

    # 创建第二个锁对象
    b = _thread.allocate_lock()
    b.acquire()
    _thread.start_new_thread(fun, (2, 4, b))

    # 通过while循环和locked()方法判断锁是否全部释放,并退出执行
    # 当任何一个锁处于上锁状态,while循环判定为True,程序会一直执行,直到False退出
    while a.locked() or b.locked():
        # pass不做操作
        pass

if __name__ == "__main__":
    main()

执行以上代码,输出结果为:

 1 个线程开始执行时间 Mon Apr 29 08:48:51 2024
 2 个线程开始执行时间 Mon Apr 29 08:48:51 2024
 1 个线程结束执行时间 Mon Apr 29 08:48:53 2024
 2 个线程结束执行时间 Mon Apr 29 08:48:55 2024

上面的例子中,我们在main()主线程函数里面启动两个线程,并创建两个锁对象,在运行两个线程函数之前,先使用acquire()方法获取这两个锁,让锁处于锁定状态,当线程函数执行完毕后,会调用锁对象的release()方法释放锁。通过while循环判断锁是否全部释放,若两个锁都没释放,程序不能停止。从执行的结果可以看到,两个线程可以“同步”地执行,并且两个线程会根据自己线程内的time.sleep()休眠时间执行,直到两个线程函数都执行完毕会自动退出。使用了线程锁可以有效地避免主线程过早或者过晚地退出,而不需要在main()主线程函数里面手动使用time.sleep()暂停等待程序执行完毕。