0
Posted on Friday, June 28, 2019 by 醉·醉·鱼 and labeled under ,
最近在刷python的题,遇到经典的银行提款的问题。做完以后,想在Ruby上面也实验一番,进而发现了更多好玩的知识点,略微整理一下。

问题回顾


从银行账户里面取钱和存钱,多线程操作,看是否会导致账户余额出错。按道理来说,最后应该还是1000块钱。


class BankAccount(object):

    def __init__(self):
        self.balance = 0
        pass

    def withdraw(self, amount):           
        self.balance -= amount

    def deposit(self, amount):
        self.balance += amount



    def test_can_handle_concurrent_transactions(self):
        account = BankAccount()
        account.open()
        account.deposit(1000)

    self.adjust_balance_concurrently(account)

    self.assertEqual(account.get_balance(), 1000)

    def adjust_balance_concurrently(self, account):
        def transact():
            account.deposit(5)
            time.sleep(0.001)
            account.withdraw(5)

        # Greatly improve the chance of an operation being interrupted
        # by thread switch, thus testing synchronization effectively
        try:
            sys.setswitchinterval(1e-12)
        except AttributeError:
        # For Python 2 compatibility
            sys.setcheckinterval(1)

        threads = [threading.Thread(target=transact) for _ in range(1000)]
        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()

结果,上面这个python测试代码通常是失败的。根据https://opensource.com/article/17/4/grok-gil,`+=`不是原子性操作,中间可以GIL切换到其他线程上面去操作了,进而导致问题。要想避免这个问题,引入lock即以。



lock = threading.Lock()
class BankAccount(object):

    def __init__(self):
        self.balance = 0
        pass

    def withdraw(self, amount):           
        with lock:
            self.balance -= amount

    def deposit(self, amount):
        with lock:
            self.balance += amount


但是,Ruby代码却不会有这样的问题。根据https://www.jstorimer.com/blogs/workingwithcode/8100871-nobody-understands-the-gil-part-2-implementation,all methods in MRI are atomic. 所有操作都是原子性,没法在分割的,即不会出现中间GIL 切换到其他现成上面去。



class BankAccount
  attr_accessor :balance

  def initialize
    @balance = 0
    @semapore = Mutex.new
  end
  
  def deposit(amount)
    @balance += amount
  end

  def withdraw(amount)
    @balance -= amount
  end
  
end


account = BankAccount.new
account.deposit(1000)

threads = []

1000.times do
  threads << Thread.new do
    account.withdraw(1)
  end
end

threads.each(&:join)

p account.balance

那,有没有办法在Ruby里面去模拟这种情况呢?有,而且应用还颇为广泛。
如下面这段代码,先把balance缓存起来,然后sleep,再做操作,就会出现GIL切换线程的现象,进而出现线程安全问题。这种情况在很多地方都会出现,中间的sleep常常表现为系统进行其他的操作,但时间不可预知。回过头再把缓存的数据进行操作的时候,其他线程早就已经更改了。这种情况最典型的例子就是秒杀时候inventory的问题。

将注释去掉,引入`semapore.synchronize`即可。


  def withdraw(amount)
    # @semapore.synchronize do
      balance = @balance
      sleep rand(10) / 10000.0
      @balance = balance - amount
    # end
  end