0
最近在刷python的题,遇到经典的银行提款的问题。做完以后,想在Ruby上面也实验一番,进而发现了更多好玩的知识点,略微整理一下。
从银行账户里面取钱和存钱,多线程操作,看是否会导致账户余额出错。按道理来说,最后应该还是1000块钱。
结果,上面这个python测试代码通常是失败的。根据https://opensource.com/article/17/4/grok-gil,`+=`不是原子性操作,中间可以GIL切换到其他线程上面去操作了,进而导致问题。要想避免这个问题,引入lock即以。
但是,Ruby代码却不会有这样的问题。根据https://www.jstorimer.com/blogs/workingwithcode/8100871-nobody-understands-the-gil-part-2-implementation,all methods in MRI are atomic. 所有操作都是原子性,没法在分割的,即不会出现中间GIL 切换到其他现成上面去。
那,有没有办法在Ruby里面去模拟这种情况呢?有,而且应用还颇为广泛。
如下面这段代码,先把balance缓存起来,然后sleep,再做操作,就会出现GIL切换线程的现象,进而出现线程安全问题。这种情况在很多地方都会出现,中间的sleep常常表现为系统进行其他的操作,但时间不可预知。回过头再把缓存的数据进行操作的时候,其他线程早就已经更改了。这种情况最典型的例子就是秒杀时候inventory的问题。
将注释去掉,引入`semapore.synchronize`即可。
问题回顾
从银行账户里面取钱和存钱,多线程操作,看是否会导致账户余额出错。按道理来说,最后应该还是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