Python多线程编程——资源共享

电玩女神 2023-02-26 13:29 99阅读 0赞

转载来源:https://github.com/jackfrued/Python-100-Days/blob/master/Day01-15%2F13.%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B.md

问题描述:

因为多个线程可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,大家能想到的最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量(我们通常称之为“资源”)的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。如果一个资源被多个线程竞争使用,那么我们通常称之为“临界资源”,对“临界资源”的访问需要加上保护,否则资源会处于“混乱”的状态。下面的例子演示了100个线程向同一个银行账户转账(转入1元钱)的场景,在这个例子中,银行账户就是一个临界资源,在没有保护的情况下我们很有可能会得到错误的结果:

  1. # -*- coding: utf-8 -*-
  2. # -*- coding: utf-8 -*-
  3. from time import sleep, time
  4. from threading import Thread
  5. class Account(object):
  6. def __init__(self):
  7. self._balance = 0
  8. def deposit(self, money):
  9. # 计算存款后的余额
  10. new_balance = self._balance + money
  11. # 模拟受理存款业务需要0.01秒的时间
  12. sleep(0.01)
  13. # 修改账户余额
  14. self._balance = new_balance
  15. @property
  16. def balance(self):
  17. return self._balance
  18. class AddMoneyThread(Thread):
  19. def __init__(self, account, money):
  20. super().__init__()
  21. self._account = account
  22. self._money = money
  23. def run(self):
  24. self._account.deposit(self._money)
  25. def main():
  26. account = Account()
  27. threads = []
  28. start = time()
  29. # 创建100个存款的线程向同一个账户中存钱
  30. for _ in range(5):
  31. t = AddMoneyThread(account, 1)
  32. threads.append(t)
  33. t.start()
  34. # 等所有存款的线程都执行完毕
  35. for t in threads:
  36. t.join()
  37. end = time()
  38. print('账户余额为: ¥%d元,花费的时间为%.3f' % (account.balance, end - start))
  39. if __name__ == '__main__':
  40. main()
  41. # 账户余额为: ¥1元,花费的时间为0.012

解决办法:

运行上面的程序,结果让人大跌眼镜,100个线程分别向账户中转入1元钱,结果居然远远小于100元。之所以出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money这行代码,多个线程得到的账户余额都是初始状态下的0,所以都是0上面做了+1的操作,因此得到了错误的结果。在这种情况下,“锁”就可以派上用场了。我们可以通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。下面的代码演示了如何使用“锁”来保护对银行账户的操作,从而获得正确的结果。示例:

  1. # -*- coding: utf-8 -*-
  2. from time import sleep, time
  3. from threading import Thread, Lock
  4. class Account(object):
  5. def __init__(self):
  6. self._balance = 0
  7. self._lock = Lock()
  8. def deposit(self, money):
  9. # 先获取锁才能执行后续的代码
  10. self._lock.acquire()
  11. try:
  12. # 计算存款后的余额
  13. new_balance = self._balance + money
  14. # 模拟受理存款业务需要0.01秒的时间
  15. sleep(0.01)
  16. # 修改账户余额
  17. self._balance = new_balance
  18. finally:
  19. # 在finally中执行释放锁的操作保证正常异常锁都能释放
  20. self._lock.release()
  21. @property
  22. def balance(self):
  23. return self._balance
  24. class AddMoneyThread(Thread):
  25. def __init__(self, account, money):
  26. super().__init__()
  27. self._account = account
  28. self._money = money
  29. def run(self):
  30. self._account.deposit(self._money)
  31. def main():
  32. account = Account()
  33. threads = []
  34. start = time()
  35. # 创建100个存款的线程向同一个账户中存钱
  36. for _ in range(5):
  37. t = AddMoneyThread(account, 1)
  38. threads.append(t)
  39. t.start()
  40. # 等所有存款的线程都执行完毕
  41. for t in threads:
  42. t.join()
  43. end = time()
  44. print('账户余额为: ¥%d元,花费的时间为%.3f' % (account.balance, end - start))
  45. if __name__ == '__main__':
  46. main()
  47. # 账户余额为: ¥5元,花费的时间为0.055

