4
\$\begingroup\$

I've created a generic lock class with a timeout and EventHandlers. I'm asking here for any improvements in performance or techniques.

using System;
using System.Threading;
public class GenericLock<T>(T value) where T : notnull
{
    public DateTime StartTime { get; private set; }

    public DateTime EndTime { get; private set; }

    public event EventHandler<Action>? LockAquired;
    public event EventHandler? LockReleased;
    public event EventHandler? LockFailed;
        
    public GenericLock(T value, TimeSpan timeout) : this(value) => Timeout = timeout;

    public TimeSpan Timeout 
    {
        get => EndTime - StartTime; 
        set
        {
            StartTime = DateTime.Now;                
            EndTime = StartTime.Add(value);
        }
    }

    private bool LockResult => DateTime.Now < EndTime && Monitor.TryEnter(value, Timeout);

    public void Lock(Action action)
    {            
        if (LockResult)
        {
            try
            {
                LockAquired?.Invoke(this, action);
            }
            finally
            {
                LockReleased?.Invoke(this, EventArgs.Empty);
                Monitor.Exit(value);
            }
        }
        else
        {
            LockFailed?.Invoke(this, EventArgs.Empty);
        }
    }        
}
\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

I dropped an upvote on the other two answers, but I saw one more opportunity - someone could do something unpleasant like this (using the Peter Csala code):

object obj = new object();

using (GenericLock<object> lockObject = new GenericLock<object>(obj, TimeSpan.FromSeconds(5)))
{
    lockObject.LockAcquired += (sender, e) =>
    {
        (sender as GenericLock<object>).Lock(() => Console.WriteLine("This may go very badly."));
         Console.WriteLine($"Lock acquired on {e.LockedObject} with success: {e.Success}, message: {e.Message}");
    };
    lockObject.Lock(() => Console.WriteLine("Hello, World!"));
}

and it does go very badly with a StackOverflowException! A quick little check can help with that:

private bool _inLock;
public event EventHandler<LockEventArgs>? LockActionInProgress;
public void Lock(Action action)
{
    if (_disposed) throw new ObjectDisposedException(nameof(GenericLock<T>));

    if (LockResult)
    {
        if (_inLock)
        {
            Monitor.Exit(_value);
            var args = new LockEventArgs(_value, true, null);
            LockActionInProgress?.Invoke(this, args);
            return;
        }

        _inLock = true;
        try
        {
            try
            {
                var args = new LockEventArgs(_value, true, null);
                LockAcquired?.Invoke(this, args);
                action();
            }
            finally
            {
                Monitor.Exit(_value);
                var args = new LockEventArgs(_value, true, null);
                LockReleased?.Invoke(this, args);
            }
        }
        finally
        {
            _inLock = false;
        }
    }
    else
    {
        var args = new LockEventArgs(_value, false, "Lock acquisition timed out.");
        LockFailed?.Invoke(this, args);
    }
}
\$\endgroup\$
6
\$\begingroup\$

There are a couple of problems with your code.

Timeout property

You have overcomplicated your implementation. Also using the DateTime.Now to determine whether the time has elapsed in a multi-threaded environment can cause thread safety issues.

Monitor.TryEnter can receive a timeout parameter. You don't need these StartTime and EndTime properties only just a simple TimeSpan which represents the timeout duration.

Lock method

You don't execute the provided action. Also in .NET it is suggested to always pass an EventArgs derived class to an event handler invocation.

lack of IDisposable interface

That would allow you to use your code inside a using block.

Here is a modified implementation which could be still improved but addresses the above-mentioned problems.

LockEventArgs

public class LockEventArgs : EventArgs
{
    public object LockedObject { get; }
    public bool Success { get; }
    public string? Message { get; }

    public LockEventArgs(object lockedObject, bool success, string? message)
    {
        LockedObject = lockedObject;
        Success = success;
        Message = message;
    }
}

GenericLock

public class GenericLock<T> : IDisposable where T : notnull
{
    private readonly T _value;
    private readonly TimeSpan _timeout;
    private bool _disposed;

