-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Implement exercise bank-account #1176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@mrcfps Were you able to confirm that removing the @N-Parsons thoughts? |
@cmccandless yes I'm 100% sure that only with synchronization can we pass the tests. To ensure statistical stability, I've taken the same approach as java test suite to test concurrent transaction for 10 times, so that the non-deterministic nature of multi-threading won't break our tests. |
Need more insight? First get a feel of how Python execute multiple threads (from Understanding GIL, David Beazley): The key takeaway is that only one thread can access our Then let's analyze Python bytecode.
def deposit(self, amount):
if self.is_open and amount > 0:
self.balance += amount
else:
raise ValueError Here is its disassembly: >>> dis.dis(BankAccount.deposit)
16 0 LOAD_FAST 0 (self)
2 LOAD_ATTR 0 (is_open)
4 POP_JUMP_IF_FALSE 30
6 LOAD_FAST 1 (amount)
8 LOAD_CONST 1 (0)
10 COMPARE_OP 4 (>)
12 POP_JUMP_IF_FALSE 30
17 14 LOAD_FAST 0 (self)
16 DUP_TOP
18 LOAD_ATTR 1 (balance)
20 LOAD_FAST 1 (amount)
22 INPLACE_ADD
24 ROT_TWO
26 STORE_ATTR 1 (balance)
28 JUMP_FORWARD 4 (to 34)
19 >> 30 LOAD_GLOBAL 2 (ValueError)
32 RAISE_VARARGS 1
>> 34 LOAD_CONST 0 (None)
36 RETURN_VALUE To reproduce race condition, operations between If thread switch time is the default 0.005s (in Python 3) or 100 instructions (in Python 2), then thread A will have more than enough time to execute all instructions above in a row before another thread takes control as if the whole But when we set the switch interval (or check interval in Py2) to a pretty short time, then in the midst of retrieving, modifying and storing With |
@mrcfps Very good analysis! I would still like to hear @N-Parsons thoughts on the subject as he had some things to say when the concurrency issue was raised before. |
I just tried running this without the threading and lock, and it still passes in both Python 2 and Python 3. This seems to be because the concurrency test is starting with £1000 in the account and then adds and subtracts a small amount (£5). After a fair bit of playing around, I was able to get failure to be fairly reproducible, but there are a few more issues that I think need to be looked at (noted at the bottom of this comment). I've edited the code a little (initial balance of 0, sleep for 0.001s in the middle of the transaction, run it 100 times), and it now seems to fail every time, but I'm going to do a quick statistical analysis to optimise the number of runs and check that failure is sufficiently likely. I also want to check this on some VMs to make sure that reliable failure isn't core-count dependent. Modified test:import time
...
def test_can_handle_concurrent_transactions(self):
self.account.open()
#self.account.deposit(0)
for _ in range(100):
self.adjust_balance_concurrently()
def adjust_balance_concurrently(self):
def transact():
self.account.deposit(5)
time.sleep(0.001)
self.account.withdraw(5)
# Greatly improve the chance of an operation being interuppted
# by thread switch, thus testing synchronization effectively
try:
sys.setswitchinterval(1e-12)
except AttributeError:
# Python 2 compatible
sys.setcheckinterval(1)
threads = []
for _ in range(1000):
t = threading.Thread(target=transact)
threads.append(t)
t.start()
for thread in threads:
thread.join()
self.assertEqual(self.account.get_balance(), 0) Another issue:Sometimes exceptions are raised from within the threads, but these don't currently cause the failure of the test. While I was playing around, I had a few tests pass that had exceptions raised within them - these need to be made to be failures. I also saw a few of these with failing tests with my updated test code above.
|
I played around a bit more and came up with this test implementation: https://gist.github.com/N-Parsons/cdb731944ad1e678fdd07d3b126ea853 My change has caused the test to fail every time for both Python 2 and Python 3 regardless of whether we're using locking or not - this is because the lock exists only within one operation but doesn't do anything to help when a new thread tries to withdraw money from an empty account that would have money in it if the previous thread had been allowed to finish and make the deposit. My understanding of what we should be aiming for with concurrency is that we should be able to run another thread if we're waiting for something on another thread, but that it should never result in different behaviour to if we were running it without concurrency. |
@N-Parsons I have followed your advice to tweak the test. I just add a single line of The explanation for the thread |
@cmccandless @N-Parsons Sorry for my poor branching strategy. I've been committing on the master branch, so it's pretty tricky to make new contributions. I'll re-fork this repo and work on a new branch. |
Solves #730, alternative to #757.
Hi, I'm striving to attack the problem of concurrency testing from a different angle. As is documented in Thread State and the Global Interpreter Lock, there is only ever one thread per process active to execute Python bytecode, which, as a side effect, slashes the possibility of race condition.
But when Python switches threads, chances of execution breakup come, the culprit behind race condition. By default, Python 3 switches threads every 0.005 seconds (just try
sys.getswitchinterval()
) and Python 2 every 100 Python virtual instructions (trysys.getcheckinterval()
), and that's much longer than the execution time ofdeposit
orwithdraw
methods (around 100 ns). So I set the switch interval to a small number to make bank account methods not as "atomic" as they are before.I hope my PR would help in tackling concurrency-related exercises in the Python track.