Python学习之线程(5)

138 阅读8分钟

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的关系

image-20221103150606689

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调用

image-20221103150806935

如果想利用计算机多核优势,让CPU同时处理一些任务,那么可以使用多进程开发

image-20221103150606689

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 ()

注意:使用多进程,不同的操作系统环境,启动的要求可能不一样,需要按照下面三种模式说明来启动

image-20221103153147084

3.2 常见属性

2.2 多线程开发 类似

3.3 进程间数据共享

进程之间的资源是独立维护的,所以主进程和子进程的资源都是独立的,甚至子进程拷贝了主进程的所有资源,也是各自维护的

3.3.1 shared

不常用

image-20221103153834219

3.3.2 Server process

常用

image-20221103153925436

3.3.3 Queues

常用

image-20221103154026264

3.3.4 Pipes

不常用

image-20221103154059870

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网络编程与进程&线程全家桶

2.一篇文章搞定Python多进程

3.Python 并发编程(三)对比(multiprocessing, threading, concurrent.futures, asyncio)

4.Python多线程/进程(threadin…

5.每周一个 Python 模块 | threading