了解互斥锁和连接,实现Python中安全有效的多线程。


python创建两个线程执行不同的程序 python两个线程之间通信_python创建两个线程执行不同的程序

同步的重要性是什么?

假设有一个共享的家庭银行账户,余额为50美元,属于你和你父亲。

爸爸挣钱后把钱存进银行账户,不花钱,而你来花钱。

如果把这句话写成代码,从50美元的初始投资开始,爸爸向银行账户存入10美元,使总金额达到60美元。同时,你花了10美元,结果余额为50美元(60-10=50)。

然而,当爸爸和你同时进行交易时,会出现一个复杂的情况。

考虑到如果爸爸增加10美元,结果是50+10=60,而如果你花10美元,结果是50-10=40。就会少了10美元......

这就是所谓的“竞争条件”。

python创建两个线程执行不同的程序 python两个线程之间通信_python创建两个线程执行不同的程序_02

当有不良的程序设计,并且有几个进程和线程试图访问相同的资源时,就会出现“竞争条件”。

这种竞争条件是出了名的难以调试。可能有一个代码已经运行了数周而没有发生任何意外,然后在一天由于这种竞争条件而突然崩溃了。

进行编码

from threading import Thread
import time

class JointAccount:
  current_balance = 50

  def dad(self):
    for i in range(1000000):
      self.current_balance += 10
    print('Dad saved!')


  def son(self):
    for i in range(1000000):
      self.current_balance -= 10
    print('Son Spent!')


account = JointAccount()
Thread(target=account.dad, args=()).start()
Thread(target=account.son, args=()).start()
time.sleep(5) # 在这里,让两个线程都完成。
print(f"The current balance in the account is: {account.current_balance}")

输出是:

Dad saved!
Son Spent!
The current balance in the account is: -963810

可以看出,这里有一个错误!预测结果是60,但观察到的是-963810。如果再试一次,可以不断获得各种结果。

如何才能使这一问题得到解决?

如果要克服竞争条件,需要使用同步。

如何使用同步?

有2种同步技术。互斥锁是其中之一,另一种是利用连接。

1.互斥锁

为了理解这一点,可以想象一把锁保护一段特定的代码并且一次只允许一个线程访问它。

为了确保在任何给定时候只有一个线程可以访问银行账户,必须限制对场景中的current_balance变量的访问。

假设没有其他人锁定current_balance,爸爸将按照常规程序请求锁定它。爸爸在进行锁定调用后会收到一条信息,即他正持有该互斥锁。

在爸爸完成对current_balance的更新后,他将解锁,以便其他线程可以访问它。儿子也将遵循类似的程序。

如果一个线程已经持有一个mutex,而另一个线程请求它,请求的线程将被置入睡眠状态并一直等待,直到mutex被释放。

python创建两个线程执行不同的程序 python两个线程之间通信_数据库_03

python创建两个线程执行不同的程序 python两个线程之间通信_python创建两个线程执行不同的程序_04

python创建两个线程执行不同的程序 python两个线程之间通信_python_05

如果两个线程同时尝试访问,则保证只有一个线程可以访问并锁定它。

锁的实现

from threading import Thread, Lock
import time

class JointAccount:
  current_balance = 50
  mutex = Lock()

  def dad(self):
    for i in range(1000000):
      self.mutex.acquire()
      self.current_balance += 10
      self.mutex.release()
    print('Dad saved! ')


  def son(self):
    for i in range(1000000):
      self.mutex.acquire()
      self.current_balance -= 10
      self.mutex.release()
    print('Son spend!')


account = JointAccount()
Thread(target=account.dad, args=()).start()
Thread(target=account.son, args=()).start()
time.sleep(5) # 在这里,让两个线程都完成。
print("current_balance remaining is: ", account.current_balance)

输出是:

Dad saved! 
Son spend!
current_balance remaining is:  50

2.连接

什么是连接?

连接是一种使线程能够等待直到另一个线程执行完毕的方法。

实践连接

一个主线程创建一个子线程来执行任务。该子线程开始执行其任务。

在进入睡眠状态之前,主线程将调用join()函数。join()函数将保留主线程,直到子线程完成其任务并终止。

一旦子线程执行完毕,join()函数将释放,主线程将被唤醒并继续执行。

python创建两个线程执行不同的程序 python两个线程之间通信_数据库_06

单一任务的JOIN()的实现

import time
from threading import Thread

def child():
  print("subThread is created!")
  print("subThread is doing work")
  time.sleep(10)
  print("subThread work's done")

def parent():
  t = Thread(target=child, args=([]))
  t.start()
  print("mainThread thread put to sleep")
  t.join() # Parent将被封锁,直到child终止。
  # t.join(timeout=10)
  print("mainThread is awaken")

如果只是想让父母等待一段时间,就使用timeout

输出是:

subThread is created!
subThread is doing work
mainThread thread put to sleep
subThread work's done
mainThread is awaken

假设任务量很大,比如渲染图像。想尽可能迅速地完成这项任务。怎样才能做到这一点呢?为了加速渲染过程,将把图像分割成多个部分,并为每个部分分配不同的线程。将有一个主线程产生许多子线程。主线程等待所有的子线程完成渲染工作,它将与第一个子线程连接,开始渲染,接着是第二个子线程,以此类推,直到生成整个图像。为了实现这一点,将利用一个for循环。任何已经完成的线程将立即渲染。

首先,将尝试在不使用线程的情况下进行:

写一个脚本来查找一个词,然后将该词号与相应的短语相匹配。

matches = []

passage1 = """This is a sample passage
with multiple lines and words.
It is meant to demonstrate how to
search for a specific word in Python."""

# 循环浏览每一行和每一个词,找到特定的词
def word_search(word, passage):
  # 把这段话分成几行
  lines = passage.split("\n")

  for i, line in enumerate(lines):
      words = line.split()
      for j, w in enumerate(words):
          if w == word:
              matches.append(i+1)
              matches.append(j+1)
    
if __name__ == '__main__':
  word_search('demonstrate', passage1)
  print("matched!")
  print("Sentence is :", matches[0])
  print("Word is :", matches[1])

现在以join()的方式重写以上代码:

from threading import Lock, Thread

mutex = Lock()
matches = []

passage1 = """This is a sample passage
with multiple lines and words.
It is meant to demonstrate how to
search for a specific word in Python."""

# 循环浏览每一行和每一个词,找到特定的词
def word_search(word, passage):
  # 把这段话分成几行
  lines = passage.split("\n")

  for i, line in enumerate(lines):
      words = line.split()
      for j, w in enumerate(words):
          if w == word:
              mutex.acquire()
              matches.append(i+1)
              matches.append(j+1)
              mutex.release()

if __name__ == '__main__':
  t = Thread(target=word_search, args=(['demonstrate', passage1]))
  t.start()
  t.join() # 等待它完成
  print("matched!")
  print("Sentence is :", matches[0])
  print("Word is :", matches[1])

总结

理解互斥锁和连接对于在Python中实现安全和有效的多线程是至关重要的。互斥锁提供了一种方法,以确保一次只有一个线程访问共享资源,防止竞争条件和其他并发性问题。连接允许一个线程等待另一个线程完成后再继续,确保在程序终止前完成所有必要的工作。通过在多线程Python程序中加入这些技术,可以提高它们的安全性、效率和可靠性,从而提高整体性能,减少错误。