目录
一、多线程
1. 线程基础概念
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,比如内存空间、文件描述符等。这使得线程之间的通信和数据共享相对容易,但也带来了资源竞争和同步的问题。
2. threading
模块
在 Python 中,threading
模块提供了创建和管理线程的功能。
创建线程
可以通过定义一个函数,然后将这个函数作为目标传递给threading.Thread
类的构造函数来创建线程。例如:
import threading
def print_numbers():
for i in range(5):
print(threading.current_thread().name, i)
thread1 = threading.Thread(target=print_numbers, name='Thread 1')
thread2 = threading.Thread(target=print_numbers, name='Thread 2')
在这个例子中,print_numbers
是要在线程中执行的函数,thread1
和thread2
是两个线程对象,分别被命名为Thread 1
和Thread 2
。
启动和结束线程
创建线程对象后,需要调用start
方法来启动线程,线程会开始执行目标函数。例如:
thread1.start()
thread2.start()
当线程的目标函数执行完毕后,线程自动结束。也可以使用join
方法来阻塞主线程,直到调用join
的线程执行完毕。例如:
thread1.join()
thread2.join()
这在需要确保所有子线程都完成任务后再进行后续操作的场景中很有用,比如在主线程中等待所有数据处理线程完成后再进行结果汇总。
3. 线程同步
当多个线程访问共享资源(如全局变量、共享数据结构等)时,如果没有正确的同步机制,可能会导致数据不一致、程序崩溃等问题。
线程锁(Lock
)
Lock
是最基本的同步原语。它通过acquire
方法获取锁,通过release
方法释放锁。当一个线程获取锁后,其他线程必须等待该线程释放锁才能获取它。例如:
import threading
counter = 0
lock = threading.Lock()
def increment_counter():
global counter
for _ in range(5):
with lock:
counter += 1
print(threading.current_thread().name, counter)
在这个例子中,lock
用于保护对counter
变量的访问。with lock
语句是一种简洁的获取和释放锁的方式,它等效于lock.acquire()
和lock.release()
,但更安全,即使在代码块中发生异常也能保证锁被正确释放。
其他同步机制
条件变量(Condition
)
除了简单的锁机制,Condition
对象可以让线程在满足特定条件时等待或被通知。例如,多个线程等待某个资源可用,当资源准备好时,可以通过Condition
的notify
或notify_all
方法通知等待的线程。
1. 基本概念
条件变量是一种更高级的同步原语,它在锁(Lock)的基础上提供了一种让线程能够基于特定条件进行等待和被通知的机制。简单来说,就是线程可以在满足一定条件之前一直处于等待状态,直到其他线程通过某种方式通知它条件已经满足,然后它才继续执行后续操作。
2. 主要组成部分及方法
-
锁(Lock)关联:条件变量通常是和一个锁对象相关联的,这个锁用于保护条件变量所涉及的共享资源以及控制对条件变量相关操作的并发访问。一般情况下,在操作条件变量之前,需要先获取与之关联的锁,以确保操作的正确性和线程安全性。
-
等待(wait)方法:线程可以调用条件变量的
wait
方法来进入等待状态。当线程调用wait
时,它会首先释放与之关联的锁,然后进入阻塞状态,等待其他线程的通知。这样做的目的是为了避免线程在等待条件满足的过程中一直占用着锁,从而导致其他线程无法获取锁来修改可能影响条件的共享资源。例如,在一个生产者 - 消费者模型中,如果消费者线程发现共享缓冲区为空,它就可以调用wait
方法等待生产者线程生产出产品并通知它。 -
通知(notify 和 notify_all)方法:当共享资源的状态发生变化,使得某些等待的线程所等待的条件可能已经满足时,其他线程(通常是修改共享资源状态的线程)可以通过条件变量的
notify
或notify_all
方法来通知那些处于等待状态的线程。notify
方法会随机唤醒一个等待的线程,而notify_all
方法则会唤醒所有正在等待的线程。例如,在生产者 - 消费者模型中,当生产者线程生产出一个产品并放入共享缓冲区后,它就可以通过notify
或notify_all
方法通知正在等待的消费者线程,告诉它们缓冲区中有产品可供消费了。
3. 示例代码
以下是一个简单的示例代码,展示了条件变量在生产者 - 消费者模型中的应用:
import threading
import time
import queue
# 共享缓冲区大小
BUFFER_SIZE = 5
# 创建共享队列作为缓冲区
buffer_queue = queue.Queue(BUFFER_SIZE)
# 创建条件变量及关联的锁
condition = threading.Condition()
def producer():
global buffer_queue
for i in range(10):
with condition:
while buffer_queue.full():
print("生产者:缓冲区已满,等待消费者消费...")
condition.wait()
item = i
buffer_queue.put(item)
print(f"生产者:生产了产品 {item},放入缓冲区")
condition.notify_all()
time.sleep(1)
def consumer():
global buffer_queue
for i in range(10):
with condition:
while buffer_queue.empty():
print("消费者:缓冲区已空,等待生产者生产...")
condition.wait()
item = buffer_queue.get()
print(f"消费者:消费了产品 {item}")
condition.notify_all()
time.sleep(2)
if __name__ == '__main__':
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
在上述示例中:
- 首先定义了一个共享缓冲区的大小,并创建了一个共享队列
buffer_queue
作为缓冲区,同时创建了一个条件变量condition
及其关联的锁。 - 在生产者函数
producer
中,每次生产一个产品前,先获取条件变量的锁,然后检查缓冲区是否已满。如果已满,则调用wait
方法等待消费者消费产品并通知它。生产出产品后,将产品放入缓冲区,并通过notify_all
方法通知所有等待的消费者线程。 - 在消费者函数
consumer
中,每次消费一个产品前,先获取条件变量的锁,然后检查缓冲区是否已空。如果已空,则调用wait
方法等待生产者生产产品并通知它。消费完产品后,从缓冲区中取出产品,并通过notify_all
方法通知所有等待的生产者线程。
通过这样的方式,利用条件变量实现了生产者和消费者之间的协调配合,使得生产和消费过程能够有条不紊地进行。
信号量(Semaphore
)
信号量用于控制对有限资源的访问。它维护一个计数器,通过acquire
和release
方法来改变计数器的值。当计数器大于 0 时,线程可以获取资源(调用acquire
),否则线程需要等待直到其他线程释放资源(调用release
)。例如,限制同时访问某个网络连接的线程数量。
1. 基本概念
信号量是一种用于控制对有限资源访问的同步机制。它通过维护一个计数器来实现对资源的管理,这个计数器的值表示当前可供使用的资源数量。线程可以通过调用 acquire
和 release
方法来改变计数器的值,从而获取或释放资源。
2. 主要组成部分及方法
-
计数器:信号量内部维护一个计数器,它初始化为某个值,表示可供使用的资源数量。例如,如果要限制同时访问某个网络连接的线程数量为
3
,那么可以将信号量的计数器初始化为3
。 -
acquire 方法:线程若要使用资源,需要调用信号量的
acquire
方法。当线程调用acquire
时,如果计数器的值大于0
,则表示还有可供使用的资源,此时计数器的值会减1
,线程可以获取到资源并继续执行后续操作;如果计数器的值等于0
,则表示资源已经用完,线程需要进入等待状态,直到其他线程调用release
方法释放资源,使得计数器的值大于0
。 -
release 方法:当线程使用完资源后,需要调用信号量的
release
方法来释放资源。调用release
时,计数器的值会加1
,这样就可以让其他等待的线程有机会获取资源。
3. 示例代码
以下是一个简单的示例代码,展示了信号量在限制线程访问网络连接中的应用:
import threading
import time
# 设定可同时访问网络连接的线程数量
MAX_CONNECTIONS = 3
# 创建信号量对象,初始值为可同时访问的线程数量
semaphore = threading.Semaphore(MAX_CONNECTIONS)
def access_network():
global semaphore
with semaphore:
print(f"{threading.current_thread().name}:获取到网络连接,开始访问...")
time.sleep(2)
print(f"{threading.current_thread().name}:访问完毕,释放网络连接")
if __name__ == '__main__':
threads = []
for i in range(5):
thread = threading.Thread(target=access_network)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
在上述示例中:
- 首先设定了可同时访问网络连接的线程数量为
3
,并创建了一个信号量对象semaphore
,其初始值为3
。 - 在
access_network
函数中,线程首先通过with semaphore:
语句调用acquire
方法来获取资源(这里就是获取网络连接)。如果能够获取到,就可以开始访问网络连接,访问完毕后,通过release
方法释放网络连接,使得其他等待的线程有机会获取资源。 - 在主程序中,创建了
5
个线程来尝试访问网络连接,由于信号量的限制,最多只有3
个线程能够同时获取到网络连接并进行访问,其他线程需要等待前面的线程释放资源后才能获取到网络连接并进行访问。
通过信号量这种同步机制,可以有效地控制对有限资源的访问,避免资源的过度使用和线程之间的冲突。
二、多进程
1. 进程基础概念
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间、文件描述符等资源,这使得进程之间相对独立,但进程间的通信和数据共享相对复杂,需要特定的机制。
2. multiprocessing
模块
Python 的multiprocessing
模块用于创建和管理多进程。
创建进程
与创建线程类似,可以通过定义一个函数,然后将这个函数作为目标传递给multiprocessing.Process
类的构造函数来创建进程。例如:
import multiprocessing
def print_numbers():
for i in range(5):
print(multiprocessing.current_process().name, i)
process1 = multiprocessing.Process(target=print_numbers, name='Process 1')
process2 = multiprocessing.Process(target=print_numbers, name='Process 2')
这里创建了两个进程对象process1
和process2
,它们将分别执行print_numbers
函数。
启动和结束进程
创建进程对象后,通过调用start
方法来启动进程。例如:
process1.start()
process2.start()
与线程不同的是,进程结束后不需要像线程的join
方法那样手动阻塞等待(当然也可以使用join
方法来等待进程结束,这在需要确保进程执行结果对后续操作有影响的场景中很有用)。因为进程是独立的资源分配单位,即使主进程结束,子进程也可以继续运行(取决于操作系统的调度和进程的属性设置)。
3. 进程间通信
由于进程之间的内存空间是独立的,它们之间的通信需要特殊的机制。
队列(Queue
)
multiprocessing.Queue
类提供了一种简单的进程间通信方式。一个进程可以将数据放入队列,另一个进程可以从队列中取出数据。例如:
import multiprocessing
def producer(q):
for i in range(5):
q.put(i)
q.put(None)
def consumer(q):
while True:
item = q.get()
if item is None:
break
print(multiprocessing.current_process().name, item)
queue = multiprocessing.Queue()
producer_process = multiprocessing.Process(target=producer, args=(queue,))
consumer_process = multiprocessing.Process(target=consumer, args=(queue,))
- 在这个例子中,
producer
进程将数字 0 到 4 放入队列,consumer
进程从队列中取出数字并打印。当consumer
进程取出None
时,它知道数据已经全部接收完毕,从而结束循环。
管道(Pipe
)
Pipe
提供了一种双向的进程间通信机制,它返回两个连接对象,分别用于在两个方向上发送和接收数据。例如:
import multiprocessing
def f(conn):
conn.send([42, None, 'hello'])
conn.close()
parent_conn, child_conn = multiprocessing.Pipe()
p = multiprocessing.Process(target=f, args=(child_conn,))
p.start()
print(parent_conn.recv())
p.join()
在这个例子中,一个进程通过管道发送一个列表数据,另一个进程接收并打印这个数据。
共享内存
multiprocessing
模块还支持共享内存的方式来实现进程间通信,比如Value
和Array
类型。它们允许不同的进程访问和修改同一块内存区域,但需要注意同步问题,因为多个进程同时访问和修改可能会导致数据不一致。例如:
import multiprocessing
num = multiprocessing.Value('i', 0)
arr = multiprocessing.Array('d', [1.0, 2.0, 3.0])
在这个例子中,num
是一个共享的整数变量,arr
是一个共享的双精度浮点数数组。不同的进程可以通过适当的同步机制来访问和修改这些共享内存区域。
1. 共享内存的基本概念
在多进程编程中,每个进程都有自己独立的内存空间,这使得进程间直接共享数据变得困难。共享内存是一种机制,它允许不同的进程能够访问同一块物理内存区域,从而实现数据的共享和交互,这样就可以在不同进程之间传递信息,而无需像管道等方式那样进行数据的复制和传输,提高了数据共享的效率。
2. multiprocessing.Value
的介绍
a.创建方式:通过 multiprocessing.Value
可以创建一个共享的简单数据类型变量,比如整数、浮点数等。它的构造函数接受两个参数,第一个参数是数据类型的字符串表示,第二个参数是初始值。例如:
num = multiprocessing.Value('i', 0)
这里创建了一个共享的整数变量 num
,初始值为 0
。其中 'i'
表示整数类型(在 multiprocessing
模块中,常用的数据类型表示有:'c'
表示字符型,'b'
表示有符号字节型,'B'
表示无符号字节型,'h'
表示有符号短整型,'H'
表示无符号短整型,'i'
表示有符号整型,'I'
表示无符号整型,'l'
表示有符号长整型,'L'
表示无符号长整型,'f'
表示单精度浮点数,'d'
表示双精度浮点数等)。
b.访问和修改:不同的进程可以通过这个共享变量来进行访问和修改操作。但要注意,由于多个进程可能同时对其进行操作,所以需要使用同步机制(后面会详细介绍)来确保数据的一致性。例如,一个进程可以这样修改 num
的值:
num.value = 10
这里通过 num.value
来访问共享变量 num
的实际值,并将其设置为 10
。
3.multiprocessing.Array
的介绍
a.创建方式:multiprocessing.Array
用于创建一个共享的数组,可以是各种数据类型的数组,如整数数组、浮点数数组等。它的构造函数接受两个参数,第一个参数是数据类型的字符串表示,第二个参数是一个可迭代对象,用于初始化数组。例如:
arr = multiprocessing.Array('d', [1.0, 2.0, 3.0])
这里创建了一个共享的双精度浮点数数组 arr
,初始值为 [1.0, 2.0, 3.0]
。
访问和修改:不同的进程同样可以访问和修改这个共享数组。对于数组元素的访问,可以通过索引来进行。例如,一个进程可以这样修改数组中的某个元素:
arr[0] = 1.5
这里将数组 arr
的第一个元素修改为 1.5
。同样,由于多个进程可能同时操作数组,所以也需要同步机制来保障数据的一致性。
4. 同步问题及解决办法
a.同步问题的产生
当多个进程同时访问和修改共享内存区域(如共享变量或共享数组)时,就可能会出现数据不一致的情况。
例如,假设两个进程同时对一个共享整数变量进行自增操作,如果没有合适的同步机制,可能会出现这样的情况:进程 A 读取到变量的值为 5
,准备进行自增操作;与此同时,进程 B 也读取到该变量的值为 5
,然后进程 A 先完成自增操作并将结果写回共享变量,使其值变为 6
,接着进程 B 完成自增操作并将结果写回共享变量,此时变量的值应该是 7
,但由于没有同步,进程 B 并不知道进程 A 已经修改了值,所以它仍然按照自己读取到的 5
进行自增并写回,导致最终共享变量的值为 6
,这就出现了数据不一致的情况。
b.解决办法:使用锁(Lock)
为了避免上述数据不一致的情况,通常需要使用锁(Lock
)等同步机制。multiprocessing
模块提供了 Lock
类来实现这种同步功能。以下是一个使用锁来同步对共享变量操作的示例:
import multiprocessing
def increment(num, lock):
for i in range(10):
with lock:
num.value += 1
if __name__ == '__main__':
num = multiprocessing.Value('i', 0)
lock = multiprocessing.Lock()
p1 = multiprocessing.Process(target=increment, args=(num, lock))
p2 = multiprocessing.Process(target=increment, args=(num, lock))
p1.start()
p2.start()
p1.join()
p2.join()
print("最终共享变量的值:", num.value)
在上述示例中:
- 首先创建了一个共享整数变量
num
和一个锁lock
。 - 然后定义了一个函数
increment
,该函数用于对共享变量进行自增操作。在函数内部,通过with lock:
语句来获取锁,在获取锁之后,才能对共享变量进行操作,这样就保证了在同一时刻只有一个进程能够对共享变量进行修改,避免了数据不一致的情况。 - 最后创建了两个进程
p1
和p2
,并让它们都执行increment
函数,分别对共享变量进行自增操作。当两个进程都完成后,通过print("最终共享变量的值:", num.value)
来打印出最终共享变量的值。
同样的道理,对于共享数组的操作,如果多个进程需要同时对其进行修改,也可以使用锁来进行同步,确保数据的一致性。
通过共享内存(Value
和 Array
类型)结合合适的同步机制(如锁),可以在 multiprocessing
模块中实现高效且准确的进程间通信和数据共享。