讨论(非转载):

  • 对于原作者的解决方法:通过“锁”来保护“临界资源”,对于一般的操作是个不错的方法,即:
    在同一个线程里面对“临街资源”的访问和修改必须要在一个“锁”之内完成,这样可以保证线程对“临街资源”的修改对其他线程产生影响。
  • 但是当对“临街资源”的访问和修改过程是一个比较耗时的操作的时候,即占了该线程所耗时间的绝大部分的时候,则会严重削弱多线程所带来的执行效率的提升,如:

    -- coding: utf-8 --

    from time import sleep, time
    from threading import Thread, Lock

  1. class Account(object):
  2. def __init__(self):
  3. self._balance = 0
  4. self._lock = Lock()
  5. def deposit(self, money):
  6. # 先获取锁才能执行后续的代码
  7. self._lock.acquire()
  8. try:
  9. # 计算存款后的余额
  10. new_balance = self._balance + money
  11. # 模拟受理存款业务需要2秒的时间
  12. sleep(2)
  13. # 修改账户余额
  14. self._balance = new_balance
  15. finally:
  16. # 在finally中执行释放锁的操作保证正常异常锁都能释放
  17. self._lock.release()
  18. @property
  19. def balance(self):
  20. return self._balance
  21. class AddMoneyThread(Thread):
  22. def __init__(self, account, money):
  23. super().__init__()
  24. self._account = account
  25. self._money = money
  26. def run(self):
  27. self._account.deposit(self._money)
  28. def main():
  29. account = Account()
  30. threads = []
  31. start = time()
  32. # 创建100个存款的线程向同一个账户中存钱
  33. for _ in range(5):
  34. t = AddMoneyThread(account, 1)
  35. threads.append(t)
  36. t.start()
  37. # 等所有存款的线程都执行完毕
  38. for t in threads:
  39. t.join()
  40. end = time()
  41. print('账户余额为: ¥%d元,花费的时间为%.3f' % (account.balance, end - start))
  42. if __name__ == '__main__':
  43. main()
  44. # 账户余额为: ¥5元,花费的时间为10.004

在该例子中,线程“锁”内部的操作耗费了大量的时间(即:sleep(2)),多线程对执行效率的提升效果非常小。

  1. # -*- coding: utf-8 -*-
  2. from time import sleep, time
  3. from threading import Thread, Lock
  4. class Account(object):
  5. def __init__(self):
  6. self._balance = 0
  7. self._lock = Lock()
  8. def deposit(self, money):
  9. sleep(2)
  10. # 先获取锁才能执行后续的代码
  11. self._lock.acquire()
  12. try:
  13. # 计算存款后的余额
  14. new_balance = self._balance + money
  15. # 模拟受理存款业务需要0.01秒的时间
  16. sleep(0.01)
  17. # 修改账户余额
  18. self._balance = new_balance
  19. finally:
  20. # 在finally中执行释放锁的操作保证正常异常锁都能释放
  21. self._lock.release()
  22. @property
  23. def balance(self):
  24. return self._balance
  25. class AddMoneyThread(Thread):
  26. def __init__(self, account, money):
  27. super().__init__()
  28. self._account = account
  29. self._money = money
  30. def run(self):
  31. self._account.deposit(self._money)
  32. def main():
  33. account = Account()
  34. threads = []
  35. start = time()
  36. # 创建100个存款的线程向同一个账户中存钱
  37. for _ in range(5):
  38. t = AddMoneyThread(account, 1)
  39. threads.append(t)
  40. t.start()
  41. # 等所有存款的线程都执行完毕
  42. for t in threads:
  43. t.join()
  44. end = time()
  45. print('账户余额为: ¥%d元,花费的时间为%.3f' % (account.balance, end - start))
  46. if __name__ == '__main__':
  47. main()
  48. # 账户余额为: ¥5元,花费的时间为2.056

但是,当线程中“锁”外面的操作远耗时(sleep(2))大于“锁”内部的操作(sleep(0.01))的时候,多线程对执行效率的提升效果非常明显。

发表评论

表情:
评论列表 (有 0 条评论,99人围观)

还没有评论,来说两句吧...

相关阅读