python 线程锁

python 多线程
python 线程池
python 线程锁


转载:
https://zhuanlan.zhihu.com/p/2157113684

1. 线程锁概述

1.1. 线程锁的基本概念

线程锁是一种同步原语,用于协调多个线程对共享资源的访问。它的主要目的是防止多个线程同时修改同一资源,从而避免数据竞争和不一致性问题。
线程锁的基本工作原理如下:

  • 当一个线程需要访问共享资源时,它首先尝试获取锁。
  • 如果锁是可用的,该线程获得锁,并可以安全地访问共享资源。
  • 如果锁已被其他线程持有,当前线程将被阻塞,直到锁被释放。
  • 线程完成对共享资源的访问后,必须释放锁,以允许其他线程访问该资源。

通过这种机制,线程锁确保在任何给定时刻,只有一个线程可以访问受保护的共享资源,从而维护了数据的一致性和完整性。

1.2. 线程锁的使用场景

线程锁在多线程编程中有广泛的应用,以下是一些常见的使用场景:

  • 保护共享数据: 当多个线程需要读写同一数据结构时,使用锁可以防止数据竞争和不一致性。
  • 实现线程安全的数据结构: 例如,创建线程安全的队列、栈或字典。
  • 控制资源访问: 限制对数据库连接、文件句柄等有限资源的并发访问。
  • 实现同步机制: 在特定条件满足时才允许线程继续执行。
  • 避免竞态条件: 在检查然后设置(check-then-act)操作中确保原子性。
  • 实现生产者-消费者模式: 协调生产者和消费者线程之间的交互。
  • 实现读写锁: 允许多个读操作同时进行,但写操作需要独占访问。
  • 限制并发度: 使用信号量控制同时运行的线程数量。
  • 线程同步: 使用事件或条件变量在线程间传递信号。
  • 防止死锁: 通过合理使用锁的层次结构和超时机制来避免死锁情况。

1.3. 线程锁的类型

Python 的 threading 模块提供了多种类型的线程锁,每种类型都有其特定的用途和特性。以下是 Python 中最常用的线程锁类型:

1.3.1. threading.Lock()

threading.Lock() 是最基本的锁类型,也称为互斥锁(Mutex)。它有两种状态:锁定和未锁定。一旦一个线程获得了锁,其他试图获取该锁的线程将被阻塞,直到锁被释放。

1.3.2. threading.RLock()

threading.RLock() 是可重入锁(Reentrant Lock)的缩写。与普通的 Lock 不同,RLock 允许同一个线程多次获取同一个锁,而不会导致死锁。这在递归调用或嵌套锁定的场景中特别有用。

1.3.3. threading.Semaphore()

信号量(Semaphore)是一种更高级的同步原语,它维护一个内部计数器。当计数器大于零时,线程可以获取信号量;当计数器为零时,线程将被阻塞。信号量通常用于限制对资源的并发访问数量。

1.3.4. threading.Event()

threading.Event() 提供了一种简单的线程通信机制。一个事件对象管理一个内部标志,线程可以等待这个标志被设置,或者将其设置(或清除)。这对于一个线程向其他线程发出事件发生的信号很有用。

1.3.5. threading.Condition()

条件变量(Condition)结合了锁和事件通知的功能。它允许线程等待某个条件成立,并在条件满足时得到通知。这在生产者-消费者问题等场景中非常有用。

2. 线程锁实践

2.1. 使用 threading.Lock()

2.1.1. 基本使用

我们首先来看一个使用 threading.Lock() 的基本示例。这个例子展示了如何使用锁来保护共享资源,确保多个线程安全地更新一个计数器。

import threading
import time

class Counter:
    def __init__(self):
        self.count = 0
        self.lock = threading.Lock()
    
    def increment(self):
        with self.lock:
            self.count += 1
            time.sleep(0.1)  # 模拟一些处理时间
    
    def get_count(self):
        with self.lock:
            return self.count

def worker(counter, num):
    for idx in range(num):
        # print(threading.current_thread(), idx)
        counter.increment()

counter = Counter()

