跳至主要內容

19. 并发编程中的常见问题与解决方案

小白debug大约 6 分钟

19. 并发编程中的常见问题与解决方案

我们将深入探讨并发编程中可能会遇到的一些常见问题,以及如何巧妙地解决它们。我们将以简单易懂的方式向您介绍这些概念,让您能够在编写 Python 代码时更加从容。

问题 1:竞态条件

问题描述:

在并发编程中,当两个或多个线程同时访问共享资源时,由于执行顺序的不确定性,可能会导致程序的行为出现问题。

问题示例代码:

import threading

shared_resource = 0

def increment():
    global shared_resource
    for _ in range(1000000):
        shared_resource += 1

def decrement():
    global shared_resource
    for _ in range(1000000):
        shared_resource -= 1

# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=decrement)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

print("Shared Resource:", shared_resource)

问题原因:

在上述代码中,shared_resource += 1shared_resource -= 1 这两行代码并不是一个原子操作,它们在底层会被拆分成多个步骤,因此可能会导致竞态条件。

解决方案:

使用锁(Lock)来保护共享资源,确保同一时刻只有一个线程能够访问它。

解决方案示例代码:

import threading

shared_resource = 0
lock = threading.Lock()

def increment():
    global shared_resource
    for _ in range(1000000):
        lock.acquire()
        shared_resource += 1
        lock.release()

def decrement():
    global shared_resource
    for _ in range(1000000):
        lock.acquire()
        shared_resource -= 1
        lock.release()

# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=decrement)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

print("Shared Resource:", shared_resource)

问题 2:死锁

问题描述:

当多个线程同时等待某些资源的释放时,可能会发生死锁,导致程序无法继续执行。

问题示例代码:

import threading

# 创建两个锁
lock1 = threading.Lock()
lock2 = threading.Lock()

def acquire_lock1_then_lock2():
    lock1.acquire()
    lock2.acquire()
    lock2.release()
    lock1.release()

def acquire_lock2_then_lock1():
    lock2.acquire()
    lock1.acquire()
    lock1.release()
    lock2.release()

# 创建两个线程
thread1 = threading.Thread(target=acquire_lock1_then_lock2)
thread2 = threading.Thread(target=acquire_lock2_then_lock1)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

print("Execution Completed")

问题原因:

在上述代码中,acquire_lock1_then_lock2 函数先尝试获取 lock1,再尝试获取 lock2,而 acquire_lock2_then_lock1 函数则相反,先尝试获取 lock2,再尝试获取 lock1,这可能会导致死锁。

解决方案:

避免使用多个锁,并确保在获取锁的顺序上保持一致。

解决方案示例代码:

import threading

# 创建一个锁
lock = threading.Lock()

def acquire_lock1_then_lock2():
    lock.acquire()
    lock.acquire()
    lock.release()
    lock.release()

def acquire_lock2_then_lock1():
    lock.acquire()
    lock.acquire()
    lock.release()
    lock.release()

# 创建两个线程
thread1 = threading.Thread(target=acquire_lock1_then_lock2)
thread2 = threading.Thread(target=acquire_lock2_then_lock1)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

print("Execution Completed")

问题 3:数据竞争

问题描述:

当多个线程同时修改一个共享的数据结构时,可能会导致数据的不一致性。

问题示例代码:

from threading import Thread, Lock

class ThreadSafeList:
    def __init__(self):
        self._list = []

    def append(self, value):
        self._list.append(value)

    def pop(self):
        return self._list.pop()

# 使用线程安全的列表
my_list = ThreadSafeList()

def add_to_list():
    for i in range(5):
        my_list.append(i)

def remove_from_list():
    for _ in range(5):
        value = my_list.pop()
        print("Removed:", value)

# 创建两个线程
thread1 = Thread(target=add_to_list)
thread2 = Thread(target=remove_from_list)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

问题原因:

上述代码中,由于没有使用锁保护 _list 的修改,可能会导致数据的不一致性。

解决方案:

使用线程安全的数据结构,如threading.ThreadSafeList或者使用锁来保护数据的访问。

解决方案示例代码:

from threading import Thread, Lock

class ThreadSafeList:
    def __init__(self):
        self._list = []
        self._lock = Lock()

    def append(self, value):
        with self._lock:
            self._list.append(value)

    def pop(self):
        with self._lock:
            return self._list.pop()

# 使用线程安全的列表
my_list = ThreadSafeList()

def add_to_list():
    for i in range(5):
        my_list.append(i)

def remove_from_list():
    for _ in range(5):
        value = my_list.pop()
        print("Removed:", value)

# 创建两个线程
thread1 = Thread(target=add_to_list)
thread2 = Thread(target=remove_from_list)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

问题 4:使用 Event 进行线程同步

问题描述:

在某些情况下,我们需要在多个线程之间进行同步,以便在特定条件满足时通知其他线程。

问题示例代码:

import threading

event = threading.Event()

def wait_for_event():
    print("Thread is waiting for event...")
    event.wait()
    print("Event has been set!")

def set_event():
    print("no Setting the event...")


# 创建一个线程等待事件
thread1 = threading.Thread(target=wait_for_event)

# 创建另一个线程设置事件
thread2 = threading.Thread(target=set_event)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

问题原因:

上述代码中,wait_for_event 函数会等待 event 被设置,但由于没有设置事件,所以会导致第一个线程一直等待。

解决方案:

使用threading.Event()来创建一个事件对象,可以在需要时设置它以通知其他线程。

解决方案示例代码:

import threading

event = threading.Event()

def wait_for_event():
    print("Thread is waiting for event...")
    event.wait()
    print("Event has been set!")

def set_event():
    print("Setting the event...")
    event.set()

# 创建一个线程等待事件
thread1 = threading.Thread(target=wait_for_event)

# 创建另一个线程设置事件
thread2 = threading.Thread(target=set_event)

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程执行完毕
thread1.join()
thread2.join()

实战例子:模拟抢票系统

让我们通过一个简单的实例来巩固所学知识。我们将创建一个抢票系统,多个用户同时尝试购买一张票,然后检查是否会发生竞态条件。

import threading

available_tickets = 10
lock = threading.Lock()

def buy_ticket(user):
    global available_tickets
    lock.acquire()
    if available_tickets > 0:
        print(f"{user} bought a ticket!")
        available_tickets -= 1
    else:
        print(f"{user} couldn't get a ticket. Tickets sold out.")
    lock.release()

# 创建多个用户线程
users = ["Alice", "Bob", "Charlie", "David", "Eve"]
threads = [threading.Thread(target=buy_ticket, args=(user,)) for user in users]

# 启动线程
for thread in threads:
    thread.start()

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

问题:竞态条件和数据竞争的区别是啥

区别:

竞态条件通常指的是在多个线程或者进程中,由于执行顺序的不确定性导致程序行为出现问题的情况。例如,多个线程尝试同时修改一个共享变量。

数据竞争是指在多个线程同时访问共享资源时,由于缺乏同步机制导致数据的不一致性。

竞态条件通常是一种导致数据竞争的情况。虽然二者看起来相似,但竞态条件更侧重于程序执行时的逻辑问题,而数据竞争则更关注数据的正确性。

总结

通过这篇文章,我们深入了解了并发编程中的常见问题以及相应的解决方案。我们学会了如何使用锁来处理竞态条件,避免死锁,以及如何保护共享资源。

同时,我们还通过一个抢票系统的实例加深了对这些概念的理解。希望通过这篇文章,你能够更加从容地处理并发编程中的问题,写出更加稳定和可靠的代码。

训练营看视频