Python学习之线程(5)
1.线程与进程
1.1 简介
当我们运行一个xxx.py时,内部就会创建一个进程(主进程),在进程中又会创建一个线程(主线程),由线程逐行执行代码
线程是计算机可以被CPU调度的最小单位
进程是计算机资源分配的最小单元,进程为线程提供资源
一个进程中可以创建多个线程,同一个进程中的线程可以共享此进程中的所有资源
串行:多个任务排队一个一个按顺序执行
并发:假如多个任务,只有一个CPU,那么在同一个时刻只能执行一个任务
并行:假如多个任务,有多个CPU,那么同一时刻每个CPU都执行任务,这样多个任务就能达到同时执行的效果
1.2 案例
有三个文件需要下载,若是平时,则直接通过循环一个一个下载,如下:
import time
import requests
url_list = [("F4.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video id=v0300f570000bvbmace0gvch71053oog"),
("aa.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video id=v0200f3e0000bv52fpn5t6p007e34qlg"),
("mvp.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajgg")]
print(time.time())
for name, url in url_list:
res = requests.get(url)
with open(name,mode='wb') as f:
f.write(res.content)
print(name,time.time())
1.3 选择
常见的程序开发中,计算操作需要使用CPU多核优势,I0操作不需要利用CPU的多核优势,所以,就有这一句话:
- 计算密集型:用多进程,例如:大量的数据计算【累加计算示例】。
- IO密集型:用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。
如下图,多进程开发,能够充分利用CPU的性能,一个进程会默认创建一个线程,因为线程是最小执行单位,而一个进程只会占用一个核,所以多进程能占用多核,而多个线程只能占用一个核 可以查看 进程和cpu的关系
2.线程
2.1 案例
上面例子是通过串行一个一个下载,下面我们通过现场下载,如下:
import time
import requests
import threading
url_list = [("F4.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video id=v0300f570000bvbmace0gvch71053oog"),
("aa.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video id=v0200f3e0000bv52fpn5t6p007e34qlg"),
("mvp.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajgg")]
def task(file_name, video_url):
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, args=(name,url))
t.start()
2.2 多线程开发
2.2.1 start()
当前线程准备就绪(即等待CPU调度,具体时间由CPU决定)
格式如下:
import threading
def task(arg):
pass
# 创建一个Thread对象(线程),并封装线程被CPU调度时应该执行的任务和相关参数。
t = threading.Thread(target=task, args=( 'xxx',))
# 线程准备就绪(等待CPU调度),代码继续向下执行。
t.start()
print('继续执行...')# 主线程执行完所有代码,不结束(等待子线程)
2.2.2 join
等待当前线程的任务执行完毕后,再继续向下执行
import threading
def task(arg):
pass
t = threading.Thread(target=task, args=( 'xxx',))
t.start()
t.join() # 子线程插入先执行,主线程继续等待,等到子线程执行完,才继续往下走
print('继续执行...')# 主线程执行完所有代码,不结束(等待子线程)
2.2.3 setDaemon
t.setDaemon(布尔值),守护线程(必须放在start之前)
- t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
- t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
import threading
def task(arg):
pass
t = threading.Thread(target=task, args=( 'xxx',))
t.setDaemon(True) #设置成后台运行,那么主线程不再等待
t.start()
print('结束...')# 主线程执行完,整个进程结束,不再等待子线程
2.2.4 线程名称获取与设置
import threading
def task(arg):
name = threading.current_thread().getName()
print(name)
pass
t = threading.Thread(target=task, args=( 'xxx',))
t.setName("线程A")
t.start()
2.2.5 自定义线程
class MyTread(threading.Thread):
def run(self):
print("test")
t = MyTread()
t.start()
2.3 线程安全
多个线程同时操作一个数据的时候,可能会出现该数据混乱的情况
可以使用线程锁控制:threading.RLock
注意:当有些工具类标注是线程安全的,那么我们可以直接使用,而不用自己再加锁去控制
import threading
lock_obj = threading.RLock()
loop = 10000000
number = 0
def add(count):
lock_obj.acquire() # 获取锁 当此线程获取到锁后,其他线程只能等着
global number
for i in range(count):
number += 1
lock_obj.release() # 释放锁
def sub(count):
lock_obj.acquire()
global number
for i in range(count):
number -= 1
lock_obj.release()
t1 = threading.Thread(target=add,args=(loop,))
t2 = threading.Thread(target=sub,args=(loop,))
t1.start()
t2.start()
t1.join()#t1线程执行完毕,才继续往后走
t2.join()#t2线程挤行完毕,才继续往后走
print(number)
上面的锁需要手动增加和关闭,太麻烦了,可以使用with语句来自动关闭锁,如下:
def task():
with lock_obj:
global num
for i range(1000)
num += 1
print(num)
for i in range(3):
t = threading.Thread(target=task)
t.start()
2.4 线程锁
在程序中,如果想要手动添加锁,那么一般有两种:Lock和RLock
2.4.1 Lock
同步锁,该锁不能嵌套使用
import threading
lock_obj = threading.Lock()
loop = 10000000
number = 0
def add(count):
lock_obj.acquire() # 获取锁 当此线程获取到锁后,其他线程只能等着
global number
for i in range(count):
number += 1
lock_obj.release() # 释放锁 释放后,其他线程再竞争
for i in range(3):
t = threading.Thread(target=add,args=(loop,))
t.start()
print(number)
2.4.2 RLock
递归锁,可以嵌套使用
import threading
lock_obj = threading.RLock()
loop = 10000000
number = 0
def add(count):
lock_obj.acquire() # 获取锁 当此线程获取到锁后,其他线程只能等着
lock_obj.acquire() # 再次获取
global number
for i in range(count):
number += 1
lock_obj.release() # 释放锁 释放后,其他线程再竞争
lock_obj.release()
for i in range(3):
t = threading.Thread(target=add,args=(loop,))
t.start()
print(number)
2.5 死锁
就是线程对锁之间的相互竞争,本身自己的锁没有释放,又要获取对方的锁,而对方锁也不释放,又要获取自己的锁,从而导致死锁
import threading
import time
lock_1=threading.Lock()
lock_2=threading.Lock()
def task1():
lock_1.acquire()
time.sleep(1)
lock_2.acquire() # 需要获取lock2的锁
print(11)
lock_2.release()
print(111)
lock_1.release()
print(1111)
def task2():
lock_2.acquire()
time.sleep(1)
lock_1.acquire() # 需要获取lock1的锁
print(22)
lock_1.release()
print(222)
lock_2.release()
print(2222)
t1=threading.Thread(target=task1)
t1.start()
t2=threading.Thread(target=task2)
t2.start()
2.6 GIL锁
GIL(全局解释器锁):是Cpython解释器特有的一个锁,让一个进程中同一个时刻只能由一个线程被CPU调用
如果想利用计算机多核优势,让CPU同时处理一些任务,那么可以使用多进程开发
2.7 线程池
Python3中官方正式提供了线程池
2.7.1 创建线程池
import time
from concurrent.futures import ThreadPoolExecutor
def task(video_ur1,num):
print("开始执行任务", video_ur1)
time.sleep(5)
# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)
url_list=["www.xxxx-{}.com".f .format(i) for i in range(300)]
for url in url_list:
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
pool.submit(task, url, 2)
print("END")
2.7.2 等待线程池执行完毕
import time
from concurrent.futures import ThreadPoolExecutor
def task(video_ur1,num):
print("开始执行任务", video_ur1)
time.sleep(5)
# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)
url_list=["www.xxxx-{}.com".f .format(i) for i in range(300)]
for url in url_list:
pool.submit(task, url, 2)
print("执行中...")
pool.shutdown(True) # 等待线程池中的任务执行完毕后,再往下继续执行
print("END")
2.7.3 执行其他任务
import time
import random
from concurrent.futures import ThreadPoolExecutor,Future
def task(video_url):
print("开始执行任务", video_url)
time.sleep(2)
return random.randint(0,10)
def done(response):
print("任务执行后的返回值", response.result())
#创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)
url_list=["www.xxxx-{}.com".format(i) for i in range(15)]
for url in url_list:
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
future=pool.submit(task,url)
future.add_done_callback(done) #是 子主线程执行
# 可以做分工,例如:task专门下载,done专门将下载的数据写入本地文件。
2.7.4 最后统一获取结果
import time
import random
from concurrent.futures import ThreadPoolExecutor,Future
def task(video_url):
print("开始执行任务",video_url)
time.sleep(2)
return random.randint(0,10)
#创建线程池,最多维护10个线程。
pool=ThreadPoolExecutor(10)
future_list=[]
url_list=["www.xxxx-{}.com".format(i)for i in range(15)]
for url in url_list:
#在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
future =pool.submit(task,url)
future_list.append(future)
pool.shutdown(True)
for fu in future_list:
print(fu.result())
3.进程
进程与进程之间是相互隔离
多进程可以充分利用CPU的多核优势
3.1 进程三大模式
多进程启动如下:
import multiprocessing
def task ():
pass
if __name__ == 'main':
p1 = multiprocessing.Process (target=task)
p1.start ()
注意:使用多进程,不同的操作系统环境,启动的要求可能不一样,需要按照下面三种模式说明来启动
3.2 常见属性
同 2.2 多线程开发 类似
3.3 进程间数据共享
进程之间的资源是独立维护的,所以主进程和子进程的资源都是独立的,甚至子进程拷贝了主进程的所有资源,也是各自维护的
3.3.1 shared
不常用
3.3.2 Server process
常用
3.3.3 Queues
常用
3.3.4 Pipes
不常用
3.4 进程锁
如果多个进程抢占式去执行某些操作的时候,为了防止操作出问题,可以通过进程锁来控制
import time
import multiprocessing
def task (lock):
print("开始")
lock.acquire ()
#假设文件中保存的内容就是一个值: 10
with open ('fl.txt', mode='r', encoding='utf-8') as f:
current_num = int (f.read ())
print("排队抢票了")
time.sleep(0.5)
current_num -= 1
with open ('fl.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))
lock.release ()
if __name__ == '__main__':
multiprocessing.set_start_method("spawn")
lock = multiprocessing.RLock ()
# 进程锁
for i in range (10):
p = multiprocessing.Process (target=task, args=(lock, ))
p.start()
# spawn模式,需要特殊处理。
time.sleep(7)
3.5 进程池
相关操作类似 2.7 线程池
import time
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
def task(num):
print("执行", num)
time.sleep(2)
if __name__ == '__main__':
pool = ProcessPoolExecutor(4)
for i in range(10):
pool.submit(task,i)
print(1)
print(2)
参考
1.全网最新教学Python3.10网络编程与进程&线程全家桶
3.Python 并发编程(三)对比(multiprocessing, threading, concurrent.futures, asyncio)