几乎所有的操作系统都支持同时运行多个任务,每个任务通常是一个程序,每一个运行中的程序就是一个进程,即进程是应用程序的执行实例。现代的操作系统几乎都支持多进程并发执行。注意,并发和并行是两个概念,并行指在同一时刻有多条指令在多个处理器上同时执行;并发是指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
例如,程序员一边开着开发工具在写程序,一边开着参考手册备查,同时还使用电脑播放音乐……除此之外,每台电脑运行时还有大量底层的支撑性程序在运行……这些进程看上去像是在同时工作。但事实的真相是,对于一个 CPU 而言,在某个时间点它只能执行一个程序。也就是说,只能运行一个进程,CPU 不断地在这些进程之间轮换执行。那么,为什么用户感觉不到任何中断呢?这是因为相对人的感觉来说,CPU 的执行速度太快了(如果启动的程序足够多,则用户依然可以感觉到程序的运行速度下降了)。所以,虽然 CPU 在多个进程之间轮换执行,但用户感觉到好像有多个进程在同时执行。
线程是进程的组成部分,一个进程可以拥有多个线程。在多线程中,会有一个主线程来完成整个进程从开始到结束的全部操作,而其他的线程会在主线程的运行过程中被创建或退出。当进程被初始化后,主线程就被创建了,对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在进程内创建多个顺序执行流,这些顺序执行流就是线程。当一个进程里只有一个线程时,叫作单线程。超过一个线程就叫作多线程。每个线程必须有自己的父进程,且它可以拥有自己的堆栈、程序计数器和局部变量,但不拥有系统资源,因为它和父进程的其他线程共享该进程所拥有的全部资源。线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同完成进程所要完成的任务。多个线程共享父进程里的全部资源,会使得编程更加方便,需要注意的是,要确保线程不会妨碍同一进程中的其他线程。线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的运行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。多线程也是并发执行的,即同一时刻,Python 主程序只允许有一个线程执行,这和全局解释器锁有关系,后续会做详细介绍。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发运行。从逻辑的角度来看,多线程存在于一个应用程序中,让一个应用程序可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配,线程的调度和管理由进程本身负责完成。
简而言之,进程和线程的关系是这样的:操作系统可以同时执行多个任务,每一个任务就是一个进程,进程可以同时执行多个任务,每一个任务就是一个线程。
Python创建线程 threading
Python 中,有关线程开发的部分被单独封装到了模块中,和线程相关的模块有以下 2 个:
_thread:是 Python 3 以前版本中 thread 模块的重命名,此模块仅提供了低级别的、原始的线程支持,以及一个简单的锁。功能比较有限。正如它的名字所暗示的(以 _ 开头),一般不建议使用 thread 模块;
threading:Python 3 之后的线程模块,提供了功能丰富的多线程支持,推荐使用。
本节就以 threading 模块为例进行讲解。Python 主要通过两种方式来创建线程:
- 使用 threading 模块中 Thread 类的构造器创建线程。即直接对类 threading.Thread 进行实例化创建线程,并调用实例化对象的 start() 方法启动线程。
- 继承 threading 模块中的 Thread 类创建线程类。即用 threading.Thread 派生出一个新的子类,将新建类实例化创建线程,并调用其 start() 方法启动线程。
调用Thread类的构造器创建线程
Thread 类提供了如下的 __init__()构造器,可以用来创建线程:__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None) 此构造方法中,以上所有参数都是可选参数,即可以使用,也可以忽略。其中各个参数的含义如下:
- group:指定所创建的线程隶属于哪个线程组(此参数尚未实现,无需调用);
- target:指定所创建的线程要调度的目标方法(最常用);
- args:以元组的方式,为 target 指定的方法传递参数;
- kwargs:以字典的方式,为 target 指定的方法传递参数;
- daemon:指定所创建的线程是否为后代线程。
这些参数,初学者只需记住 target、args、kwargs 这 3 个参数的功能即可。
下面程序演示了如何使用 Thread 类的构造方法创建一个线程:
#定义线程要调用的方法,*add可接收多个以非关键字方式传入的参数
def action(*add):
for arc in add:
#调用 getName() 方法获取当前执行该程序的线程名
print(threading.current_thread().getName() +" "+ arc)
#定义为线程方法传入的参数
my_tuple = ("http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/java/")
#创建线程
thread = threading.Thread(target = action,args =my_tuple)
有关 Thread 类提供的和线程有关的方法,可阅读Python Thread手册,由于不是本节重点,这里不再进行详细介绍。
由此就创建好了一个线程。但是线程需要手动启动才能运行,threading 模块提供了 start() 方法用来启动线程。因此在上面程序的基础上,添加如下语句:thread.start()
再次执行程序,其输出结果为:
Thread-1 http://c.biancheng.net/shell/
Thread-1 http://c.biancheng.net/java/
可以看到,新创建的 thread 线程(线程名为 Thread-1)执行了 action() 函数。
默认情况下,主线程的名字为 MainThread,用户启动的多个线程的名字依次为 Thread-1、Thread-2、Thread-3、…、Thread-n 等。
为了使 thread 线程的作用更加明显,可以继续在上面程序的基础上添加如下代码,让主线程和新创建线程同时工作:
for i in range(5):print(threading.current_thread().getName())
再次执行程序,其输出结果为:
MainThreadThread-1 http://c.biancheng.net/python/MainThreadThread-1 http://c.biancheng.net/shell/
MainThreadThread-1 http://c.biancheng.net/java/
MainThread
MainThread
可以看到,当前程序中有 2 个线程,分别为主线程 MainThread 和子线程 Thread-1,它们以并发方式执行,即 Thread-1 执行一段时间,然后 MainThread 执行一段时间。通过轮流获得 CPU 执行一段时间的方式,程序的执行在多个线程之间切换,从而给用户一种错觉,即多个线程似乎同时在执行。
如果程序中不显式创建任何线程,则所有程序的执行,都将由主线程 MainThread 完成,程序就只能按照顺序依次执行。
继承Thread类创建线程类
通过继承 Thread 类,我们可以自定义一个线程类,从而实例化该类对象,获得子线程。需要注意的是,在创建 Thread 类的子类时,必须重写从父类继承得到的 run() 方法。因为该方法即为要创建的子线程执行的方法,其功能如同第一种创建方法中的 action() 自定义函数。
下面程序,演示了如何通过继承 Thread 类创建并启动一个线程:
#创建子线程类,继承自 Thread 类
class my_Thread(threading.Thread):
def __init__(self,add):
threading.Thread.__init__(self)
self.add = add
# 重写run()方法
def run(self):
for arc in self.add:
#调用 getName() 方法获取当前执行该程序的线程名
print(threading.current_thread().getName() +" "+ arc)
#定义为 run() 方法传入的参数
my_tuple = ("http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/java/")
#创建子线程
mythread = my_Thread(my_tuple)
#启动子线程
mythread.start()
#主线程执行此循环
for i in range(5):
print(threading.current_thread().getName())
程序执行结果为:
MainThreadThread-1 http://c.biancheng.net/python/MainThreadThread-1 http://c.biancheng.net/shell/
MainThreadThread-1 http://c.biancheng.net/java/
MainThread
MainThread
此程序中,子线程 Thread-1 执行的是 run() 方法中的代码,而 MainThread 执行的是主程序中的代码,它们以快速轮换 CPU 的方式在执行。
Thread join()用法
如何通过 Thread 类创建并启动一个线程,当时给读者用如下的程序进行演示:
import threading#定义线程要调用的方法,*add可接收多个以非关键字方式传入的参数
def action(*add):
for arc in add:
#调用 getName() 方法获取当前执行该程序的线程名
print(threading.current_thread().getName() +" "+ arc)
#定义为线程方法传入的参数
my_tuple = ("http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/java/")
#创建线程
thread = threading.Thread(target = action,args =my_tuple)
#启动线程
thread.start()
#主线程执行如下语句
for i in range(5):
print(threading.current_thread().getName())
程序执行结果为(不唯一):
Thread-1 http://c.biancheng.net/python/MainThreadThread-1 http://c.biancheng.net/shell/MainThread
Thread-1 http://c.biancheng.net/java/MainThread
MainThread
MainThread
可以看到,我们用 Thread 类创建了一个线程(线程名为 Thread-1),其任务是执行 action() 函数。同时,我们也给主线程 MainThread 安排了循环任务(第 16、17 行)。通过前面的学习我们知道,主线程 MainThread 和子线程 Thread-1 会轮流获得 CPU 资源,因此该程序的输出结果才会向上面显示的这样。
但是,如果我们想让 Thread-1 子线程先执行,然后再让 MainThread 执行第 16、17 行代码,该如何实现呢?很简单,通过调用线程对象的 join() 方法即可。
join() 方法的功能是在程序指定位置,优先让该方法的调用者使用 CPU 资源。该方法的语法格式如下:thread.join( [timeout] )
其中,thread 为 Thread 类或其子类的实例化对象;timeout 参数作为可选参数,其功能是指定 thread 线程最多可以霸占 CPU 资源的时间(以秒为单位),如果省略,则默认直到 thread 执行结束(进入死亡状态)才释放 CPU 资源。
举个例子,修改上面的代码,如下所示:
import threading#定义线程要调用的方法,*add可接收多个以非关键字方式传入的参数
def action(*add):
for arc in add:
#调用 getName() 方法获取当前执行该程序的线程名
print(threading.current_thread().getName() +" "+ arc)
#定义为线程方法传入的参数
my_tuple = ("http://c.biancheng.net/python/",\
"http://c.biancheng.net/shell/",\
"http://c.biancheng.net/java/")
#创建线程
thread = threading.Thread(target = action,args =my_tuple)
#启动线程
thread.start()
#指定 thread 线程优先执行完毕
thread.join()
#主线程执行如下语句
for i in range(5):
print(threading.current_thread().getName())
程序执行结果为:
Thread-1 http://c.biancheng.net/python/Thread-1 http://c.biancheng.net/shell/
Thread-1 http://c.biancheng.net/java/
MainThread
MainThread
MainThread
MainThread
MainThread
程序中第 16 行的位置,thread 线程调用了 join() 方法,并且没有指定具体的 timeout 参数值。这意味着如果程序想继续往下执行,必须先执行完 thread 线程。
http://c.biancheng.net/python/thread/