关于APScheduler同步机制的技术咨询:并发访问共享变量是否会导致竞态条件
Great question—let's unpack this clearly, since the APScheduler docs don't explicitly spell out the synchronization implications for shared state.
First: How APScheduler's BackgroundScheduler Runs Tasks
By default, the BackgroundScheduler uses a ThreadPoolExecutor (with a default of 10 worker threads). That means your step_up and step_heavy tasks run in separate threads when their intervals trigger. Any unprotected access to shared variables (like your self.x) is absolutely at risk of race conditions.
Why Your Current Code Is Vulnerable
Take the line self.x += 1—this looks atomic, but in Python it's actually three separate operations:
- Read the current value of
self.x - Add 1 to it
- Assign the new value back to
self.x
If step_up runs while step_heavy is in the middle of its multiple self.x += -1 operations (especially during those slow np.random.random calls), the threads can overwrite each other's changes. For example:
step_heavyreadsself.x = 5, starts a long numpy calculationstep_upreadsself.x = 5, adds 1, sets it to 6step_heavyresumes, subtracts 1 from its cached 5, setsself.xto 4- You end up with
x=4instead of the expected5-1+1=5
How to Fix It: Add Thread Synchronization
You need to use a lock to ensure only one thread can modify or read self.x at a time. Python's threading.Lock is perfect for this. Here's how to modify your code:
import time import numpy as np from datetime import datetime from apscheduler.schedulers.background import BackgroundScheduler import threading scheduler = BackgroundScheduler() class Test(object): x = None def __init__(self): self.x = 0 self.lock = threading.Lock() # Add a thread lock def step_up(self): with self.lock: # Auto-acquire/release lock self.x += 1 ts = datetime.now().replace(microsecond=0) print(f'Step up {ts}: value of x={self.x}') def step_heavy(self): with self.lock: self.x += -1 ts = datetime.now().replace(microsecond=0) print(f'Heavy 1 {ts}: value of x={self.x}') # Release lock during slow numpy calls (no shared state access here) np.random.random(size=(20000, 20000)) with self.lock: self.x += -1 ts = datetime.now().replace(microsecond=0) print(f'Heavy 2 {ts}: value of x={self.x}') np.random.random(size=(20000, 50000)) with self.lock: self.x += -1 ts = datetime.now().replace(microsecond=0) print(f'Heavy 3 {ts}: value of x={self.x}') def update_setpoint(self): pass test = Test() scheduler.add_job(test.step_up, 'interval', seconds=1) scheduler.add_job(test.step_heavy, 'interval', seconds=15) scheduler.start() try: while True: time.sleep(2) except (KeyboardInterrupt, SystemExit): scheduler.shutdown()
Key notes about this fix:
- The
with self.lockstatement automatically manages lock acquisition/release, even if an error occurs in the block - We release the lock during slow numpy operations (since they don't touch
self.x)—this keeps yourstep_uptask responsive instead of waiting for heavy computations to finish
Final Verdict
Without explicit synchronization, your code will face race conditions. Adding a lock as shown ensures all accesses to self.x are atomic, eliminating unexpected value inconsistencies.
内容的提问来源于stack exchange,提问作者Jirka cigler

