多线程信号量 | XYZ
Post

多线程信号量

多线程信号量

在Python中,Semaphore(信号量)是一种用于控制对共享资源的访问的同步原语。它可以用来限制同时访问某个资源的线程的数量。信号量通常用于多线程编程,确保在并发环境中安全地共享资源。

Python的threading模块提供了一个Semaphore类,允许你创建信号量对象。信号量维护一个内部计数器,表示可用资源的数量。当线程想要访问共享资源时,它需要首先获取信号量。若信号量的计数器大于0,线程将能够继续执行,同时计数器减1;如果计数器为0,线程必须等待,直到其他线程释放信号量。

使用示例

下面是一个使用Semaphore的简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import threading
import time

# 创建一个Semaphore对象,初始计数为2
semaphore = threading.Semaphore(2)

def worker(identifier):
    print(f"Worker {identifier} is waiting to access the resource.")
    with semaphore:
        print(f"Worker {identifier} has acquired the semaphore.")
        time.sleep(2)  # 模拟资源处理过程
        print(f"Worker {identifier} is releasing the semaphore.")

# 创建多个线程
threads = []
for i in range(5):
    thread = threading.Thread(target=worker, args=(i,))
    threads.append(thread)
    thread.start()

# 等待所有线程完成
for thread in threads:
    thread.join()

print("All workers have finished.")

说明

  • 在这个例子中,信号量的初始计数为2,这意味着最多只有两个线程可以同时访问共享资源。
  • 其他线程如果尝试获取信号量,必须等待,直到有线程释放信号量。
  • with semaphore语句自动处理信号量的获取和释放,确保即使发生异常也能正确释放信号量。

使用Semaphore可以帮助管理并发,避免数据竞争和资源冲突。

LockSemaphore 都是 Python 中用于线程同步的工具,但它们在功能和用途上有一些关键的区别。以下是这两者的比较:

1. 锁的概念

  • Lock(锁)
    • Lock 是一个基本的同步原语。它用来保护共享资源,确保在某一时刻只有一个线程能够访问资源。
    • Lock 只有两种状态:可用和不可用。
    • 当一个线程持有锁时,其他线程请求锁将被阻塞,直到锁被释放。
  • Semaphore(信号量)
    • Semaphore 是一个更复杂的同步原语,可以用来控制同时访问某个特定资源的线程数量。
    • Semaphore 使用一个内部计数器来跟踪当前可用的资源数量,计数器可以大于1。
    • 当线程调用 acquire() 时,信号量的计数器会减少。线程可以在计数器为正时继续执行;当计数器为0时,线程会阻塞,直到有线程释放信号量。

2. 使用场景

  • Lock
    • 通常用于保护共享数据,确保同一时刻只有一个线程对数据进行读/write 操作。适合于需要严格互斥的场景。
  • Semaphore
    • 适用于限制对某个资源的访问,例如限制同一个数据库连接,还可以控制并发线程的数量。适合于不需要严格互斥,但需要限制访问量的场景。

3. 代码示例

以下是使用 LockSemaphore 的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import threading
import time

# 使用Lock进行线程同步
lock = threading.Lock()

def lock_worker(identifier):
    print(f"Worker {identifier} is trying to acquire the lock.")
    lock.acquire()  # 请求获取锁
    try:
        print(f"Worker {identifier} has acquired the lock.")
        time.sleep(2)
    finally:
        print(f"Worker {identifier} is releasing the lock.")
        lock.release()

# 使用Semaphore进行线程同步
semaphore = threading.Semaphore(2)  # 最多允许2个线程同时访问

def semaphore_worker(identifier):
    print(f"Worker {identifier} is trying to acquire the semaphore.")
    semaphore.acquire()  # 请求获取信号量
    try:
        print(f"Worker {identifier} has acquired the semaphore.")
        time.sleep(2)
    finally:
        print(f"Worker {identifier} is releasing the semaphore.")
        semaphore.release()

# 创建线程
lock_threads = []
for i in range(3):
    thread = threading.Thread(target=lock_worker, args=(i,))
    lock_threads.append(thread)
    thread.start()

# 等待所有Lock线程完成
for thread in lock_threads:
    thread.join()

print("All Lock workers have finished.")

# 创建Semaphore线程
semaphore_threads = []
for i in range(5):
    thread = threading.Thread(target=semaphore_worker, args=(i,))
    semaphore_threads.append(thread)
    thread.start()

# 等待所有Semaphore线程完成
for thread in semaphore_threads:
    thread.join()

print("All Semaphore workers have finished.")

总结

  • 使用 LockSemaphore 可以有效地管理线程之间的协作与资源访问。
  • Lock 适用于需要严格互斥的场景,保证一次只有一个线程在访问共享资源。
  • Semaphore 适用于需要控制同时访问的线程数量的场景,可以允许多个线程同时访问,但数量受到限制。

根据具体的需求和场景选择合适的同步原语可以提高程序的效率和安全性。

是的,如果你的资源管理不涉及一段连续的代码块使用,例如需要在多个不连续的逻辑中释放某个资源,使用 with 语句就不太适合。在这种情况下,手动调用 acquire()release() 将更为适用。

何时使用 with 语句

with 语句非常适合用于以下情况:

  1. 简单的、连续的资源使用
    • 当你在一个逻辑块中获取资源,并在逻辑块结束时释放它。比如访问某个共享资源或者进行文件读写。
  2. 异常安全
    • 如果你的代码包含可能引发异常的操作,使用 with 语句可以确保即使出现异常,资源也会被正确释放。