    private bool LockResult => Monitor.TryEnter(_value, _timeout); 
   
    public event EventHandler<LockEventArgs>? LockAcquired;
    public event EventHandler<LockEventArgs>? LockReleased;
    public event EventHandler<LockEventArgs>? LockFailed;

    public GenericLock(T value, TimeSpan timeout)
    {
        _value = value;
        _timeout = timeout;
        _disposed = false;
    }

    public void Lock(Action action)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(GenericLock<T>));

        if (LockResult)
        {
            try
            {
                var args = new LockEventArgs(_value, true, null);
                LockAcquired?.Invoke(this, args);
                action();
            }
            finally
            {
                Monitor.Exit(_value);
                var args = new LockEventArgs(_value, true, null);
                LockReleased?.Invoke(this, args);
            }
        }
        else
        {
            var args = new LockEventArgs(_value, false, "Lock acquisition timed out.");
            LockFailed?.Invoke(this, args);
        }
    }

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                if (Monitor.IsEntered(_value))
                {
                    Monitor.Exit(_value);
                    var args = new LockEventArgs(_value, true, "Lock released via Dispose.");
                    LockReleased?.Invoke(this, args);
                }
            }

            _disposed = true;
        }
    }

    ~GenericLock()
      =>Dispose(disposing: false);
}

Usage

var obj = new object();
using (var lockObject = new GenericLock<object>(obj, TimeSpan.FromSeconds(5)))
{
    lockObject.Lock(() =>
    {
        lockObject.LockFailed += (sender, e) => {
             Console.WriteLine($"Failed to acquire lock due to {e.Message}");
        };
        //
    });
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Very good review. I have made the improvements to use DateTime.UtcNow from Rick Davin's review. I will implement the Dispose pattern but have one small suggestion\question. Should if (_disposed) throw new ObjectDisposedException(nameof(GenericLock<T>)); instead be ObjectDisposedException.ThrowIf(_disposed, this); \$\endgroup\$ Commented Jul 16 at 2:47
  • \$\begingroup\$ Can you show an example using your EventHandlers in the using \$\endgroup\$ Commented Jul 16 at 3:27
  • \$\begingroup\$ @ShortInt It is just a matter of taste whether you prefer a static method call on the exception class or make the exception throwing logic conditional. I've updated the example with an event handler sample. \$\endgroup\$ Commented Jul 16 at 7:57
5
\$\begingroup\$

Interesting. Overall your code is easy to read, with nice indentation. There are some things I would recommend for improvement.

Prefer DateTime.UtcNow over DateTime.Now

On a extremely superficial level, UtcNow is faster than Now since Now must first call UtcNow anyway. The critical reason to favor UtcNow is to prevent any runtime errors that may occur during a DST transition.

Should the events be public fields?

On a very general basis, anything your class exposes publicly should either be a property or a method. Here you use fields. I would challenge you to ask if these should be public. If not, then use of private fields is appropriate. If they need to be public, I would suggest they be properties and you may want a private setter for them.

LockResult should be a method

LockResult has a property can change upon every invocation. While the earliest days of .NET had code that did this (see DateTime.Now), the trend for the past many years is that properties should be fairly static and methods would be used when info can change with different calls. And the method name should begin with an action verb, e.g. Get, Set, or other.

Consider a Car class with SerialNo, Color, and Odometer. SerialNo should be a property that never changes. Color should be a property but it can change on very rare occasions (i.e. a paint job). But Odometer should be a method renamed GetOdometer() to reflect that the value returned from one invocation can be different from another invocation.

Granted Microsoft is one of the worst offenders. Again, look at DateTime.Now. It really should have been a method called DateTime.GetNow() but these naming guidelines had evolved long after the DateTime struct was published.

All that said, LockResult should be a method named GetLockResult().

\$\endgroup\$
1
  • 1
    \$\begingroup\$ It seems like you don't understand how events work. They are supposed to be public. Otherwise, you wouldn't be able to subscribe to them. And they are not fields. Events are similar to properties. \$\endgroup\$ Commented Jul 17 at 0:26

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.