一、进程和线程
先来了解一下进程和线程
类比:
- 一个工厂,至少有一个车间,一个车间中至少有一个工人,最终是工人在工作。
- 一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作。
上述串行的代码示例就是一个程序,在使用python xx.py 运行时,内部就创建了一个进程(主进程),在进程中创建了一个线程(主线程),由线程逐行运行代码。
线程,是计算机中可以被cpu调度的最小单元,
进程,是计算机资源分配的最小单元(进程为线程提供资源)
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
在Python中,多线程和多进程是用来实现并发执行的两种机制。这些系统调用(fork、spawn)是底层操作系统提供的方式,可以用来创建新的进程或者线程。
1、linux系统fork;
在Linux系统中,fork是一种创建进程的方式,它会复制当前进程的状态创建一个新的进程。新的进程称为子进程,而原始进程称为父进程。fork系统调用返回两次,一次在父进程中返回子进程的PID,一次在子进程中返回0。子进程会继承父进程的代码段、数据段、堆栈等信息。
2、window系统:spawn;
在Windows系统中,spawn是一种创建进程的方式,它通过调用CreateProcess函数来创建一个新的进程。与fork不同,spawn在Windows系统中不能直接复制当前进程的状态,而是需要指定要执行的程序路径。CreateProcess函数会创建一个新的进程,并且可以指定该进程的参数、环境变量等信息。
3、mac系统:fork和spawn (python3.8默认支持spawn)要是需要修改fork: multiprocessing.set_start_method(‘fork’)
在Mac系统中,fork和spawn两种方式都可以用来创建进程。fork与Linux中的fork类似,复制当前进程的状态创建新的进程。而spawn与Windows中的spawn类似,通过调用posix_spawn函数创建新的进程。
对于Python的多线程和多进程,可以使用threading和multiprocessing模块来实现。threading模块提供了线程相关的功能,可以创建和管理线程。multiprocessing模块则提供了多进程相关的功能,可以创建和管理进程。
因此,无论是在Linux、Windows还是Mac系统中,都可以使用fork或spawn来创建新的进程,并结合Python的多线程或多进程机制来实现并发执行的效果。具体选择哪种方式取决于所使用的操作系统和需求。
1. GIL锁
全局解释器锁(Global Interpreter Lock),是CPython解释器的功能,让一个进程中同一时刻只能有一个线程可以被CPU调用。
如果程序想利用计算机多核优势,让CPU同时处理一些任务,适合用多进程开发(即使资源开销大)。
如果程序不利用计算机的多核优势,适合用多线程开发
常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要利用多核优势,所以,就有一句话:
- 计算密集型,用多进程,例如:大量的数据计算【累加计算实例】。
- IO密集型,用多线程,例如:文件读写,网络数据传输【下载抖音视频实例,爬虫】。
2. 线程开发
线程的常见方法
-
t.start(),当前线程准备就绪(等待CPU调度,具体时间是由CPU来决定)。
-
t.join(),等待当前线程的任务执行完毕后再向下继续执行。主线程等待子线程执行完毕在继续向下执行。
-
t.setDaemon(布尔值)守护线程(必须放在start之前)
- t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
- t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
-
线程名称的设置和获取 设置名字在start之前
import threading def task(srd): # 获取当前执行此代码的线程 name = threading.current_thread().getName() print(name) for i in range(10): t = threading.Thread(target=task, args=(11,)) # 设置名字 t.setName("日魔-{}".format(i)) t.start()
-
自定义线程类,直接将线程需要做的是写到run方法中。
import time 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 = [ ("东北F4模仿秀.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"), ("卡特扣篮.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"), ("罗斯mvp.mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajgg") ] for name,url in url_list: t = DouYinThread(args=(name,url)) t.start()
3. 线程安全
import threading
lock_object = threading.RLock()
loop = 10000000
number = 0
def _add(count):
lock_object.acquire() # 加锁(申请锁,没申请到则等待)
global number
for i in range(count):
number += 1
lock_object.release() # 释放锁
def _sub(count):
lock_object.acquire()
global number
for i in range(count):
number -= 1
lock_object.release()
t = threading.Thread(target=_add, args=(loop,))
t1 = threading.Thread(target=_sub, args=(loop,))
t.start()
t1.start()
t.join()
t1.join()
print(number)
使用 with关键字加锁
import threading
num = 0
lock_object = threading.RLock()
# def task():
# print("开始")
# lock_object.acquire()
# global num
# for i in range(1000000):
# num += 1
# lock_object.release()
# print(num)
# for i in range(2):
# t = threading.Thread(target=task)
# t.start()
def task1():
print("开始")
with lock_object:
global num
for i in range(1000000):
num += 1
print(num)
for i in range(2):
t = threading.Thread(target=task1)
t.start()
-
对于有些数据是安全的数据类型不需要加锁:
- list.append(x)
- list.extend(l2)
- x = list[i]
- x = pop()
- list[i:j] = list2
- list.sort()
- x =y
- x.feadf = y
- dict[x] = y
- dict1.update(dict2)
- dict.keys()
-
不安全的数据类型操
- i += 1
- list1[i] = list1[j]
- dict[x] = dict[x]+1
4. 线程锁
在程序中如果想要自己手动加锁,一般有两种:Lock和RLock。
- Lock 同步锁(不支持锁的嵌套)比较RLock,Lock的效率更高,性能更好
- RLock 递归锁 (支持锁的嵌套)应用场景比较多,比如你写了一个函数用到锁了,然后你同事也写了一个函数也遇到锁了并调用你的函数时就遇到嵌套锁,所以遇到这种嵌套的就要用RLock
5. 死锁
多线程和进程的死锁是并发编程中常见的问题,它们指的是在多个线程或进程中相互等待对方释放资源而无法继续执行的情况。
- 多线程死锁: 多线程死锁指的是两个或多个线程因为争夺资源而相互等待,导致程序无法继续执行下去。通常出现死锁的情况需要满足以下四个条件:互斥、占有且等待、不可抢占和循环等待。 示例代码如下:
import threading
# 创建资源对象
resourceA = threading.Lock()
resourceB = threading.Lock()
# 线程1获取资源A后,尝试获取资源B
def thread1():
resourceA.acquire()
resourceB.acquire()
# 执行操作...
resourceB.release()
resourceA.release()
# 线程2获取资源B后,尝试获取资源A
def thread2():
resourceB.acquire()
resourceA.acquire()
# 执行操作...
resourceA.release()
resourceB.release()
# 创建线程并启动
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
在上述代码中,线程1先获取了资源A,然后尝试获取资源B,而线程2先获取了资源B,然后尝试获取资源A。由于两个线程相互等待对方释放资源,导致了死锁的发生。
- 进程死锁: 进程死锁与多线程死锁类似,只不过是在并发执行的多个进程 ** 现相互等待的情况。进程死锁的解决方法通常通过资源分配顺序的调整、加锁的合理释放、避免持有多个锁等方式来避免。示例代码如下:
import multiprocessing
# 创建资源对象
resourceA = multiprocessing.Lock()
resourceB = multiprocessing.Lock()
# 进程1获取资源A后,尝试获取资源B
def process1():
resourceA.acquire()
resourceB.acquire()
# 执行操作...
resourceB.release()
resourceA.release()
# 进程2获取资源B后,尝试获取资源A
def process2():
resourceB.acquire()
resourceA.acquire()
# 执行操作...
resourceA.release()
resourceB.release()
# 创建进程并启动
p1 = multiprocessing.Process(target=process1)
p2 = multiprocessing.Process(target=process2)
p1.start()
p2.start()
在上述代码中,进程1先获取了资源A,然后尝试获取资源B,而进程2先获取了资源B,然后尝试获取资源A。同样地,由于两个进程相互等待对方释放资源,导致了死锁的发生。
6. 线程池
-
线程不是开的越多越好,开的多了可能会导致系统的性能更低了,所以就引入线程池
import time from concurrent.futures import ThreadPoolExecutor def task(video_url): print("开始执行任务", video_url) time.sleep(5) # 创建线程池,最多维护10个线程 pool = ThreadPoolExecutor(10) url_list = ["www.xxxx-{}.com".format(i) for i in range(300)] for url in url_list: # 在线程池中提交一个任务,线程池中如果有空闲的线程,侧立马分配空前的线程去执行,执行完毕后再将线程交还给线程池,如果没有空闲的线程,则等待 pool.submit(task,url) print("start") pool.shutdown(True) # 等待线程池中的任务执行完毕后,在继续执行(有点像线程的join方法) print("end")
-
线程池任务完成可以干些别的事情(比如执行完成后,再让他做写其他事情,比如爬取数据后执行完后,再将其写入一个文件中)多任务
import time from concurrent.futures import ThreadPoolExecutor def task(video_url): print("开始执行任务", video_url) time.sleep(5) def done(response): print("任务执行后的返回值") # 创建线程池,最多维护10个线程 pool = ThreadPoolExecutor(10) url_list = ["www.xxxx-{}.com".format(i) for i in range(15)] for url in url_list: # 在线程池中提交一个任务,线程池中如果有空闲的线程,侧立马分配空前的线程去执行,执行完毕后再将线程交还给线程池,如果没有空闲的线程,则等待 futures = pool.submit(task,url) futures.add_done_callback(done) # 时子主线程执行 print("start") pool.shutdown(True) # 等待线程池中的任务执行完毕后,在继续执行(有点像线程的join方法) print("end") # 可以分工, 例如:task函数专门下载, Done函数 专门将下载的数据写入本地文件中
7. 线程和进程对比
7.1 关系对比
- 线程式依附在进程里面的,没有进程就没有线程。
- 一个进程默认提供一个线程,一个进程可以创建多个线程
7.2 区别对比
- 创建进程的资源开销要比创建线程开销要大,因为创建一个进程里面默认有一个线程
- 进程是操作系统资源分配的基本单位,线程是cpu调度的基本单位
- 线程不能独立执行,必须依附存在进程中
7.3 优缺点对比
-
进程
- 优点:可以使用多核
- 缺点:资源开销大
-
线程
- 优点:开销小
- 缺点:不能使用多核
如有有些内容写的有问题,欢迎各位大神投稿!