1.基础知识
1.1 多并发的实现方式
当程序需要处理大量的任务时,不可避免地会遇到多并发编程的场景。通过多并发可以同时处理多个任务,从而显著提高程序的执行效率。在python中,可实现多并发的方式有很多种,分别为多线程、多进程、协程和异步I/O。本文主要介绍python3中,如何利用多线程和多进程来实现并发。
1.2 线程和进程
进程和线程都是并发执行程序的基本单位,但它们之间存在一些本质区别。
进程
- 进程是一个程序在特定数据集上的一次执行。每个进程都有自己的独立内存空间,进程之间的数据是相互隔离的。
- 进程之间的通信需要使用进程间通信(IPC)机制,如管道、信号、套接字等。
- 进程的创建和销毁相对较慢,因为操作系统需要为进程分配和回收资源。
线程:
- 线程是进程中的一个执行单元,一个进程可以包含多个线程。同一进程内的所有线程共享相同的内存空间,因此线程间的数据共享和通信相对容易。
- 线程相对于进程更轻量级,创建和销毁线程的速度更快。
- 多线程编程需要注意同步和锁的问题,避免出现死锁和资源竞争。
1.3 阻塞和非阻塞
在多并发编程中,阻塞和非阻塞主要涉及到IO操作,如文件读写、网络通信等。
阻塞
- 阻塞操作会导致程序等待,直到操作完成。在此期间,程序无法执行其他任务。
- 阻塞操作的一个例子是同步IO,如常规文件读写。
非阻塞
- 非阻塞操作允许程序在等待操作完成时继续执行其他任务。
- 非阻塞操作的一个例子是异步IO,如使用回调函数、事件循环等实现的文件读写和网络通信。
2、Python的多线程编程
2.1 多线程的实现
threading
模块提供了多线程的并发功能。以下是一个简单的示例,它创建了两个线程,每个线程都运行一个函数。
python
import threading
def print_numbers():
for i in range(10):
print(i)
def print_letters():
for letter in 'abcdefghij':
print(letter)
if __name__ == "__main__":
# 创建两个线程
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)
# 启动两个线程
thread1.start()
thread2.start()
# 等待两个线程都结束(如改成非阻塞操作,将下面两行注释掉即可)
thread1.join()
thread2.join()
print("All threads finished execution")
2.2 多线程的限制
在Python中,多线程受到全局解释器锁(GIL)的限制。
GIL(Global Interpreter Lock)是Python的全局解释器锁,是Python解决多线程之间数据安全问题的一种机制。由于GIL的存在,Python的多线程在任何时刻,只允许一个线程执行Python字节码。即使在多核CPU的环境中,Python的多线程也无法实现真正的并行计算。
Python引入GIL主要是因为CPython的内存管理并不是线程安全的。如果多个线程同时操作一个Python对象,可能会导致这个对象的引用计数出现问题,进而导致内存泄露或其他问题。
然而,GIL也给Python的并发编程带来了一些问题。由于GIL的存在,Python的多线程在CPU密集型任务中无法充分利用多核CPU,其性能甚至可能不如单线程。对于IO密集型任务,虽然多线程可以带来一定的性能提升,但是由于需要频繁的线程切换,性能也会受到一定影响。
为了充分利用多核CPU,Python提供了多进程和异步IO等并发模型。多进程可以绕过GIL,实现真正的并行计算。异步IO则通过在单个线程内部进行任务切换,实现高效的IO操作。
2.3 线程安全
线程安全是多线程编程时的计算机程序中的一个概念。当代码是线程安全的,它从并行线程访问中正确地管理共享数据,以便结果始终如预期那样运行。
如果代码不是线程安全的,那么当多个线程同时访问和修改同一段数据时,可能会导致数据的不一致,这种情况通常被称为"竞态条件"。
例如,考虑一个简单的程序,其中一个线程正在读取和更改一个变量的值,而另一个线程也正在做同样的事情。如果这两个线程的操作时间上有所重叠,那么最后这个变量的值可能就不是我们预期的那样。这就是因为代码不是线程安全的而导致的问题。
下面是一个例子,创建一个非线程安全的计数器。多个线程同时对这个计数器进行增加操作,最后的结果可能不是我们预期的。
python
import threading
class NonThreadSafeCounter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
def worker(counter):
for _ in range(1000):
counter.increment()
if __name__ == "__main__":
counter = NonThreadSafeCounter()
threads = []
for _ in range(10):
thread = threading.Thread(target=worker, args=(counter,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("Expected count: ", 10 * 1000)
print("Actual count: ", counter.count)
在这个例子中,我们创建了一个NonThreadSafeCounter类,它有一个increment方法,用于增加计数器的值。然后我们创建了10个线程,每个线程都调用worker函数,worker函数会调用increment方法1000次。我们期望最后计数器的值应该是10000,但实际上运行这段代码,可能会得到小于10000的结果。
这是因为increment方法不是线程安全的,当多个线程同时调用increment方法时,它们可能会读取到相同的计数器值,然后都对这个值加1,然后再写回去。这就导致了多个线程实际上只增加了计数器的值1,而不是它们应该增加的值。这就是所谓的"竞态条件"。
3、Python的多进程编程
3.1 多进程的实现
Python提供了multiprocessing
模块来支持多进程。以上一节的代码为例,将多线程改写成多进程非常方便,只要更换类就可以了。
python
import multiprocessing
# 两函数的实现省略
if __name__ == "__main__":
# 创建两个进程
process1 = multiprocessing.Process(target=print_numbers)
process2 = multiprocessing.Process(target=print_letters)
# 启动两个进程
process1.start()
process2.start()
# 等待两个进程都结束(如改成非阻塞操作,将下面两行注释掉即可)
process1.join()
process2.join()
print("All processes finished execution")
3.2 并发限制
对于一台机器而言,其cpu的核数是有限的,意味着同时可以跑的进程数也会有限制,为了避免并发多大导致cpu负载过高,需要对并发进行限制,以多进程为例,需要定义一个进程池Pool
来限制并发的数量。
以下是一个示例:
python
import multiprocessing
import time
def worker(n):
print('Worker', n, 'started')
time.sleep(2) # 模拟耗时任务
print('Worker', n, 'finished')
if __name__ == "__main__":
# 创建一个进程池,大小为5
pool = multiprocessing.Pool(processes=5)
# 创建10个任务
for i in range(10):
pool.apply_async(worker, args=(i,))
# 关闭进程池,不再接受新的任务
pool.close()
# 等待所有任务完成
pool.join()
print("All processes finished execution")
3.3 进程间通信
不同于线程之间可以共享内存,进程之间的内存是独立的。如果你需要在进程之间传递数据,你需要使用multiprocessing
模块提供的通信机制,如Queue
,Pipe
,Manager
等。虽然三者都可以实现进程间通信,但它们在使用方式和适用场景上有所不同。
3.3.1 Pipe
Pipe
是一种简单的通信方式,可以在两个进程之间进行双向或单向通信。Pipe
的优势在于速度较快,但它只能在具有共同祖先的进程之间使用。以下是一个使用Pipe
进行进程间通信的示例:
python
from multiprocessing import Process, Pipe
def worker(conn):
print("Worker received: ", conn.recv())
conn.close()
def main():
parent_conn, child_conn = Pipe()
p = Process(target=worker, args=(child_conn,))
p.start()
parent_conn.send("Hello, Worker!")
p.join()
if __name__ == "__main__":
main()
在这个示例中,我们创建了一个Pipe
对象,然后在主进程中向队列中添加了一个字符串。然后我们创建了一个子进程,并将队列传给了子进程。
3.3.2 Queue
Queue
是一种基于管道和锁的通信方式,可以在多个进程之间进行通信。它是线程安全的,适用于多个生产者和消费者的场景。但是由于使用了锁,它的速度可能比Pipe
慢。以下是一个使用Queue
进行进程间通信的示例:
python
from multiprocessing import Process, Queue
def worker(q):
print("Worker received: ", q.get())
def main():
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
q.put("Hello, Worker!")
p.join()
if __name__ == "__main__":
main()
3.3.3. Manager
Manager
是一种更高级的进程间通信方式,它允许多个进程共享更多种类的数据结构,如列表、字典、集合等。但是由于Manager
使用了服务器进程,它的速度可能比Pipe
和Queue
都慢。以下是一个使用Manager
进行进程间通信的示例:
python
from multiprocessing import Process, Manager
def worker(d, key, value):
d[key] = value
def main():
with Manager() as manager:
d = manager.dict()
p = Process(target=worker, args=(d, 'key', 'Hello, Worker!'))
p.start()
p.join()
print("Main process received: ", d['key'])
if __name__ == "__main__":
main()
总结:
- 如果你只需要在两个进程之间进行简单的通信,可以使用
Pipe
。 - 如果你需要在多个进程之间进行通信,并且需要线程安全的队列,可以使用
Queue
。 - 如果你需要在多个进程之间共享更复杂的数据结构,可以使用
Manager
。
3.4进程安全
和线程安全同理,在进行多进程编程时,也要注意进程安全。尤其是在使用共享内存或者文件时,你需要确保不同的进程不会同时写入相同的内存或文件。