Python多线程
(一)、线程与进程
1、进程介绍
-
计算机程序有静态和动态的区别
-
静态的计算机程序就是存储在磁盘上的可执行二进制(或其他类型)文件
-
动态的计算机程序就是将这些可执行文件加载到内存中并被操作系统调用,这些动态的计算机程序被称为一个进程,也就是说,进程是活跃的,只有可执行程序被调入内存中才称为进程
-
每个进程都拥有自己的地址控件、内存、数据栈统计其他用于跟踪执行的辅助数据
-
操作系统会管理系统中所有进程的执行,并为这些进程合理的分配时间
-
进程可以通过派生(fork 或 spawn)新的进程来执行其他任务,不过由于每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的反式共享信息
2、线程介绍
- 线程(有时候也被称为轻量级进程)和进程类似,不过线程是在同一个进程下执行的,共享同一个上下文。也就是说,线程属于进程,而且线程必须要依靠进程才能执行。一个进程可以包含一个或多个线程。
- 线程包括开始、执行和结束三部分。它有一个指针,用于记录当前运行的上下文,当其他线程运行时,当前线程有可能被抢占(中断)或临时挂起(睡眠)
- 一个进程中的各个线程与主线程共享同一片数据空间,所以说进程间的信息共享和通信更容易。线程一般是以并发方式执行的,正是由于这种并行和数据贡献机制,使得多任务间的协作称为可能。当然,在单核CPU的系统中,并不存在真正的并发运行,所以线程的执行实际上还是同步执行的,只是系统会根据调度算法在不同的时间安排某个线程在CPU上执行一小会,然后就会让其他线程在CPU上再执行一会,通过这种多个线程之间不断切换的方式让多个线程交替执行。因此,从宏观上看,即使再单核CPU的系统上仍然看着像多个线程并发运行一样
- 多线程间共享数据也存在风险。如果两个或多个线程访问了同一片数据,由于数据访问顺序不同,可能导致结果不一致。这种情况通常称为静态条件(static condition),但是也不必担心,大多数线程库都有一些机制让共享内存区域的数据同步,也就是说,当一个线程访问这片内存区域时,这片内存区域暂时被锁定,其他的线程只能等待这片区域解锁后再访问
- 线程的执行时间是不平等的,例如,有6个线程,6s的CPU执行时间,并不是为这6个线程平均分配CPU执行时间(每个线程1s),而是根据线程中具体的执行代码分配CPU计算时间。例如,在调动一些函数时,这些函数会在完成之前保持阻塞状态(阻止其他线程获得CPU执行时间),这样这些函数就会长时间占用CPU资源,通常来说,系统在分配CPU计算时间时更倾向于这些需要长时间的函数
(二)、线程
Python多线程在底层使用了兼容POSIX的线程,也就是pthread
1、单线程执行程序
import time
def fun1():
print("start fun1:", time.ctime())
time.sleep(4)
print("end fun1:", time.ctime())
print("---------------------------------------")
def fun2():
print("start fun2:", time.ctime())
time.sleep(2)
print("end fun2:", time.ctime())
print("---------------------------------------")
def main():
print("start main:", time.ctime())
print()
fun1()
fun2()
print()
print("end main:", time.ctime())
if __name__ == "__main__":
main()
start main: Mon Mar 20 22:33:13 2023
start fun1: Mon Mar 20 22:33:13 2023
end fun1: Mon Mar 20 22:33:17 2023
---------------------------------------
start fun2: Mon Mar 20 22:33:17 2023
end fun2: Mon Mar 20 22:33:20 2023
---------------------------------------
end main: Mon Mar 20 22:33:20 2023
程序的运行时间应该是大于6s,因为函数间的切换也需要时间,这就需要多线程来缩短执行时间
2、多线程执行程序
- Python提供了很多内建模块用于支持多线程,这里讲的第一个模块
_thread
。在Python2.x版本时,这个模块称为thread
,从Python3.x版本开始,thread
更名为_thread
- 使用
_thread
模块中的start_new_thread
函数会直接开启一个线程,该函数的第1个参数需要指定一个函数(也就是我们想要加入多线程的方法),可以把这个函数称为线程函数,当线程启动时会自动调用这个函数。start_new_thread
的第2个参数是给线程函数传递参数,必须是元组类型 _thread.start_new_thread(函数名, (参数))
import _thread as thread
import time
def fun1():
print("start fun1:", time.ctime())
time.sleep(4)
print("end fun1:", time.ctime())
def fun2():
print("start fun2:", time.ctime())
time.sleep(2)
print("end fun2:", time.ctime())
def main():
print("start main:", time.ctime())
thread.start_new_thread(fun1, ())
thread.start_new_thread(fun2, ())
time.sleep(6)
print("end main:", time.ctime())
if __name__ == "__main__":
main()
start main: Mon Mar 20 22:49:36 2023
start fun1: Mon Mar 20 22:49:36 2023
start fun2: Mon Mar 20 22:49:36 2023
end fun2: Mon Mar 20 22:49:38 2023
end fun1: Mon Mar 20 22:49:40 2023
end main: Mon Mar 20 22:49:42 2023
从结果看:执行fun1函数的过程中,会使用第二个线程执行fun2函数。这是因为在fun1函数中休眠了4s,当程序休眠时,会释放CPU的计算资源,这时fun2函数就趁虚而入,抢占fun1函数的CPU计算资源。而fun2函数只休眠了2s,所以fun2执行完后,fun1还在休眠,4s后fun1才继续执行,这是已经没函数抢占CPU资源,所以fun1顺利继续执行完毕。在main函数中也需要休眠6s(大于4s即可)等待fun1和fun2执行完,再结束程序
3、为线程函数传递参数
通过start_new_thread
函数的第2个参数可以为线程函数传递参数,该参数必须是元组
例子:for循环启动8个线程,并为每个线程函数传递不同的参数只,然后再线程函数中输出传入的参数值
import _thread as thread
import time
import random
def fun(arg1, arg2):
print("第", arg1, "个线程:", arg2)
time.sleep(random.randint(1, 5))
for i in range(8):
thread