进程
程序编写完没有运行称之为程序。正在运行的代码(程序)就是进程。在Python3语言中,对多进程支持的是multiprocessing模块和subprocess模块。multiprocessing模块为在子进程中运行任务、通讯和共享数据,以及执行各种形式的同步提供支持。
from multiprocessing import Process
#定义子进程代码
def run_proc():
print('子进程运行中')
if __name__=='__main__':
print('父进程运行')
p=Process(target=run_proc)
print('子进程将要执行')
p.start()
multiprocessing 是 Python 的标准库模块,用于在 Python 程序中创建多个进程来并发执行任务。Process 是 multiprocessing 模块中的一个类,用于创建和管理单独的进程。每个 Process 对象都表示一个独立的进程,它可以与主进程同时运行,执行不同的任务。
PCB概念:
操作系统描述程序的运行过程,通过一个结构体task_struct{......},统称为PCB(process control block)。是进程管理和控制的最重要的数据结构。每一个进程均有一个PCB,在创建进程时,建立PCB伴随进程运行的全过程,直到进程撤消而撤消。
进程的分类
1)前台进程:
是指用户可以在终端和进程相互交互的进程
2)后台进程
是指没有占用终端的进程,后台进程不需要和用户交互
3)守护进程:
是指在系统启动时启动,并且在系统关闭时结束的进程
进程间的通信
不同进程之间总会需要传播、交换数据。这里进程之间通信必须通过内核,因为进程的用户空间都是独立的,内核空间是每个进程都共享的“公共区域”,所以研究如何“对话”。(虚拟映射的问题)
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
解决用户态与内核态之间的频繁消息拷贝。
进程池
在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程。
进程的特点
动态性:进程是程序的一次执行过程,是临时的,有生命期的是动态产生,动态消亡的:
并发性:任何进程都可以同其他进行一起并发执行;
独立性:进程是系统进行资源分配和调度的一个独立单位
结构性:进程由程序,数据和进程控制块三部分组成;
独立性:每个进程拥有自己独立的内存空间、资源和上下文,进程间彼此独立,互不干扰。如果一个进程崩溃,不会影响其他进程的运行。
独立资源分配:进程拥有自己的资源(如内存、文件句柄等),操作系统为每个进程分配独立的内存地址空间和资源,隔离性强,适合需要高可靠性的任务。
进程间通信(IPC):由于进程相互独立,它们不能直接共享内存,进程之间的数据交换需要使用进程间通信机制(如管道、消息队列、共享内存等),这使得通信相对复杂。
并行执行:在多核CPU上,多个进程可以真正实现并行执行,不受像Python GIL(全局解释器锁)等限制,适合CPU密集型任务,能充分利用多核资源。
上下文切换开销大:进程拥有独立的资源,因此进程切换时,操作系统需要保存和恢复更多的上下文信息(如内存空间、CPU寄存器等),上下文切换的开销比线程大。
适合隔离任务:进程之间的内存和资源是完全独立的,因此适合需要任务隔离的场景,例如在某些任务失败时,其他任务不受影响的需求。
启动速度慢:由于进程创建需要分配独立的资源(如内存),相比线程,启动进程的速度较慢,系统开销更大。
适合CPU密集型任务:进程特别适用于需要高计算能力的任务,能充分利用多核并行的计算能力,而不受内存共享和同步机制的限制。
进程的优点
独立内存空间:每个进程都有独立的内存空间,进程之间互不干扰,一个进程的崩溃不会影响其他进程,隔离性强,安全性高。
充分利用多核CPU:进程可以在多核CPU上并行执行,适合CPU密集型任务,能够绕开Python中的GIL(全局解释器锁)限制。
稳定性高:由于进程是独立的,某个进程的错误不会影响其他进程,适合任务隔离和高可靠性的应用。
进程的缺点
创建和销毁开销大:创建、销毁进程需要分配独立的资源和内存,进程切换的开销比线程大。
进程间通信复杂:进程间不能直接共享内存,必须通过进程间通信(如管道、队列、共享内存等),增加了复杂性和延迟。
启动速度慢:由于需要分配独立资源,创建进程比创建线程慢,适合较长时间运行的任务,不适合频繁创建销毁的场景。
举个列子:
计算数字平方并写入文件
import multiprocessing
def write_to_file(filename, number):
square = number ** 2 # 计算数字的平方
with open(filename, 'a') as f: # 以追加模式打开文件
f.write(f'The square of {number} is {square}\n')
def process_task(filename, number):
write_to_file(filename, number)
if __name__ == "__main__":
filename = "output_process.txt"
# 创建进程池
processes = []
for i in range(1, 6): # 启动5个进程,分别计算1到5的平方
p = multiprocessing.Process(target=process_task, args=(filename, i))
processes.append(p)
p.start()
# 等待所有进程完成
for p in processes:
p.join()
# 读取并输出文件的内容
with open(filename, 'r') as f:
content = f.read()
print("File content:\n", content)
线程
线程也是实现多任务的一种方式,一个进程中,也经常需要同时做多件事,就需要同时运行多个‘子任务’,这些子任务就是线程。一个进程可以拥有多个并行的线程,其中每一个线程,共享当前进程的资源。
Python程序中,可以通过“_thread”和threading(推荐使用)这两个模块来处理线程。在Python3中,thread模块已经废弃。可以使用threading模块代替。所以,在Python3中不能再使用thread模块,但是为了兼容Python3以前的程序,在Python3中将thread模块重命名为“_thread”。
threading模块
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
线程共享全局变量
在一个进程内所有线程共享全局变量,多线程之间的数据共享比多进程要好。但是可能造成多个进程同时修改一个变量(即线程非安全),可能造成混乱。
互斥锁
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。最简单的同步机制就是引入互斥锁。
锁有两种状态——锁定和未锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”状态,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
使用 Thread 对象的 Lock 可以实现简单的线程同步,有上锁 acquire 方法和 释放release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。
死锁
在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
线程同步的应用
同步就是协同步调,按预定的先后次序进行运行。例如:开会。“同”字指协同、协助、互相配合。
如进程、线程同步,可以理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行,B运行后将结果给A,A继续运行。
生产者消费者模式
生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入生产者和消费者模式
生产者消费者模式通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者之间不直接通信。生产者生产商品,然后将其放到类似队列的数据结构中,消费者不找生产者要数据,而是直接从队列中取。这里使用queue模块来提供线程间通信的机制,也就是说,生产者和消费者共享一个队列。生产者生产商品后,会将商品添加到队列中。消费者消费商品,会从队列中取出商品。
ThreadLocal
我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程对全局变量的计算操作,从而出现数据混乱,即为脏数据。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁来控制对全局变量的访问。所以有时候线程使用局部变量比全局变量好,因为局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。
Python 还提供了ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。
举个列子:
多线程示例:模拟下载网页数据
import threading
import time
# 模拟网页下载函数
def download_data(url, filename):
print(f"Downloading data from {url}...")
time.sleep(1) # 模拟下载延迟
data = f"Data from {url}" # 模拟下载的数据
write_to_file(filename, data)
print(f"Finished downloading data from {url}")
# 文件写入函数(线程安全)
lock = threading.Lock() # 用于线程同步
def write_to_file(filename, content):
with lock: # 确保每次只有一个线程能写入文件
with open(filename, 'a') as f:
f.write(content + '\n')
if __name__ == "__main__":
filename = "web_data.txt"
urls = [
"http://example.com/page1",
"http://example.com/page2",
"http://example.com/page3",
"http://example.com/page4",
"http://example.com/page5"
]
# 创建并启动多个线程,每个线程模拟下载不同网页
threads = []
for url in urls:
t = threading.Thread(target=download_data, args=(url, filename))
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
# 读取并输出文件内容
with open(filename, 'r') as f:
content = f.read()
print("\nFile content:\n", content)
线程的优点
1.多个线程共享当前进程的资源。
2.进程下的线程间通信,无需操作系统干预(进程通信需要请求操作系统服务CPU切换到内核态),开销更小。
3.线程间的并发比进程的开销更小,系统并发性提升。
需要注意的是:
1.从属于不同进程的线程间通信,也必须请求操作系统服务。
2.同样,从属于不同进程的线程间切换,它是会导致进程切换的,所以开销也大。
轻量级:线程的创建和销毁开销小,比进程快,因为线程共享同一进程的内存和资源。
通信简单:线程之间共享进程内存空间,数据交换无需复杂的机制,直接共享变量,通信效率高。
启动快:线程创建速度比进程快,适合频繁切换的任务。
资源占用少:线程共享进程的资源(如内存、文件句柄),能节省系统资源。
线程的特点
建、销毁和切换比进程更轻量、速度更快。
共享内存空间:同一进程内的所有线程共享相同的地址空间和资源(如内存、文件句柄等),这使得线程之间的数据共享和通信非常高效。
独立执行:每个线程有自己的运行栈和程序计数器,能独立执行任务,多个线程可以并发运行。
高效的上下文切换:由于线程共享进程的资源,上下文切换时不需要像进程那样保存和恢复大量的系统资源,切换的开销较小,效率高。
多线程并发:多线程允许在单核处理器上通过快速切换任务实现并发执行,在多核处理器上可实现真正的并行执行(除非有像Python的GIL限制)。
需要同步机制:由于线程共享资源,如果多个线程同时访问共享资源,可能会产生数据竞争和不一致,需要使用锁、信号量等同步机制来确保线程安全。
适合I/O密集型任务:由于I/O操作常常会导致阻塞,多线程可以在等待I/O操作时执行其他任务,充分利用资源,提高系统效率。
线程的缺点
当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃。
线程安全问题:由于共享内存,多个线程同时访问数据时会产生竞争条件,必须使用锁等机制来避免数据冲突,增加了编程复杂度。
GIL限制(Python特有):Python中的全局解释器锁(GIL)限制了多线程在多核CPU上并行运行,无法提升CPU密集型任务的性能。
崩溃风险:线程共享内存,如果一个线程崩溃,可能导致整个进程崩溃,增加了系统不稳定的风险。
调试困难:线程间竞争和同步问题(如死锁、竞态条件)难以检测和调试。
进程 VS 线程
一个进程中可以有多个线程,它们共享这个进程的资源(内存空间,包括代码段,数据集,堆等,及一些进程级的资源,如打开文件和信号等。为减少进程切换的开销,把进程作为资源分配的基本
单位(很少调度或切换),线程成为独立调度的基本单位,线程上下文切换比进程上下文切换要快得多。