-
Notifications
You must be signed in to change notification settings - Fork 606
Review and improve lock usage in Tca955x interrupt state management #2429
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ | |
| private readonly int _interrupt; | ||
| private readonly Dictionary<int, PinValue> _pinValues = new Dictionary<int, PinValue>(); | ||
| private readonly ConcurrentDictionary<int, PinChangeEventHandler> _eventHandlers = new ConcurrentDictionary<int, PinChangeEventHandler>(); | ||
| private readonly Dictionary<int, PinEventTypes> _interruptPinsSubscribedEvents = new Dictionary<int, PinEventTypes>(); | ||
| private readonly ConcurrentDictionary<int, PinEventTypes> _interruptPinsSubscribedEvents = new ConcurrentDictionary<int, PinEventTypes>(); | ||
| private readonly ConcurrentDictionary<int, PinValue> _interruptLastInputValues = new ConcurrentDictionary<int, PinValue>(); | ||
|
|
||
| private GpioController? _controller; | ||
|
|
@@ -29,6 +29,10 @@ | |
|
|
||
| private I2cDevice _busDevice; | ||
|
|
||
| // Lock protects: | ||
| // 1. I2C bus access - I2C operations must be atomic and sequential | ||
| // 2. _pinValues dictionary - tightly coupled with I2C read/write operations | ||
| // 3. Interrupt task coordination - _interruptProcessingTask and _interruptPending state | ||
| private object _interruptHandlerLock = new object(); | ||
|
|
||
| // This task processes the i2c reading of the io expander in a background task to | ||
|
|
@@ -89,13 +93,13 @@ | |
| } | ||
|
|
||
| if (_interrupt != -1) | ||
| { | ||
| // Initialise the interrupt handling state because ints may start coming from the INT pin | ||
| // on the expander as soon as we register the interrupt handler. | ||
| for (int i = 0; i < PinCount; i++) | ||
| { | ||
| _interruptPinsSubscribedEvents.Add(i, PinEventTypes.None); | ||
| _interruptLastInputValues.TryAdd(i, PinValue.Low); | ||
| { | ||
| // Initialise the interrupt handling state because ints may start coming from the INT pin | ||
| // on the expander as soon as we register the interrupt handler. | ||
| for (int i = 0; i < PinCount; i++) | ||
| { | ||
| _interruptPinsSubscribedEvents.TryAdd(i, PinEventTypes.None); | ||
| _interruptLastInputValues.TryAdd(i, PinValue.Low); | ||
| } | ||
|
|
||
| _shouldDispose = shouldDispose || gpioController is null; | ||
|
|
@@ -182,6 +186,7 @@ | |
| /// <param name="mode">The mode to be set.</param> | ||
| protected override void SetPinMode(int pinNumber, PinMode mode) | ||
| { | ||
| // Lock required: I2C bus operations must be atomic and sequential | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| if (mode != PinMode.Input && mode != PinMode.Output && mode != PinMode.InputPullUp) | ||
|
|
@@ -250,6 +255,7 @@ | |
| protected override PinValue Read(int pinNumber) | ||
| { | ||
| PinValue pinValue; | ||
| // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| ValidatePin(pinNumber); | ||
|
|
@@ -272,6 +278,7 @@ | |
| /// </summary> | ||
| protected override void Read(Span<PinValuePair> pinValuePairs) | ||
| { | ||
| // Lock required: I2C bus operations must be atomic, and _pinValues is updated during read | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| byte? lowReg = null; | ||
|
|
@@ -318,6 +325,7 @@ | |
| /// <param name="value">The value to be written.</param> | ||
| protected override void Write(int pinNumber, PinValue value) | ||
| { | ||
| // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| ValidatePin(pinNumber); | ||
|
|
@@ -338,6 +346,7 @@ | |
| bool lowChanged = false; | ||
| bool highChanged = false; | ||
|
|
||
| // Lock required: I2C bus operations must be atomic, and _pinValues is updated during write | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| (uint mask, uint newBits) = new PinVector32(pinValuePairs); | ||
|
|
@@ -445,8 +454,11 @@ | |
| // i2c and detect a change in the returned input register data, not to mention run the | ||
| // event handlers that the consumer of the library has signed up, we are likely | ||
| // to miss edges while we are doing that anyway. Dropping interrupts in this | ||
| // case is the best we can do and prevents flooding the consumer with events | ||
| // case is the best we can do and prevents flooding the consumer with events | ||
| // that could queue up in the INT gpio pin driver. | ||
|
|
||
|
Check failure on line 459 in src/devices/Tca955x/Tca955x.cs
|
||
| // Lock required for task coordination: atomically check/start _interruptProcessingTask | ||
| // or set _interruptPending flag to ensure proper interrupt queueing | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| if (_interruptProcessingTask == null) | ||
|
|
@@ -461,10 +473,11 @@ | |
| } | ||
|
|
||
| private Task ProcessInterruptInTask() | ||
| { | ||
| // Take a snapshot of the current interrupt pin configuration and last known input values | ||
| // so we can safely process them outside the lock in a background task. | ||
| var interruptPinsSnapshot = new Dictionary<int, PinEventTypes>(_interruptPinsSubscribedEvents); | ||
| { | ||
| // Take a snapshot of the current interrupt pin configuration and last known input values | ||
| // so we can safely process them in a background task. ConcurrentDictionary enumeration | ||
| // is thread-safe and provides a consistent snapshot. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "ConcurrentDictionary enumeration is thread-safe and provides a consistent snapshot." So what? We are operating on the snapshots in a single thread with reentrancy prevented by the lock, so this comment is not relevant. Additionally the snapshots are not concurrent dictionaries anyway. Adding this comment is just confusing. |
||
| var interruptPinsSnapshot = new Dictionary<int, PinEventTypes>(_interruptPinsSubscribedEvents); | ||
| var interruptLastInputValuesSnapshot = new Dictionary<int, PinValue>(_interruptLastInputValues); | ||
|
|
||
| Task processingTask = new Task(() => | ||
|
|
@@ -511,14 +524,16 @@ | |
|
|
||
| processingTask.ContinueWith(t => | ||
| { | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| _interruptProcessingTask = null; | ||
| if (_interruptPending) | ||
| { | ||
| _interruptPending = false; | ||
| _interruptProcessingTask = ProcessInterruptInTask(); | ||
| } | ||
| // Lock required for task coordination: atomically check/update _interruptProcessingTask | ||
| // and _interruptPending to ensure only one processing task runs at a time | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| _interruptProcessingTask = null; | ||
| if (_interruptPending) | ||
| { | ||
| _interruptPending = false; | ||
| _interruptProcessingTask = ProcessInterruptInTask(); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -563,26 +578,29 @@ | |
| throw new InvalidOperationException("No interrupt pin configured"); | ||
| } | ||
|
|
||
| // Update subscription state using thread-safe ConcurrentDictionary operations | ||
| _interruptPinsSubscribedEvents[pinNumber] = eventType; | ||
|
|
||
|
Check failure on line 583 in src/devices/Tca955x/Tca955x.cs
|
||
| // Read current value needs lock because it accesses I2C bus | ||
| PinValue currentValue; | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| _interruptPinsSubscribedEvents[pinNumber] = eventType; | ||
| var currentValue = Read(pinNumber); | ||
| _interruptLastInputValues.TryUpdate(pinNumber, currentValue, !currentValue); | ||
| if (!_eventHandlers.TryAdd(pinNumber, callback)) | ||
| { | ||
| throw new InvalidOperationException($"An event handler is already registered for pin {pinNumber}"); | ||
| } | ||
| currentValue = Read(pinNumber); | ||
| } | ||
|
|
||
|
Check failure on line 590 in src/devices/Tca955x/Tca955x.cs
|
||
| _interruptLastInputValues.TryUpdate(pinNumber, currentValue, !currentValue); | ||
| if (!_eventHandlers.TryAdd(pinNumber, callback)) | ||
| { | ||
| throw new InvalidOperationException($"An event handler is already registered for pin {pinNumber}"); | ||
| } | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) | ||
| { | ||
| lock (_interruptHandlerLock) | ||
| { | ||
| _eventHandlers.TryRemove(pinNumber, out _); | ||
| _interruptPinsSubscribedEvents[pinNumber] = PinEventTypes.None; | ||
| } | ||
| // Use thread-safe ConcurrentDictionary operations - no lock needed | ||
| _eventHandlers.TryRemove(pinNumber, out _); | ||
| _interruptPinsSubscribedEvents[pinNumber] = PinEventTypes.None; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is misleading and incorrect. The I2cDevice we consume already guarantees that i2c reads/writes are done as transactions. Additionally, although the locks allow this class to be used multi threaded, it is not designed to be used that way. The only concurrency we need to protect against is the INT pin incoming interrupt events running on a different thread to that which the consumer of this class is running on. I'm pretty sure with a bit of thought, we could do away with the locks entirely and just use the concurrent collections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd need to amalgamate the two concurrent collections in the case of not using the lock. Otherwise there could be a race condition between the two collections.