专栏列表
- Python教程(十):面向对象编程(OOP)
- Python教程(十一):单元测试与异常捕获
- Python教程(十二):面向对象高级编程详解
- Python教程(十三):常用内置模块详解
- Python教程(十四):Requests模块详解
- Python教程(十五):IO 编程
正文开始
,如果觉得文章对您有帮助,请帮我三连+订阅,谢谢
💖💖💖
引言
在现代软件开发中,充分利用
多核处理器
的能力,提高应用程序的性能和响应性,是每个开发者都需要考虑的问题。Python 作为一门高级编程语言,提供了多种并发编程的解决方案,包括多进程、多线程和线程锁
进程与线程基础
进程(Process)
进程是操作系统进行资源分配和调度的一个独立单位,每个进程都有自己的内存空间和系统资源。我们可以把进程想象成手机中的一个APP,每个进程(APP)都有自己独立的内存空间,数据独立不互扰
线程(Thread)
线程是进程中的为完成一些事情而独立出的分支,是被系统独立调度和分派的基本单位,一个进程中至少有一个线程,多个线程间可以共享同一个进程所拥有的全部资源。例如一个APP拥有(发送网络线程,执行脚本线程,渲染界面线程)
Python 多进程
Python 的 multiprocessing
模块允许你创建进程,利用多核处理器的优势。由于 Python 的全局解释器锁(GIL),多进程是实现 CPU 密集型任务并行化的首选。
- 调用用
start()
方法启动进程 join()
方法可以等待子进程结束后再继续往下运行
,通常用于进程间的同步。os.getpid()
可以获取系统的进程Id,os.getppid()
可以获取父进程的 进程ID
# 基本使用
from multiprocessing import Process
import os
def worker(name , addr):
print(f'Worker: {name } {addr}, 父进程pid {os.getppid()}')
if __name__ == '__main__':
print(f'系统pid {os.getpid()}')
p = Process(target=worker, args=('新进程','未知'))
p.start()
p.join()
进程池
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print(f'运行任务 {name} {os.getpid()}')
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('PID %s.' % os.getpid())
p = Pool(4) # 开启4个进程池
for i in range(5):
p.apply_async(long_time_task, args=(i,)) # 添加 5 个同步任务 , 就会出现任务等待情况
print('等到所有子进程执行结束...')
p.close() # 调用close()之后就不能继续添加新的Process了。
p.join()
print('所有进程执行结束')
子进程
很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。
subprocess
模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
import subprocess
print('$ ping www.baidu.com')
r = subprocess.call(['ping', 'www.baidu.com'])
print('Exit code:', r)
进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
Python 线程
Python的标准库提供了两个模块:
_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。然而,由于 GIL 的存在,多线程在 CPU 密集型任务上的优势并不明显,但在 I/O 密集型任务上,多线程可以显著提高性能。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()
开始执行:
import threading
def print_numbers():
for i in range(1, 6):
print(f'当前线程{threading.current_thread().name} , 输出值 {i}')
thread = threading.Thread(target=print_numbers ,name='ziyu')
thread.start()
thread.join()
print(f'默认主线程 {threading.current_thread().name} 执行完毕')
- Python的threading模块有个
current_thread()
函数,它永远返回当前线程的实例 主线程实例
的名字叫MainThread
,子线程
不起名字Python就自动给线程命名为Thread-1,Thread-2……
多线程示例
import threading
def print_numbers():
for i in range(1, 6):
print(f'Thread {threading.current_thread().name}: {i}')
threads = []
for i in range(2):
t = threading.Thread(target=print_numbers)
threads.append(t)
t.start()
for t in threads:
t.join()
锁(Lock)
多线程和多进程最大的不同在于,
多进程中内存空间是相互独立的
,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,
多线程中,所有变量都由所有线程共享,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量
,容易出现数据错误
在 Python 中,如果没有使用锁(例如 threading.Lock),多个线程同时访问和修改共享数据时可能会遇到竞态条件(Race Condition),导致数据不一致或程序行为异常。
请注意,由于 Python 的全局解释器锁(Global Interpreter Lock),在 CPython 解释器中,这种简单的自增操作实际上可能不会遇到竞态条件,因为 GIL 会确保在任何给定时间只有一个线程执行 Python 字节码。
# 共享计数器
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1
# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
print(f"期待计数: 200000")
print(f"实际计数: {counter}")
为了避免竞态条件,我们可以使用 threading.Lock 来同步对共享资源的访问:
import threading
# 共享计数器
counter = 0
# 创建一个锁
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
# 获取锁
lock.acquire()
try:
counter += 1
finally:
# 释放锁
lock.release()
# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
print(f"期待计数: 200000")
print(f"实际计数: {counter}")
多进程 vs 多线程
- 多进程:适合 CPU 密集型任务,可以绕过 GIL,利用多核处理器。
- 多线程:适合 I/O 密集型任务,由于 GIL 的存在,CPU 密集型任务并行效果不佳。