一、什么是线程
线程是操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程。
线程顾名思义,就是一条流水线工作的过程(流水线的工作需要电源,电源就相当于cpu),而一条流水线必须属于一个车间,一个车间的工作过程是一个进程,车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一条流水线。
所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
多线程(即多个控制线程)的概念是,在一个进程中存在多个线程,多个线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
二、线程与进程的区别
1.线程共享创建它的进程的地址空间;进程有自己的地址空间。
2.线程可以直接访问其进程的数据段;进程有自己的父进程的数据段副本。
3.线程可以直接与进程的其他线程通信;进程必须使用进程间通信与同级进程通信。
4.新线程很容易创建;新进程需要复制父进程。
5.线程可以对同一进程的线程进行相当大的控制;进程只能对子进程进行控制。
6.对主线程的更改(取消、优先级更改等)可能会影响进程的其他线程的行为;对父进程的更改不会影响子进程。
三、多线程应用举例
开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。
只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
四、threading模块介绍
之前讲进程的时候介绍过multiprocess包,threading包的接口跟mulitprocess包的接口是类似的,二者在使用层面,有很大的相似性。
Thread
threading包中的Thread模块可以创建线程。
先看一下源码:
class Thread:
"""A class that represents a thread of control.
表示控制线程的类。
This class can be safely subclassed in a limited fashion.
这个类可以安全地以有限的方式进行子类化。
There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding the run() method in a subclass.
有两种方法可以指定活动:将可调用对象传递给构造函数,或重写子类中的run()方法。
"""
类的头注释表明了两种创建线程的方式:
1.通过Thread类直接实例化对象,给对象传递构造函数
2.继承Thread类并重写子类的run方法
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
assert group is None, "group argument must be None for now"
if kwargs is None:
kwargs = {}
self._target = target
self._name = str(name or _newname())
self._args = args
self._kwargs = kwargs
if daemon is not None:
self._daemonic = daemon
else:
self._daemonic = current_thread().daemon
self._ident = None
self._tstate_lock = None
self._started = Event()
self._is_stopped = False
self._initialized = True
# sys.stderr is not stored in the class like
# sys.exc_info since it can be changed between instances
self._stderr = _sys.stderr
# For debugging and _after_fork()
_dangling.add(self)
This constructor should always be called with keyword arguments.
应该始终使用关键字参数调用此构造函数。
Arguments are:
参数为:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
group应该为None;在实现ThreadGroup类时保留为将来的扩展。
target is the callable object to be invoked by the run() method.
target是run()方法要调用的可调用对象。
Defaults to None, meaning nothing is called.
默认为“无”,表示不调用任何内容。
name is the thread name.
name是线程名。
By default, a unique name is constructed of the form “Thread-N” where N is a small decimal number.
默认情况下,一个唯一的名称由“Thread-N”构成,其中N是一个小的十进制数。
args is the argument tuple for the target invocation. Defaults to ().
args是目标调用的参数元组。默认为元组。
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
kwargs是目标调用的关键字参数字典。默认为字典。
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
如果子类重写构造函数,则必须确保在对线程执行其他操作之前调用基类构造函数(Thread.__init__())。
好了,现在我们知道创建线程的参数了,接下来就可以开始coding了。
Thread创建线程的两种方式
1.通过Thread类直接实例化对象,给对象传递构造函数
import time
from threading import Thread
def task(thread_name):
print(thread_name, "is start:", time.asctime(time.localtime(time.time())))
time.sleep(1)
print(thread_name, "is done:", time.asctime(time.localtime(time.time())))
if __name__ == '__main__':
thread1 = Thread(target=task, kwargs={"thread_name": "Thread 1"})
thread1.start()
print("Main thread is done:", time.asctime(time.localtime(time.time())))
输出结果为:
Thread 1 is start: Fri Feb 7 16:08:11 2020
Main thread is done: Fri Feb 7 16:08:11 2020
Thread 1 is done: Fri Feb 7 16:08:12 2020
2.继承Thread类并重写子类的run方法
import time
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
super(MyThread, self).__init__()
self.name = name
def run(self):
print(self.name, "is start:", time.asctime(time.localtime(time.time())))
time.sleep(2)
print(self.name, "is done:", time.asctime(time.localtime(time.time())))
if __name__ == '__main__':
MyThread("Thread 1").start()
print("Main thread is done:", time.asctime(time.localtime(time.time())))
输出结果为:
Thread 1 is start: Fri Feb 7 16:09:49 2020
Main thread is done: Fri Feb 7 16:09:49 2020
Thread 1 is done: Fri Feb 7 16:09:51 2020
输出的结果似乎与创建子进程的时候差不多,不过你仔细看看,我们在主程序的最后一行输出Main thread is done,但是在终端上的输出确实在第二行,第一行输出的是Thread 1 is start,这说明在thread 1开启的一瞬间,线程中的代码就开始执行了,开启速度超快的。
咱们来对比一下进程的开启速度:
import time
from threading import Thread
from multiprocessing import Process
def task(name):
print(name, " is start: ", time.asctime(time.localtime(time.time())))
time.sleep(2)
print(name, " is done: ", time.asctime(time.localtime(time.time())))
if __name__ == '__main__':
process1 = Process(target=task, kwargs={"name": "Process 1"})
process1.start()
print("Under process 1.")
thread1 = Thread(target=task, kwargs={"name": "Thread 1"})
thread1.start()
print("Under thread 1.")
输出结果为:
Under process 1.
Thread 1 is start: Fri Feb 7 16:43:08 2020
Under thread 1.
Process 1 is start: Fri Feb 7 16:43:08 2020
Thread 1 is done: Fri Feb 7 16:43:10 2020
Process 1 is done: Fri Feb 7 16:43:10 2020
Process finished with exit code 0
我们首先是开启了一个进程process 1,之后立马在屏幕上打印Under Process 1,这时候process 1还没有开始执行,然后又开启了一个现成thread 1,开启之后立马打印了Thread 1 is start,说明thread 1已经开始执行,到此为止进程process 1还是没有开始执行,之后打印的Under thread 1,线程已经开启完毕,再之后才打印Process 1 is start,这才是process 1开始执行的标志,由此说明,开启进程的开销远大于开线程。
结果形成鲜明的对比,在开启线程的速度比进程很多。
我们说进程就好像一间工厂干活,开的工厂多干的活也多,线程就好像工厂里的流水线,开的流水线越多干的话也越多,那么,理所当然的,新开一个流水线的代价肯定远小于新开一间工厂的代价,所有线程的开启速度比进程快。
既然线程是进程开启的,那么在线程中查看PID的话,应该是跟进程一样的,我们来验证一下:
import os
import time
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
super(MyThread, self).__init__()
self.name = name
def run(self):
print(self.name, "PID =", os.getpid())
if __name__ == '__main__':
MyThread("Thread 1").start()
time.sleep(0.1)
print("Main process PID =", os.getpid())
输出结果为:
Thread 1 PID = 2908
Main process PID = 2908
说明线程的PID跟开启线程的进程PID是一样的。
最后再来看一下同一进程内的线程是否共享该进程的数据?
from threading import Thread
from multiprocessing import Process
number = 100
def task(name):
global number
number = 70
print(name, "number =", number)
if __name__ == '__main__':
p1 = Process(target=task, args=("Process 1",))
p1.start()
p1.join()
print("Under process 1, number =", number)
t1 = Thread(target=task, args=("Thread 1",))
t1.start()
t1.join()
print("Under thread 1, number =", number)
输出结果为:
Process 1 number = 70
Under process 1, number = 100
Thread 1 number = 70
Under thread 1, number = 70
在线程中更改number的值之后,进程中查看,number也被更改了,说明进程剀切线程共享该进程的数据空间。
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
import threading
from threading import Thread
def task():
import time
time.sleep(2)
print("Thread name =", threading.current_thread().getName())
if __name__ == '__main__':
thread = Thread(target=task)
thread.start()
print("Before join thread is alive:", thread.is_alive())
thread.join()
print("After join thread is alive:", thread.is_alive())
print("Threading enumerate:", threading.enumerate())
print("Threading active count:", threading.active_count())
print("MainThread:", threading.current_thread())
print("MainThread name:", threading.current_thread().getName())
输出结果为:
Before join thread is alive: True
Thread name = Thread-1
After join thread is alive: False
Threading enumerate: [<_MainThread(MainThread, started 3868)>]
Threading active count: 1
MainThread: <_MainThread(MainThread, started 3868)>
MainThread name: MainThread
五、守护线程
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁。
需要强调的是:运行完毕并非终止运行,对主进程来说,运行完毕指的是主进程代码运行完毕,对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收),因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
来看一个例子验证一下我们的说法:
import time
from threading import Thread
def thread1():
print("Thread 1 is start.")
time.sleep(1)
print("Thread 1 is done.")
def thread2():
print("Thread 2 is start.")
time.sleep(2)
print("Thread 2 is done.")
if __name__ == '__main__':
thread1 = Thread(target=thread1)
thread2 = Thread(target=thread2)
thread2.daemon = True
thread1.start()
thread2.start()
print("Under thread 1 and thread 2 start.")
输出结果为:
Thread 1 is start.
Thread 2 is start.
Under thread 1 and thread 2 start.
Thread 1 is done.
thread2是守护线程,也就是说thread2会等到除主线程之外的非守护线程全死了之后才死,thread1睡一秒,就结束,而thread2要睡两秒,所以当thread2还在沉睡的时候thread1就已经死了,非守护进程thread1死了之后,主线程也跟着死了,主线程一死,守护线程thread2也得死,所以Thread 2 id done没有输出。
练习题
1:编写一个简单的文本处理工具,具备三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
from threading import Thread
from multiprocessing import Queue
def read(q):
while True:
msg = input(">>> ").strip()
q.put(msg)
if msg == "exit":
break
def upper(q):
while True:
msg = q.get().upper()
q.put(msg)
if msg == "EXIT":
break
def dump(q):
while True:
msg = q.get()
if msg == "EXIT":
break
with open("练习题.txt", "a+", encoding="utf-8") as f:
f.write(msg + "
")
if __name__ == '__main__':
queue = Queue()
read_thread = Thread(target=read, kwargs={"q": queue})
read_thread.start()
upper_thread = Thread(target=upper, kwargs={"q": queue})
upper_thread.start()
dump_thread = Thread(target=dump, kwargs={"q": queue})
dump_thread.start()
read_thread.join()
upper_thread.join()
dump_thread.join()
输出结果为:
>>> Alex
>>> Coco
>>> exit
Process finished with exit code 0
文件内容为:
ALEX
COCO