不适合使用 with 的情况

如果你需要在多个不同的逻辑步骤中使用信号量或其他资源,可能更适合手动管理:

  1. 分散的操作
    • 如果你的逻辑分散在多个地方,你可能希望在不同条件下释放信号量,或者在获取信号量后执行某些操作而没有立即完成。
  2. 多个条件分支
    • 例如,如果你有多个条件下的逻辑,其中不是每个条件都应该释放信号量,手动管理会更灵活。

示例

以下是一个不适合使用 with 的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import threading
import time

semaphore = threading.Semaphore(2)

def worker(identifier):
    print(f"Worker {identifier} is trying to acquire the semaphore.")
    semaphore.acquire()  # 手动获取信号量
    try:
        print(f"Worker {identifier} has acquired the semaphore.")
        # 可能在这里做一些操作
        time.sleep(1)
        if identifier == 1:
            print(f"Worker {identifier} needs to do something and will release the semaphore.")
            # 在此释放信号量,但并不做完整的操作
            semaphore.release()
            return  # 提前返回
        # 完成其他操作
        print(f"Worker {identifier} has completed its task.")
    finally:
        # 这里,以保障最后总会释放信号量
        semaphore.release()  # 确保最后释放信号量

# 启动多个线程
threads = []
for i in range(3):
    thread = threading.Thread(target=worker, args=(i,))
    threads.append(thread)
    thread.start()

# 等待所有线程完成
for thread in threads:
    thread.join()

print("All workers have finished.")

在这个示例中,信号量的释放并不是总是在一个统一的代码块中完成的,因此没有使用 with 语句。而是用 tryfinally 确保资源在不论何种情况下都能被释放。

总结

  • 当你的资源获取和释放是局部的、分开的,或者在不同情况下需做不同处理时,手动调用 acquire()release() 是更好的选择。
  • 而当资源的使用在一段逻辑中是连续的且不易引发异常,使用 with 语句会更简洁和安全。选择合适的资源管理方式,能提高代码的可读性和可维护性。 在Python的threading模块中,Condition对象是一种用于线程间同步的原语,通常用于一种生产者-消费者模式。它允许一个或多个线程通过条件变量协调其执行。条件变量是用来实现线程间的通信和同步的机制。

主要功能

Condition对象的关键功能是允许线程等待某个条件发生,并在该条件发生后被唤醒。它是以Lock作为基础构建的,因此可以用来同步对共享资源的访问。同时,Condition可以绑定一个条件表达式,使得线程在某种特定条件下进行等待或者被唤醒。

主要方法

  1. wait(timeout=None):
    • 线程调用此方法时会释放与Condition相关联的锁,并进入等待状态,直到被通知(如调用 notify()notify_all())或者超时。
    • 一旦被通知,它会重新获得锁,并继续执行。
  2. notify(n=1):
    • 唤醒一个等待在该条件上的线程。如果有多个线程在等待,可以传递参数n来指定最多唤醒n个线程。
  3. notify_all():
    • 唤醒所有等待在该条件上的线程。

使用示例

以下是一个使用Condition实现生产者-消费者模式的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import threading
import time
import random

class ProducerConsumer:
    def __init__(self):
        self.condition = threading.Condition()
        self.buffer = []
        self.buffer_size = 5

    def producer(self):
        while True:
            item = random.randint(1, 100)  # 模拟生产一个物品
            with self.condition:
                while len(self.buffer) >= self.buffer_size:  # 如果缓冲区满
                    print("Buffer is full, producer is waiting...")
                    self.condition.wait()  # 等待消费者消费

                print(f"Producer produced: {item}")
                self.buffer.append(item)  # 生产物品
                self.condition.notify()  # 通知消费者

            time.sleep(random.random())  # 模拟生产时间

    def consumer(self):
        while True:
            with self.condition:
                while not self.buffer:  # 如果缓冲区空
                    print("Buffer is empty, consumer is waiting...")
                    self.condition.wait()  # 等待生产者生产

                item = self.buffer.pop(0)  # 消费物品
                print(f"Consumer consumed: {item}")
                self.condition.notify()  # 通知生产者

            time.sleep(random.random())  # 模拟消费时间

# 启动生产者和消费者线程
pc = ProducerConsumer()

producer_thread = threading.Thread(target=pc.producer)
consumer_thread = threading.Thread(target=pc.consumer)

producer_thread.start()
consumer_thread.start()

producer_thread.join()
consumer_thread.join()

工作原理

  1. 生产者
    • 生产者不断尝试产生新的物品。首先它会检查缓冲区是否已满,如果缓冲区满,生产者会调用self.condition.wait(),此时释放锁并进入等待状态。
    • 当消费者消费了物品并调用notify()后,生产者会被唤醒,重新获得锁,继续生产。
  2. 消费者
    • 消费者检查缓冲区是否为空。如果为空,消费者调用self.condition.wait(),释放锁并进入等待状态。
    • 当生产者放置了物品并调用notify()后,消费者会被唤醒,重新获得锁,继续消费。

总结

  • 条件变量(Condition) 有助于实现更复杂的多线程同步机制,适合用于需要多个线程相互协调的场景,例如生产者-消费者模型。
  • Condition 允许线程在某种条件下队列等待,并在条件满足时得到通知以继续执行。
  • 使用锁结合条件变量可以有效避免死锁和其他竞争条件,确保线程安全地访问共享资源。
This post is licensed under CC BY 4.0 by the author.