Necesito tener la pieza de código que permitió ejecutar solo por 1 subproceso al mismo tiempo según la clave de parámetro:
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Semaphores = new(); private async Task<TModel> GetValueWithBlockAsync<TModel>(string valueKey, Func<Task<TModel>> valueAction) { var semaphore = Semaphores.GetOrAdd(valueKey, s => new SemaphoreSlim(1, 1)); try { await semaphore.WaitAsync(); return await valueAction(); } finally { semaphore.Release(); // Exception here - System.ObjectDisposedException if (semaphore.CurrentCount > 0 && Semaphores.TryRemove(valueKey, out semaphore)) { semaphore?.Dispose(); } } }De vez en cuando recibí el error:
The semaphore has been disposed. : System.ObjectDisposedException: The semaphore has been disposed. at System.Threading.SemaphoreSlim.CheckDispose() at System.Threading.SemaphoreSlim.Release(Int32 releaseCount) at Project.GetValueWithBlockAsync[TModel](String valueKey, Func`1 valueAction)Todos los casos que puedo imaginar aquí son seguridad de subprocesos. Por favor ayuda, ¿qué caso me perdí?
Tiene una carrera de subprocesos aquí, donde otra tarea está tratando de adquirir el mismo semáforo, y lo adquiere cuando lo Release , es decir, otro subproceso está esperando el semaphore.WaitAsync() . La verificación contra CurrentCount es una condición de carrera, y podría ir en cualquier dirección dependiendo del tiempo. La verificación de TryRemove es irrelevante, ya que el subproceso de la competencia ya obtuvo el semáforo; después de todo, estaba esperando WaitAsync() .
Como se discutió en los comentarios, aquí tienes un par de condiciones de carrera.
WaitAsync() . El subproceso 1 libera el bloqueo y luego verifica semaphore.CurrentCount antes de que el subproceso 2 pueda adquirirlo.semaphore.CurrentCount que pasa. El subproceso 2 ingresa a GetValueWithBlockAsync , llama a Semaphores.GetOrAdd y obtiene el semáforo. El subproceso 1 luego llama a Semaphores.TryRemove y desecha el semáforo. Realmente necesita bloquear la decisión de eliminar una entrada de Semaphores ; no hay forma de evitar esto. Tampoco tiene una forma de rastrear si algún subproceso ha obtenido un semáforo de Semaphores (y lo está esperando actualmente o aún no ha llegado a ese punto).
Una forma es hacer algo como esto: tener un bloqueo que se comparte entre todos, pero que solo se necesita al buscar/crear un semáforo y decidir si se desecha. Realizamos un seguimiento manual de cuántos subprocesos tienen actualmente interés en un semáforo en particular. Cuando un subproceso ha liberado el semáforo, adquiere el bloqueo compartido para verificar si alguien más tiene interés en ese semáforo y lo elimina solo si nadie más lo tiene.
private static readonly object semaphoresLock = new(); private static readonly Dictionary<string, State> semaphores = new(); private async Task<TModel> GetValueWithBlockAsync<TModel>(string valueKey, Func<Task<TModel>> valueAction) { State state; lock (semaphoresLock) { if (!semaphores.TryGetValue(valueKey, out state)) { state = new(); semaphores[valueKey] = state; } state.Count++; } try { await state.Semaphore.WaitAsync(); return await valueAction(); } finally { state.Semaphore.Release(); lock (semaphoresLock) { state.Count--; if (state.Count == 0) { semaphores.Remove(valueKey); state.Semaphore.Dispose(); } } } } private class State { public int Count { get; set; } public SemaphoreSlim Semaphore { get; } = new(1, 1); } La otra opción, por supuesto, es dejar que Semaphores crezca. Tal vez tenga una operación periódica para realizar y borrar cualquier cosa que no se esté utilizando, pero esto, por supuesto, deberá protegerse para garantizar que un subproceso no se interese repentinamente en un semáforo que se está limpiando.