# 创建5个线程, 每个线程增加计数器10次
threads = [threading.Thread(target=worker, args=(counter, 10))
            for _ in range(5)]
for t in threads:
    t.start()

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

print(f"Final count: {counter.get_count()}")

运行结果:

Final count: 50

在这个例子中:

  • 我们定义了一个 Counter 类,它使用 threading.Lock() 来保护 count 变量。
  • increment 方法使用 with 语句来获取和释放锁,确保在增加计数时不会发生竞争条件。
  • 我们创建了5个线程,每个线程调用 increment 方法10次。
  • 最终的计数结果是50,这证明了锁成功地防止了数据竞争。

如果我们不使用锁,由于线程调度和上下文切换的不确定性,最终的计数可能会小于50。

2.1.2. 使用线程锁实现线程安全的单例模式

单例模式是一种常用的设计模式,确保一个类只有一个实例。在多线程环境中,实现线程安全的单例模式需要使用锁。以下是一个使用线程锁实现线程安全单例模式的例子:

import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # 再次检查,因为可能有多个线程同时通过了第一次检查
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        # 初始化代码(如果需要)
        pass

def worker():
    singleton = Singleton()
    print(f"Thread {threading.current_thread().name} got instance {id(singleton)}")

# 创建多个线程来测试单例
threads = [threading.Thread(target=worker) for _ in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

print("All threads finished")

运行结果:

Thread Thread-1 (worker) got instance 2867860092384
Thread Thread-2 (worker) got instance 2867860092384
Thread Thread-3 (worker) got instance 2867860092384
Thread Thread-4 (worker) got instance 2867860092384
Thread Thread-5 (worker) got instance 2867860092384
Thread Thread-7 (worker) got instance 2867860092384
Thread Thread-8 (worker) got instance 2867860092384
Thread Thread-10 (worker) got instance 2867860092384
Thread Thread-9 (worker) got instance 2867860092384
Thread Thread-6 (worker) got instance 2867860092384
All threads finished

在这个例子中:

  • 我们使用了双重检查锁定(Double-Checked Locking)模式来实现线程安全的单例。
  • 第一次检查在锁外面,以避免每次获取实例时都需要获取锁。
  • 如果第一次检查通过,我们获取锁并再次检查,以确保在获取锁的过程中没有其他线程创建实例。
  • 如果第二次检查也通过,我们创建实例。
  • 我们创建了10个线程,每个线程都尝试获取单例实例。
  • 输出显示所有线程获得的都是同一个实例(相同的内存地址)。

这个例子展示了如何使用线程锁来确保在多线程环境中只创建一个单例实例。这种模式在需要全局唯一资源(如配置管理器、数据库连接池等)的场景中非常有用。

2.2. 使用 threading.RLock()

2.2.1. 使用 RLock 实现递归调用

接下来,让我们看一个使用 threading.RLock() 的例子。这个例子展示了可重入锁如何允许同一个线程多次获取锁而不导致死锁。

import threading

class RecursiveCounter:
    def __init__(self):
        self.count = 0
        self.lock = threading.RLock()
    
    def increment(self, n):
        with self.lock:
            if n > 0:
                self.count += 1
                self.increment(n - 1)  # 递归调用
    
    def get_count(self):
        with self.lock:
            return self.count

def worker(counter, num):
    counter.increment(num)

counter = RecursiveCounter()
threads = []

# 创建5个线程, 每个线程增加计数器10次
threads = [threading.Thread(target=worker, args=(counter, 10))
            for _ in range(5)]
for t in threads:
    t.start()
    
# 等待所有线程完成
for t in threads:
    t.join()

print(f"Final count: {counter.get_count()}")

运行结果:

Final count: 50

在这个例子中:

  • RecursiveCounter 类使用 threading.RLock() 作为锁。
  • increment 方法是递归的,它在持有锁的情况下多次调用自己。
  • 由于使用了 RLock,同一个线程可以多次获取这个锁而不会导致死锁。
  • 我们创建了5个线程,每个线程递归地增加计数器10次。
  • 最终的计数是50,证明所有的递归调用都成功执行了。

如果我们在这个例子中使用普通的 Lock 而不是 RLock,程序将会陷入死锁,因为 Lock 不允许同一个线程多次获取锁。

2.2.2. 使用 RLock 实现可重入锁

可重入锁允许同一个线程多次获取同一个锁,而不会导致死锁。python 的threading.RLock 提供了这种功能。以下是一个使用 RLock 的例子:

import threading
import time

class ReentrantResource:
    def __init__(self):
        self._lock = threading.RLock()
        self._data = 0

    def outer_operation(self):
        with self._lock:
            print(f"Thread {threading.current_thread().name} acquired lock in outer_operation")
            time.sleep(0.1)  # 模拟一些操作
            self._data += 1
            self.inner_operation()

    def inner_operation(self):
        with self._lock:
            print(f"Thread {threading.current_thread().name} acquired lock in inner_operation")
            time.sleep(0.1)  # 模拟一些操作
            self._data += 1

def worker(resource):
    for _ in range(3):
        resource.outer_operation()
        time.sleep(0.1)

# 创建一个可重入资源
resource = ReentrantResource()

# 创建多个线程
threads = [threading.Thread(target=worker, args=(resource,)) for _ in range(3)]
# 启动所有线程
for t in threads:
    t.start()

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

print(f"Final data value: {resource._data}")

运行结果:

Thread Thread-1 (worker) acquired lock in outer_operation
Thread Thread-1 (worker) acquired lock in inner_operation
Thread Thread-2 (worker) acquired lock in outer_operation
Thread Thread-2 (worker) acquired lock in inner_operation
Thread Thread-3 (worker) acquired lock in outer_operation
Thread Thread-3 (worker) acquired lock in inner_operation
Thread Thread-1 (worker) acquired lock in inner_operation
Thread Thread-2 (worker) acquired lock in outer_operation
Thread Thread-2 (worker) acquired lock in inner_operation
Thread Thread-3 (worker) acquired lock in outer_operation
Thread Thread-3 (worker) acquired lock in inner_operation
Thread Thread-1 (worker) acquired lock in outer_operation
Thread Thread-1 (worker) acquired lock in inner_operation
Thread Thread-2 (worker) acquired lock in outer_operation
Thread Thread-2 (worker) acquired lock in inner_operation
Thread Thread-3 (worker) acquired lock in outer_operation
Thread Thread-3 (worker) acquired lock in inner_operation
Final data value: 18

在这个例子中:

  • 我们定义了一个 ReentrantResource 类,它使用 threading.RLock() 作为锁。
  • outer_operation 方法获取锁,然后调用 inner_operation 方法。
  • inner_operation 方法也尝试获取同一个锁。
  • 由于使用的是 RLock,同一个线程可以在 inner_operation 中再次获取锁,而不会导致死锁。
  • 我们创建了3个工作线程,每个线程调用 outer_operation 三次。

这个例子展示了 RLock 的可重入特性。如果我们使用普通的 Lock 而不是 RLock,程序将会在 inner_operation 尝试获取锁时陷入死锁。

2.3. 使用 threading.Semaphore()

2.3.1. 基础使用

现在让我们来看一个使用 threading.Semaphore() 的例子。信号量常用于限制对资源的并发访问数量。在这个例子中,我们将模拟一个简单的连接池。

import threading
import time
import random

class ConnectionPool:
    def __init__(self, max_connections):
        self.semaphore = threading.Semaphore(max_connections)
        self.connections = []
        for i in range(max_connections):
            self.connections.append(f"Connection-{i}")
    
    def get_connection(self):
        self.semaphore.acquire()
        return self.connections.pop()
    
    def release_connection(self, connection):
        self.connections.append(connection)
        self.semaphore.release()

def worker(pool):
    connection = pool.get_connection()
    print(f"Thread {threading.current_thread().name} acquired {connection}")
    time.sleep(random.uniform(0.1, 0.3))  # 模拟使用连接
    pool.release_connection(connection)
    print(f"Thread {threading.current_thread().name} released {connection}")

# 创建一个最多允许3个并发连接的连接池
pool = ConnectionPool(3)

# 创建5个线程, 每个线程增加计数器10次
threads = [threading.Thread(target=worker, args=(pool,), name=f"Thread-{idx}")
            for idx in range(5)]
for t in threads:
    t.start()

for t in threads:
    t.join()

运行结果:

Thread Thread-0 acquired Connection-2
Thread Thread-1 acquired Connection-1
Thread Thread-2 acquired Connection-0
Thread Thread-1 released Connection-1
Thread Thread-3 acquired Connection-1
Thread Thread-2 released Connection-0
Thread Thread-4 acquired Connection-0
Thread Thread-0 released Connection-2
Thread Thread-3 released Connection-1
Thread Thread-4 released Connection-0

在这个例子中:

  • 我们创建了一个 ConnectionPool 类,使用 Semaphore 来限制同时活跃的连接数量。
  • get_connection 方法获取一个信号量,如果没有可用的信号量,线程将被阻塞。
  • release_connection 方法释放一个信号量,允许其他线程获取连接。
  • 我们创建了5个线程,但连接池最多只允许3个并发连接。
  • 输出显示,尽管有5个线程,但在任何时候最多只有3个线程同时持有连接。

这个例子展示了如何使用信号量来控制对有限资源的访问,这在管理数据库连接、线程池等场景中非常有用。

2.3.2. 使用 Semaphore 控制资源访问

信号量((Semaphore)是一种更加灵活的同步原语,它可以控制同时访问某个资源的线程数量。以下是一个使用 Semaphore 来限制并发数的例子:

import threading
import time
import random

class LimitedResource:
    def __init__(self, max_workers):
        self.semaphore = threading.Semaphore(max_workers)
        self.active_workers = 0
        self.lock = threading.Lock()

    def use_resource(self, worker_id):
        with self.semaphore:
            self.increment_active_workers()
            print(f"Worker {worker_id} is using the resource. Active workers: {self.active_workers}")
            time.sleep(random.uniform(0.5, 1.5))  # 模拟资源使用
            self.decrement_active_workers()

    def increment_active_workers(self):
        with self.lock:
            self.active_workers += 1

    def decrement_active_workers(self):
        with self.lock:
            self.active_workers -= 1

def worker(resource, worker_id):
    for _ in range(3):
        resource.use_resource(worker_id)
        time.sleep(random.uniform(0.1, 0.3))  # 随机等待一段时间

# 创建一个最多允许3个工作线程同时使用的资源
limited_resource = LimitedResource(3)

# 创建10个工作线程
threads = [threading.Thread(target=worker, args=(limited_resource, i)) for i in range(5)]
# 启动所有线程
for t in threads:
    t.start()

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

print("All workers have finished.")

运行结果:

Worker 0 is using the resource. Active workers: 1
Worker 1 is using the resource. Active workers: 2
Worker 2 is using the resource. Active workers: 3
Worker 3 is using the resource. Active workers: 3
Worker 4 is using the resource. Active workers: 3
Worker 1 is using the resource. Active workers: 3
Worker 2 is using the resource. Active workers: 3
Worker 0 is using the resource. Active workers: 3
Worker 3 is using the resource. Active workers: 3
Worker 4 is using the resource. Active workers: 3
Worker 2 is using the resource. Active workers: 3
Worker 1 is using the resource. Active workers: 3
Worker 0 is using the resource. Active workers: 3
Worker 3 is using the resource. Active workers: 3
Worker 4 is using the resource. Active workers: 3
All workers have finished.

在这个例子中:

  • 我们定义了一个 LimitedResource 类,它使用 Semaphore 来限制同时访问资源的线程数量。
  • use_resource 方法使用 with self.semaphore 来获取信号量,确保同时最多只有指定数量的线程可以访问资源。
  • 我们使用额外的锁来保护 active_workers 计数器的增减操作。
  • 创建了5个工作线程,但资源同时最多只允许3个线程访问。
  • 每个工作线程尝试使用资源3次。

这个例子展示了如何使用 Semaphore 来控制对共享资源的并发访问。这种模式在限制数据库连接数、控制API请求频率等场景中非常有用。

2.4. 使用 threading.Event()

threading.Event() 提供了一种简单的线程通信机制。下面的例子展示了如何使用事件来协调多个工作线程和一个控制线程。

import threading
import time
import random

def worker(name, event):
    while not event.is_set():
        print(f"{name} is working")
        time.sleep(1+random.uniform(0.1, 0.9))
    print(f"{name} received stop event. Cleaning up.")
    time.sleep(0.1)
    print(f"{name} finished cleanup.")

def controller(event, duration):
    print(f"Controller: letting workers run for {duration} seconds")
    time.sleep(duration)
    print("Controller: sending stop event")
    event.set()

# 创建一个事件对象
stop_event = threading.Event()
# 创建并启动工作线程
workers = [threading.Thread(target=worker, args=(f"Worker-{idx}", stop_event))
            for idx in range(5)]
for t in workers:
    t.start()

# 创建并启动控制线程
controller_thread = threading.Thread(target=controller, args=(stop_event, 3))
controller_thread.start()

# 等待所有线程完成
for t in workers:
    t.join()
controller_thread.join()

print("All threads have finished.")

运行结果:

Worker-0 is working
Worker-1 is working
Worker-2 is working
Worker-3 is working
Worker-4 is working
Controller: letting workers run for 3 seconds
Worker-4 is working
Worker-2 is working
Worker-3 is working
Worker-1 is working
Worker-0 is working
Worker-2 is working
Worker-3 is working
Worker-1 is working
Worker-4 is working
Controller: sending stop event
Worker-0 received stop event. Cleaning up.
Worker-0 finished cleanup.
Worker-2 received stop event. Cleaning up.
Worker-2 finished cleanup.
Worker-4 received stop event. Cleaning up.
Worker-4 finished cleanup.
Worker-3 received stop event. Cleaning up.
Worker-3 finished cleanup.
Worker-1 received stop event. Cleaning up.
Worker-1 finished cleanup.
All threads have finished.

在这个例子中:

  • 我们创建了一个 Event 对象 stop_event。
  • 工作线程在一个循环中运行,直到事件被设置。
  • 控制线程等待3秒后设置事件,通知所有工作线程停止。
  • 当事件被设置后,工作线程执行清理操作并退出。

这个例子展示了如何使用事件来协调多个线程的行为,这在需要同时停止多个线程或等待某个条件发生时非常有用。

2.5. 使用 threading.Condition()

2.5.1. 简单的生产者-消费者模式

threading.Condition 结合了锁和通知机制,非常适合用于生产者-消费者模式。下面的例子展示了如何使用条件变量来实现一个有界缓冲区。

import threading
import time
import random

class BoundedBuffer:
    def __init__(self, size):
        self.buffer = []
        self.size = size
        self.condition = threading.Condition()
    
    def produce(self, item):
        with self.condition:
            while len(self.buffer) == self.size:
                print("Buffer is full. Producer is waiting.")
                self.condition.wait()
            self.buffer.append(item)
            print(f"Produced {item}. Buffer size: {len(self.buffer)}")
            self.condition.notify()
    
    def consume(self):
        with self.condition:
            while len(self.buffer) == 0:
                print("Buffer is empty. Consumer is waiting.")
                self.condition.wait()
            item = self.buffer.pop(0)
            print(f"Consumed {item}. Buffer size: {len(self.buffer)}")
            self.condition.notify()
            return item

def producer(buffer, items):
    for i in items:
        time.sleep(random.uniform(0.1, 0.3))  # 模拟生产时间
        buffer.produce(i)

def consumer(buffer, num_items):
    for _ in range(num_items):
        time.sleep(random.uniform(0.1, 0.3))  # 模拟消费时间
        buffer.consume()

# 创建一个大小为5的缓冲区
buffer = BoundedBuffer(5)

# 创建生产者和消费者线程
producer_thread = threading.Thread(target=producer, args=(buffer, range(5)))
consumer_thread = threading.Thread(target=consumer, args=(buffer, 5))

# 启动线程
producer_thread.start()
consumer_thread.start()

# 等待线程完成
producer_thread.join()
consumer_thread.join()

print("Production and consumption completed.")

运行结果:

Produced 0. Buffer size: 1
Consumed 0. Buffer size: 0
Produced 1. Buffer size: 1
Consumed 1. Buffer size: 0
Produced 2. Buffer size: 1
Consumed 2. Buffer size: 0
Produced 3. Buffer size: 1
Consumed 3. Buffer size: 0
Buffer is empty. Consumer is waiting.
Produced 4. Buffer size: 1
Consumed 4. Buffer size: 0
Production and consumption completed.

在这个例子中:

  • 我们创建了一个 BoundedBuffer 类,使用 Condition 来同步生产者和消费者。
  • produce 方法在缓冲区满时等待,在生产后通知消费者。
  • consume 方法在缓冲区空时等待,在消费后通知生产者。
  • 生产者线程生产5个项目,消费者线程消费5个项目。
  • Condition 的 wait 和 notify 方法用于线程间的通信。

这个例子展示了如何使用条件变量来协调生产者和消费者之间的活动,确保在缓冲区满或空时,相应的线程会等待。

2.5.2. 复杂的生产者-消费者模式

以下是一个使用 threading.Condition 实现的更复杂的生产者-消费者示例,包含多个生产者和消费者:

import threading
import queue
import time
import random

class SharedBuffer:
    def __init__(self, size):
        self.queue = queue.Queue(size)
        self.condition = threading.Condition()
        self.max_size = size

    def put(self, item):
        with self.condition:
            while self.queue.full():
                self.condition.wait()
            self.queue.put(item)
            self.condition.notify()

    def get(self):
        with self.condition:
            while self.queue.empty():
                self.condition.wait()
            item = self.queue.get()
            self.condition.notify()
            return item
        
def producer(buffer, id):
    for i in range(3):
        item = f"Item-{id}-{i}"
        time.sleep(random.uniform(0.1, 0.3))  # 模拟生产时间
        buffer.put(item)
        print(f"Producer {id} produced {item}")

def consumer(buffer, id):
    for _ in range(6):
        time.sleep(random.uniform(0.1, 0.3))  # 模拟消费时间
        item = buffer.get()
        print(f"Consumer {id} consumed {item}")

# 创建一个共享缓冲区
shared_buffer = SharedBuffer(5)
# 创建生产者和消费者线程
producers = [threading.Thread(target=producer, args=(shared_buffer, i)) for i in range(4)]
consumers = [threading.Thread(target=consumer, args=(shared_buffer, i)) for i in range(2)]

# 启动所有线程
for p in producers:
    p.start()
for c in consumers:
    c.start()

# 等待所有线程完成
for p in producers:
    p.join()
for c in consumers:
    c.join()

print("All producers and consumers have finished.")

运行结果:

Producer 2 produced Item-2-0
Producer 1 produced Item-1-0
Consumer 0 consumed Item-2-0
Producer 0 produced Item-0-0
Consumer 1 consumed Item-1-0
Producer 3 produced Item-3-0
Producer 1 produced Item-1-1
Consumer 0 consumed Item-0-0
Producer 2 produced Item-2-1
Producer 0 produced Item-0-1
Consumer 1 consumed Item-3-0
Producer 1 produced Item-1-2
Producer 0 produced Item-0-2
Consumer 0 consumed Item-1-1
Producer 2 produced Item-2-2
Consumer 1 consumed Item-2-1
Producer 3 produced Item-3-1
Consumer 1 consumed Item-0-1
Producer 3 produced Item-3-2
Consumer 0 consumed Item-1-2
Consumer 0 consumed Item-0-2
Consumer 1 consumed Item-2-2
Consumer 0 consumed Item-3-1
Consumer 1 consumed Item-3-2
All producers and consumers have finished.

在这个例子中:

  • 我们定义了一个 SharedBuffe r类,,它使用 queue.Queue 来存储项目,并使用 threading.Condition 来同步生产者和消费者。
  • put 方法在缓冲区满时等待,get 方法在缓冲区空时等待。
  • 我们创建了4个生产者线程和2个消费者线程。
  • 每个生产者生产3个项目,每个消费者消费6个项目。
  • 使用 Condition 的 wait 和 notify 方法来协调生产者和消费者之间的活动。

这个例子展示了如何在更复杂的场景中使用线程锁和条件变量。它模拟了一个更现实的生产者-消费者系统,其中有多个生产者和消费者同时工作。

2.6. 线程锁与上下文管理器

python 的 with 语句提供了一种优雅的方式来自动管理资源的获取和释放。对于线程锁,使用上下文管理器可以确保锁总是被正确释放,即使在出现异常的情况下也是如此。以下是一个使用自定义上下文管理器的例子:

import threading
import time
from contextlib import contextmanager

class CustomLock:
    def __init__(self):
        self._lock = threading.Lock()
        self.locked_by = None
    
    @contextmanager
    def acquire_timeout(self, timeout):
        result = self._lock.acquire(timeout=timeout)
        try:
            if result:
                self.locked_by = threading.current_thread().name
                yield True
            else:
                yield False
        finally:
            if result:
                self.locked_by = None
                self._lock.release()

def worker(lock, worker_id):
    for _ in range(3):
        with lock.acquire_timeout(timeout=0.2) as acquired:
            if acquired:
                print(f"Worker {worker_id} acquired the lock")
                time.sleep(0.1)  # 模拟工作
                print(f"Worker {worker_id} releasing the lock")
            else:
                print(f"Worker {worker_id} couldn't acquire the lock")
        time.sleep(0.1)  # 在尝试之间稍作等待

lock = CustomLock()

# 创建并启动工作线程
threads = [threading.Thread(target=worker, args=(lock, idx))
            for idx in range(5)]
for t in threads:
    t.start()

for t in threads:
    t.join()

print("All workers finished")

运行结果:

Worker 0 acquired the lock
Worker 0 releasing the lock
Worker 1 acquired the lock
Worker 2 couldn't acquire the lock
Worker 3 couldn't acquire the lock
Worker 1 releasing the lock
Worker 4 couldn't acquire the lock
Worker 0 acquired the lock
Worker 0 releasing the lock
Worker 4 acquired the lock
Worker 4 releasing the lock
Worker 1 acquired the lock
Worker 2 couldn't acquire the lock
Worker 3 couldn't acquire the lock
Worker 1 releasing the lock
Worker 0 acquired the lock
Worker 0 releasing the lock
Worker 4 acquired the lock
Worker 4 releasing the lock
Worker 3 acquired the lock
Worker 2 couldn't acquire the lock
Worker 1 couldn't acquire the lock
Worker 3 releasing the lock
All workers finished

在这个例子中:

  • 我们定义了一个 CustomLock 类,它封装了一个 threading.Lock 对象。
  • acquire_timeout 方法是一个上下文管理器,它尝试在指定的超时时间内获取锁。
  • 如果成功获取锁,它会记录获取锁的线程名称,并在退出上下文时自动释放锁。
  • 工作线程使用 with 语句和 acquire_timeout 方法来尝试获取锁。
  • 如果无法在超时时间内获取锁,线程会继续执行而不会被阻塞。

这个例子展示了如何使用上下文管理器来简化锁的使用,并添加额外的功能(如超时和锁持有者跟踪)。这种方法可以使代码更加清晰、安全,并且更容易管理锁的生命周期。

2.7. 死锁问题及其解决

死锁是多线程编程中的一个常见问题。下面的例子首先展示一个死锁情况,然后提供一个解决方案。

  • 死锁实例
import threading
import time

# 死锁示例
def deadlock_example():
    lock1 = threading.Lock()
    lock2 = threading.Lock()

    def thread1_function():
        print("Thread 1 starting...")
        with lock1:
            print("Thread 1 acquired lock1")
            time.sleep(0.5)
            with lock2:
                print("Thread 1 acquired lock2")
        print("Thread 1 finished")

    def thread2_function():
        print("Thread 2 starting...")
        with lock2:
            print("Thread 2 acquired lock2")
            time.sleep(0.5)
            with lock1:
                print("Thread 2 acquired lock1")
        print("Thread 2 finished")

    t1 = threading.Thread(target=thread1_function)
    t2 = threading.Thread(target=thread2_function)

    t1.start()
    t2.start()

    t1.join()
    t2.join()

print("Demonstrating deadlock:")
deadlock_example()
print("This will hang due to deadlock")

运行结果:

Demonstrating deadlock:
Thread 1 starting...
Thread 1 acquired lock1
Thread 2 starting...
Thread 2 acquired lock2
# 阻塞在这里
  • 解决死锁
import threading
import time

# 解决死锁的示例
def resolved_deadlock_example():
    lock1 = threading.Lock()
    lock2 = threading.Lock()

    def acquire_locks(lock_1, lock_2):
        while True:
            have_lock1 = lock_1.acquire(blocking=False)
            if have_lock1:
                try:
                    have_lock2 = lock_2.acquire(blocking=False)
                    if have_lock2:
                        return True
                    else:
                        lock_1.release()
                except:
                    lock_1.release()
            time.sleep(0.1)

    def thread1_function():
        print("Thread 1 starting...")
        if acquire_locks(lock1, lock2):
            print("Thread 1 acquired both locks")
            lock2.release()
            lock1.release()
        print("Thread 1 finished")

    def thread2_function():
        print("Thread 2 starting...")
        if acquire_locks(lock2, lock1):
            print("Thread 2 acquired both locks")
            lock1.release()
            lock2.release()
        print("Thread 2 finished")

    t1 = threading.Thread(target=thread1_function)
    t2 = threading.Thread(target=thread2_function)

    t1.start()
    t2.start()

    t1.join()
    t2.join()

print("Demonstrating resolved deadlock:")
resolved_deadlock_example()
print("Deadlock resolved successfully")

运行结果:

Demonstrating resolved deadlock:
Thread 1 starting...
Thread 1 acquired both locks
Thread 2 starting...
Thread 1 finished
Thread 2 acquired both locks
Thread 2 finished
Deadlock resolved successfully

在这个例子中:

  • 第一部分演示了一个典型的死锁情况,其中两个线程以不同的顺序尝试获取两个锁。
  • 第二部分展示了如何通过使用非阻塞的锁获取方式和重试机制来解决死锁问题。
  • 解决方案中的 acquire_locks 函数尝试以非阻塞的方式获取两个锁,如果无法同时获得两个锁,就释放已获得的锁并重试。

这个例子说明了死锁是如何发生的,以及如何通过改变锁的获取策略来避免死锁。在实际应用中,还可以考虑使用超时机制或锁的层次结构来进一步降低死锁风险。

3. 线程锁的最佳实践

在使用Python线程锁时,以下是一些最佳实践:

  • 最小化锁的范围: 只在必要的代码段上使用锁,以减少线程等待时间和提高并发性。
  • 避免死锁:
    总是以相同的顺序获取多个锁。
    使用超时机制。
    考虑使用更高级的同步原语,如 threading.Condition 或 queue.Queue。
  • 使用上下文管理器: 使用with语句来自动管理锁的获取和释放,这可以防止忘记释放锁。
  • 选择合适的锁类型:
    使用 threading.Lock 用于简单的互斥。
    使用 threading.RLock 当同一线程需要多次获取锁时。
    使用 threading.Semaphore 控制资源访问的线程数。
  • 考虑使用 queue.Queue: 对于生产者-消费者模式,Queue 提供了内置的线程安全性。
  • 避免过度使用锁: 过多的锁可能导致性能下降。考虑使用无锁算法或其他并发模式。
  • 使用threading.local(): 对于线程特定的数据,使用 threading.local() 可以避免需要显式的锁。
  • 定期审查和测试: 定期审查代码以确保锁的使用是正确和高效的。使用压力测试来验证线程安全性。
  • 考虑使用更高级的并发工具: 对于复杂的并发场景,考虑使用 asyncio 或concurrent.futures 模块。
  • 文档化锁的使用: 在代码注释中清楚地说明每个锁的目的和使用方式,以便其他开发者理解和维护。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值