/// <summary>
            /// Reserves a read lock from a previously held lock.
            /// </summary>
            /// <returns>The releaser for the read lock.</returns>
            /// <exception cref="InvalidOperationException">Thrown if no lock is held by the caller.</exception>
            private ResourceReleaser AcquirePreexistingLockOrThrow()
            {
                if (!this.service.IsAnyLockHeld)
                {
                    Verify.FailOperation(Strings.InvalidWithoutLock);
                }

                AsyncReaderWriterResourceLock <TMoniker, TResource> .ResourceAwaiter awaiter = this.service.ReadLockAsync(CancellationToken.None).GetAwaiter();
                Assumes.True(awaiter.IsCompleted);
                return(awaiter.GetResult());
            }
            /// <summary>
            /// Initializes a new instance of the <see cref="Helper"/> class.
            /// </summary>
            /// <param name="service">The owning lock instance.</param>
            internal Helper(AsyncReaderWriterResourceLock <TMoniker, TResource> service)
            {
                Requires.NotNull(service, nameof(service));

                this.service = service;
                this.prepareResourceConcurrentDelegate = state =>
                {
                    var tuple = (Tuple <TResource, CancellationToken>)state;
                    return(this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2));
                };

                this.prepareResourceExclusiveDelegate = state =>
                {
                    var tuple = (Tuple <TResource, LockFlags, CancellationToken>)state;
                    return(this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3));
                };

                this.prepareResourceConcurrentContinuationDelegate = (prev, state) =>
                {
                    var tuple = (Tuple <TResource, CancellationToken>)state;
                    return(this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2));
                };

                this.prepareResourceExclusiveContinuationDelegate = (prev, state) =>
                {
                    var tuple = (Tuple <TResource, LockFlags, CancellationToken>)state;
                    return(this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3));
                };

                // this delegate is to handle the case that we prepare resource when the previous task might be cancelled.
                // Because the previous task might not be cancelled, but actually finished. In that case, we will consider the work has done, and there is no need to prepare it again.
                this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate = (prev, state) =>
                {
                    if (!prev.IsFaulted && !prev.IsCanceled)
                    {
                        return(prev);
                    }

                    var tuple = (Tuple <TResource, CancellationToken>)state;
                    return(this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2));
                };

                this.prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate = (prev, state) =>
                {
                    if (!prev.IsFaulted && !prev.IsCanceled)
                    {
                        return(prev);
                    }

                    var tuple = (Tuple <TResource, LockFlags, CancellationToken>)state;
                    return(this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3));
                };
            }
