进程与线程
一个程序至少有一个进程,一个进程至少有一个线程,最终是线程在工作。
- 进程:计算机资源分配的最小单元(进程为线程提供资源)
- 线程:计算机中可以被CPU调度的的最小单元(真正的工作)
- 一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源
多线程:以下载抖音视频为例
import time
import requests
import threading
url_list = [
('1.mp4', 'https://www.xxxxxx.......'),
('2.mp4', 'https://www.xxxxxx.......'),
('3.mp4', 'https://www.xxxxxx.......')
]
def task(file_name, video_urlk):
res = requests.get(video_url)
with open(file_name, mode='wb') as f:
f.write(res.content)
print(time.time())
print(time.time())
for name, url in url_list:
# 创建线程,让每个线程都去执行task函数
t = threading.Thread(target=task, atgs=(name, url))
t.start
t = threading.Thread(target=函数名, args=(arg1, arg2, ...))创建了一个线程对象,执行的任务是target参数的值(一个函数),args是执行任务所需的参数。t.start()创建的线程开始工作。
多进程
import time
import requests
import multiprocessing
url_list = [
('1.mp4', 'https://www.xxxxxx.......'),
('2.mp4', 'https://www.xxxxxx.......'),
('3.mp4', 'https://www.xxxxxx.......')
]
def task(file_name, video_urlk):
res = requests.get(video_url)
with open(file_name, mode='wb') as f:
f.write(res.content)
print(time.time())
if __name__=="__main__":
print(time.time())
for name, url in url_list:
p = multiprocessing.Process(target=task, args=(name, url))
p.start()
p = multiprocessing.Process(target=函数名, args=(arg1, arg2, ...))创建一个进程,执行target传参的任务。注意创建进程之后,还会创建一个线程。p.start()开始执行进程。
注意Linux系统支持fork创建进程,Windows系统支持spawn,Mac既支持fork也支持spawn(Python3.8默认设置为spawn)。所以Windows下需要加上if __name__=="__main__".
多进程比多线程的开销要大。
GIL锁
GIL,全局解释器锁,是CPython解释器特有的一个东西,让一个进程中同一时刻只能有一个线程被CPU调度。
如果程序想利用计算机的多核优势,则应该选择多进程帮助开发;如果不利用计算机的多核优势,适合选择多线程开发。
常见程序开发中,计算操作需要使用CPU多核优势,IO操作不需要利用CPU多核优势,所以:
- 计算密集型任务,用多进程,例如大量的数据计算
- IO密集型任务,用多线程,例如文件读写,网络数据传输等
多线程开发
import threading
def task(arg):
pass
# 创建一个Thread的实例,并封装线程被CPU调度时应该执行的任务与相关参数
# threading.Thread创建的是子线程,主线程负责从上到下执行代码
t = threading.Thread(target=task, args=('xxx', ))
# 线程准备就绪(等待被CPU调度),代码继续向下执行
t.start()
print("继续执行...") # 主线程执行完所有代码,不结束;等待子线程
threading.Thread类有两种方法可以指定活动:将可调用对象传递给构造函数,或者重写子类中的run()方法。子类中不应重写任何其他方法(构造函数除外)。换句话说,只重写这个类的__init__()和run()方法。
当线程对象一旦被创建,其活动必须通过调用线程的 start() 方法开始。 这会在独立的控制线程中发起调用 run() 方法。
一旦线程活动开始,该线程会被认为是 '存活的' 。当它的 run() 方法终结了(不管是正常的还是抛出未被处理的异常),就不是'存活的'。 is_alive() 方法用于检查线程是否存活。
其他线程可以调用一个线程的 join() 方法。这会阻塞调用该方法的线程,直到被调用 join() 方法的线程终结。
线程有名字。名字可以传递给构造函数,也可以通过 name 属性读取或者修改。
如果 run() 方法引发了异常,则会调用 threading.excepthook() 来处理它。 在默认情况下,threading.excepthook() 会静默地忽略 SystemExit。
class threading.Thread(*group=None, target=None, name=None, args=(), kwargs={}, \*, daemon=None*)
- group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
- target 是用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。
- name 是线程名称。默认情况下,由 "Thread-N" 格式构成一个唯一的名称,其中 N 是小的十进制数。
- args 是用于调用目标函数的参数元组。默认是 ()。
- 如果不是
None,daemon 参数将显式地设置该线程是否为守护模式。 如果是None(默认值),线程将继承当前线程的守护模式属性。守护线程会随着主线程的结束而消失,当没有存活的非守护线程时,整个Python程序才会退出。 - 如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(
Thread.__init__())。
线程的常见方法
threading.Thread.start():当前线程准备就绪,等待CPU调度,但是具体时间由CPU来决定。它在一个线程里最多只能被调用一次。 它安排对象的 run() 方法在一个独立的控制线程中被调用。如果同一个线程对象中调用这个方法的次数大于一次,会抛出 RuntimeError 。
import threading
loop = 10000000
number = 0
def _add(count):
global number
for i in range(count):
number += i
t = threading.Thread(target=_add, args=(loop, ))
t.start()
print(number)
这里输出number的值是不确定的,因为主线程和子线程之间来回切换,主线程运行print(number)时,无法确定子线程计算到了哪一步。
threading.Thread.join(timeout=None):等待当前线程执行完毕后,再继续向下运行。这会阻塞调用这个方法的线程,直到被调用join()的线程终结 -- 不管是正常终结还是抛出未处理异常 -- 或者直到发生超时,超时选项是可选的。
import threading
loop = 10000000
number = 0
def _add(count):
global number
for i in range(count):
number += i
t = threading.Thread(target=_add, args=(loop, ))
t.start()
t.join() # 主线程等待中
print(number)
现在主线程输出的number的值是固定的,因为当主线程执行print(number)时,子线程已经完成了全部计算。
import threading
loop = 10000000
number = 0
def _add(count):
global number
for i in range(count):
number += i
def _sub(count):
global number
for i in range(count):
number -= i
t1 = threading.Thread(target=_add, args=(loop, ))
t2 = threading.Thread(target=_sub, args=(loop, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(number)
这里主线程最终输出的number仍然是不确定的,因为当t1、t2两个线程就绪之后就开始执行,两个线程对同一资源(number)操作导致number结果会出现混乱,例如当t2执行number -= i时,切换到t1执行了number += i,可是再切换回t2线程时,number没有同步更新,number -= i操作中的number不是执行了number += i后的number,所以最终number的值不确定。
threading.Thread.run():代表线程活动的方法。你可以在子类型里重载这个方法。 标准的run()方法会对作为 target 参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从 args 和 kwargs 参数分别获取的位置和关键字参数。threading.Thread.is_alive():返回线程是否存活。当run()方法刚开始直到run()方法刚结束,这个方法返回True。模块函数enumerate()返回包含所有存活线程的列表。- 继承
threading.Thread,自定义线程类
import requests
import threading
class DouYinThread(threading.THread):
def run(self):
file_name, video_url = self._args
res = requests.get(video_url)
with open(file_name, mode='wb') as f:
f.write(res.content)
url_list = [
('1.mp4', 'https://www.xxxxxx.......'),
('2.mp4', 'https://www.xxxxxx.......'),
('3.mp4', 'https://www.xxxxxx.......')
]
for file_name, video_url in url_list:
t = DouYinThread(args=(file_name, video_url))
t.start()
线程安全
一个进程中可以有多个线程,且线程共享进程的所有资源。多个线程同时去操作一个资源,可能会存在数据混乱的情况。
import threading
lock = threading.Rlock() # 创建线程锁
loop = 10000000
number = 0
def _add(count):
lock.acquire() # 申请锁,没有申请成功会等待其他线程释放锁
global number
for i in range(count):
number += i
lock.release() # 释放锁
def _sub(count):
lock.acquire() # 申请锁
global number
for i in range(count):
number -= i
lock.release() # 释放锁
t1 = threading.Thread(target=_add, args=(loop, ))
t2 = threading.Thread(target=_sub, args=(loop, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(number)
也可以通过上下文管理的机制获取/释放锁。
import threading
num = 0
lock = threading.Rlock()
def task():
with lock: # 基于上下文管理,内部制动执行acquire()和release()
global num
for i in range(10000000):
num += 1
print(num)
for i in range(2):
t = threading.Thread(target=task)
t.start()
有些数据类型是线程安全的,在操作它们的时候,它们内部自带锁。所以在操作它们的时候不需要去获取/释放锁。
线程锁
线程加锁一般有两种:Lock同步锁,Rlock递归锁。区别是Lock不支持锁的嵌套,但是Rlock支持;Lock的效率比Rlock高。
import threading
lock = threading.Rlock()
def func_A():
with lock:
do_something
def func_B():
...
func_A()
...
def func_C():
with lock:
...
func_A()
...
如果上述的三个函数由三个不同的程序员开发,func_C中上锁,而func_C中引用的func_A在执行过程中也上了锁,这种情况就是锁的嵌套,只能使用递归锁Rlock。
死锁
由于竞争资源或者彼此通信而造成的一种阻塞状态。
线程池
线程不是开的越多越好,开多了线程会导致系统性能降低,尤其是不能无限制地创建线程。
import time
# 引入线程池
from concurrent.futures import ThreadPoolExecutor
def task(video_url):
do_something
url_list = [f"www.xxx.com/?page={i}" for in in range(100)]
# 创建了一个线程池,最多维护10个线程
pool = THreadPoolExecutor(10)
for url in url_list:
# 在线程池提交一个任务。如果线程池中有空闲的线程,则分配一个线程去执行,执行完毕后再将线程交还线程池
# 如果没有空闲线程,则等待其他线程执行完毕
pool.submit(task, url)
pool.shutdown(True) :等待线程池的任务执行完毕后,再继续执行。
执行完任务,再干点别的事情。不如task执行下载,done将下载的数据写入本地。
def done(args):
do_something
for url in url_list:
# 线程池的函数先执行task,完毕后再执行done函数
future = pool.submit(task, url)
future.add_done_callback(done)
也可以统一获取全部线程池的结果
future_list = []
for url in url_list:
future = pool,submit(task.url)
future.append(future)
pool.shutdown(True)
for future in future_list:
print(future.result())
单例模式
单例模式:每次实例化类的对象时,都是最开始创建的那个对象,不再重复创建对象。