Exemple #3
0
            /// <summary>
            /// Initializes a new instance of the <see cref="Helper"/> class.
            /// </summary>
            /// <param name="service">The owning lock instance.</param>
            internal Helper(AsyncReaderWriterResourceLock <TMoniker, TResource> service)
            {
                Requires.NotNull(service, nameof(service));

                this.service = service;
                this.prepareResourceConcurrentDelegate = state => this.service.PrepareResourceForConcurrentAccessAsync((TResource)state, CancellationToken.None);
                this.prepareResourceExclusiveDelegate  = state =>
                {
                    var tuple = (Tuple <TResource, LockFlags>)state;
                    return(this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, CancellationToken.None));
                };
                this.prepareResourceConcurrentContinuationDelegate = (prev, state) => this.service.PrepareResourceForConcurrentAccessAsync((TResource)state, CancellationToken.None);
                this.prepareResourceExclusiveContinuationDelegate  = (prev, state) =>
                {
                    var tuple = (Tuple <TResource, LockFlags>)state;
                    return(this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, CancellationToken.None));
                };
            }
            /// <summary>
            /// Prepares the specified resource for access by a lock holder.
            /// </summary>
            /// <param name="resource">The resource to prepare.</param>
            /// <param name="cancellationToken">The token whose cancellation signals lost interest in this resource.</param>
            /// <param name="forcePrepareConcurrent">Force preparation of the resource for concurrent access, even if an exclusive lock is currently held.</param>
            /// <returns>A task that is completed when preparation has completed.</returns>
            private Task PrepareResourceAsync(TResource resource, CancellationToken cancellationToken, bool forcePrepareConcurrent = false)
            {
                Requires.NotNull(resource, nameof(resource));
                Assumes.True(Monitor.IsEntered(this.service.SyncObject));

                // We deliberately ignore the cancellation token in the tasks we create and save because the tasks can be shared
                // across requests and we can't have task continuation chains where tasks within the chain get canceled
                // as that can cause premature starting of the next task in the chain.
                bool forConcurrentUse = forcePrepareConcurrent || !this.service.IsWriteLockHeld;

                AsyncReaderWriterResourceLock <TMoniker, TResource> .Helper.ResourceState finalState = forConcurrentUse ? ResourceState.Concurrent : ResourceState.Exclusive;

                Task?preparationTask = null;

                if (!this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? preparationState))
                {
                    Func <object, Task>?preparationDelegate = forConcurrentUse
                        ? this.prepareResourceConcurrentDelegate
                        : this.prepareResourceExclusiveDelegate;

                    // We kick this off on a new task because we're currently holding a private lock
                    // and don't want to execute arbitrary code.
                    // Let's also hide the ARWL from the delegate if this is a shared lock request.
                    using (forConcurrentUse ? this.service.HideLocks() : default(Suppression))
                    {
                        // We can't currently use the caller's cancellation token for this task because
                        // this task may be shared with others or call this method later, and we wouldn't
                        // want their requests to be cancelled as a result of this first caller cancelling.
                        (preparationState, preparationTask) = ResourcePreparationTaskState.Create(
                            combinedCancellationToken => Task.Factory.StartNew(
                                NullableHelpers.AsNullableArgFunc(preparationDelegate),
                                forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken),
                                combinedCancellationToken,
                                TaskCreationOptions.None,
                                TaskScheduler.Default).Unwrap(),
                            finalState,
                            cancellationToken);
                    }
                }
                else
                {
                    Func <Task, object, Task>?preparationDelegate = null;
                    if (preparationState.State != finalState || preparationState.InnerTask.IsFaulted)
                    {
                        preparationDelegate = forConcurrentUse
                            ? this.prepareResourceConcurrentContinuationDelegate
                            : this.prepareResourceExclusiveContinuationDelegate;
                    }
                    else if (!preparationState.TryJoinPrepationTask(out preparationTask, cancellationToken))
                    {
                        preparationDelegate = forConcurrentUse
                            ? this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate
                            : this.prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate;
                    }

                    if (preparationTask is null)
                    {
                        Assumes.NotNull(preparationDelegate);

                        // We kick this off on a new task because we're currently holding a private lock
                        // and don't want to execute arbitrary code.
                        // Let's also hide the ARWL from the delegate if this is a shared lock request.
                        using (forConcurrentUse ? this.service.HideLocks() : default(Suppression))
                        {
                            (preparationState, preparationTask) = ResourcePreparationTaskState.Create(
                                combinedCancellationToken => preparationState.InnerTask.ContinueWith(
                                    preparationDelegate !,
                                    forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken),
                                    CancellationToken.None,
                                    TaskContinuationOptions.RunContinuationsAsynchronously,
                                    TaskScheduler.Default).Unwrap(),
                                finalState,
                                cancellationToken);
                        }
                    }
                }

                Assumes.NotNull(preparationState);
                this.resourcePreparationStates[resource] = preparationState;

                return(preparationTask);
            }
            /// <summary>
            /// Retrieves the resource with the specified moniker.
            /// </summary>
            /// <param name="resourceMoniker">The identifier for the desired resource.</param>
            /// <param name="cancellationToken">The token whose cancellation signals lost interest in this resource.</param>
            /// <returns>A task whose result is the desired resource.</returns>
            internal async Task <TResource> GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken)
            {
                using (AsyncReaderWriterResourceLock <TMoniker, TResource> .ResourceReleaser resourceLock = this.AcquirePreexistingLockOrThrow())
                {
                    TResource?resource = await this.service.GetResourceAsync(resourceMoniker, cancellationToken).ConfigureAwait(false);

                    Task preparationTask;

                    lock (this.service.SyncObject)
                    {
                        this.SetResourceAsAccessed(resource);

                        preparationTask = this.PrepareResourceAsync(resource, cancellationToken);
                    }

                    await preparationTask.ConfigureAwait(false);

                    return(resource);
                }
            }
            /// <summary>
            /// Prepares the specified resource for access by a lock holder.
            /// </summary>
            /// <param name="resource">The resource to prepare.</param>
            /// <param name="cancellationToken">The token whose cancellation signals lost interest in this resource.</param>
            /// <param name="forcePrepareConcurrent">Force preparation of the resource for concurrent access, even if an exclusive lock is currently held.</param>
            /// <returns>A task that is completed when preparation has completed.</returns>
            private Task PrepareResourceAsync(TResource resource, CancellationToken cancellationToken, bool forcePrepareConcurrent = false)
            {
                Requires.NotNull(resource, nameof(resource));
                Assumes.True(Monitor.IsEntered(this.service.SyncObject));

                // We deliberately ignore the cancellation token in the tasks we create and save because the tasks can be shared
                // across requests and we can't have task continuation chains where tasks within the chain get canceled
                // as that can cause premature starting of the next task in the chain.
                bool forConcurrentUse = forcePrepareConcurrent || !this.service.IsWriteLockHeld;

                AsyncReaderWriterResourceLock <TMoniker, TResource> .Helper.ResourceState finalState = forConcurrentUse ? ResourceState.Concurrent : ResourceState.Exclusive;
                object stateObject = forConcurrentUse
                    ? (object)resource
                    : Tuple.Create(resource, this.service.GetAggregateLockFlags());

                if (!this.resourcePreparationTasks.TryGetValue(resource, out ResourcePreparationTaskAndValidity preparationTask))
                {
                    Func <object, Task>?preparationDelegate = forConcurrentUse
                        ? this.prepareResourceConcurrentDelegate
                        : this.prepareResourceExclusiveDelegate;

                    // We kick this off on a new task because we're currently holding a private lock
                    // and don't want to execute arbitrary code.
                    // Let's also hide the ARWL from the delegate if this is a shared lock request.
                    using (forConcurrentUse ? this.service.HideLocks() : default(Suppression))
                    {
                        preparationTask = new ResourcePreparationTaskAndValidity(
                            Task.Factory.StartNew(NullableHelpers.AsNullableArgFunc(preparationDelegate), stateObject, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).Unwrap(),
                            finalState);
                    }
                }
                else if (preparationTask.State != finalState || preparationTask.PreparationTask.IsFaulted)
                {
                    Func <Task, object, Task>?preparationDelegate = forConcurrentUse
                        ? this.prepareResourceConcurrentContinuationDelegate
                        : this.prepareResourceExclusiveContinuationDelegate;

                    // We kick this off on a new task because we're currently holding a private lock
                    // and don't want to execute arbitrary code.
                    // Let's also hide the ARWL from the delegate if this is a shared lock request.
                    using (forConcurrentUse ? this.service.HideLocks() : default(Suppression))
                    {
                        preparationTask = new ResourcePreparationTaskAndValidity(
                            preparationTask.PreparationTask.ContinueWith(preparationDelegate !, stateObject, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default).Unwrap(),
                            finalState);
                    }
                }

                Assumes.NotNull(preparationTask.PreparationTask);
                this.resourcePreparationTasks[resource] = preparationTask;

                // We tack cancellation onto the task that we actually return to the caller.
                // This doesn't cancel resource preparation, but it does allow the caller to return early
                // in the event of their own cancellation token being canceled.
                return(preparationTask.PreparationTask.WithCancellation(cancellationToken));
            }
            /// <summary>
            /// Retrieves the resource with the specified moniker.
            /// </summary>
            /// <param name="resourceMoniker">The identifier for the desired resource.</param>
            /// <param name="cancellationToken">The token whose cancellation signals lost interest in this resource.</param>
            /// <returns>A task whose result is the desired resource.</returns>
            internal async Task <TResource> GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken)
            {
                using (AsyncReaderWriterResourceLock <TMoniker, TResource> .ResourceReleaser resourceLock = this.AcquirePreexistingLockOrThrow())
                {
                    TResource?resource = await this.service.GetResourceAsync(resourceMoniker, cancellationToken).ConfigureAwait(false);

                    Task preparationTask;

                    lock (this.service.SyncObject)
                    {
                        this.SetResourceAsAccessed(resource);

                        // We can't currently use the caller's cancellation token for this task because
                        // this task may be shared with others or call this method later, and we wouldn't
                        // want their requests to be cancelled as a result of this first caller cancelling.
                        preparationTask = this.PrepareResourceAsync(resource, cancellationToken);
                    }

                    await preparationTask.ConfigureAwait(false);

                    return(resource);
                }
            }