简体   繁体   中英

Multiple calls (at once) to a method that can only run one at a time

I have a business logic method that has to be finished before it can be called again. Multiple clients have the ability to call it at once:

public void DoSomething() {}

I was thinking about solving it by making the method private, and creating a new public method to put the requests on a queue:

public void QueueSomeWork()
{
    // put on a Queue object
    // How will DoSomething get invoked now?
}

private void DoSomething() {}

I'm trying to solve this issue in an elegant way. My issue is how DoSomething() will know to run. I thought about creating a timer to check the queue, but then it would be running 24/7 for something that happens maybe twice per year.

Another thought is to have DoSomething() fire an event when it's done that something else would subscribe to, pick some work off the queue, and call DoSomething() . Is there a better way?

Why don't you use a lock guard?

Eg :

   private static Object lockGuard = new Object();
   public void DoSomething()
   {
     lock (lockGuard) 
      {
          //logic gere
      }
    }

Locking a resource will prevent access from multiple threads in the same time.

More on lock : http://msdn.microsoft.com/en-us/library/c5kehkcz(v=vs.110).aspx

If the numbers are not so high (it depends how DoSomething internals consume resource); I would go with this:

public static async void QueueSomeWork()
{
    await Task.Run(() => { DoSomething(); });
}

static readonly object lockObject = new object();
static void DoSomething()
{
    lock (lockObject)
    {
        // implementation
    }
}

And if the numbers are higher, you should put a limit on number of allowed queued tasks:

static long numberOfQueuedTasks = 0;
const long MAX_TASKS = 10000; // it depends how DoSomething internals consume resource
public static async void QueueSomeWork()
{
    if (numberOfQueuedTasks > MAX_TASKS)
    {
        var wait = new SpinWait();

        while (numberOfQueuedTasks > MAX_TASKS) wait.SpinOnce();
    }

    await Task.Run(() => { Interlocked.Increment(ref numberOfQueuedTasks); DoSomething(); });
}

static readonly object lockObject = new object();
static void DoSomething()
{
    try
    {
        lock (lockObject)
        {
            // implementation
        }
    }
    finally
    {
        Interlocked.Decrement(ref numberOfQueuedTasks);
    }
}

Simple way of doing it is by decorating the method with MethodImplOptions.Synchronized , whose function is similar to the synchronized keyword in Java:

[MethodImpl(MethodImplOptions.Synchronized)]
private void DoSomething()
{
    // ...
}

The main downside is that this will lock on the current instance, which might lead to deadlock if you're already using locking elsewhere.

Here is an idea. You'd probably want to lock the doSomethingCount when using it, but as for queuing the DoSomething and going on this might work because it runs on a separate thread. Since you were ok with a queue, I assume you want fire and forget and don't actually need to block the caller.

    // This will increment the count and kick off the process of going through
    // the calls if it isn't already running. When it is done, it nulls out the task again
    // to be recreated when something is queued again.
    public static void QueueSomething()
    {
        doSomethingCount++;
        if (doSomethingTask == null)
        {
            doSomethingTask =
                Task.Run((Action)(() =>
                {
                    while (doSomethingCount > 0)
                    {
                        DoSomething();
                        doSomethingCount--;
                    }
                }))
                .ContinueWith(t => doSomethingTask = null);
        }
    }

    // I just put something in here that would take time and have a measurable result.
    private static void DoSomething()
    {
        Thread.Sleep(50);
        thingsDone++;
    }

    // These two guys are the data members needed.
    private static int doSomethingCount = 0;
    private static Task doSomethingTask;

    // This code is just to prove that it works the way I expected. You can use it too.
    public static void Run()
    {
        for (int i = 0; i < 10; i++)
        {
            QueueSomething();
        }

        while (thingsDone < 10)
        {
            Thread.Sleep(100);
        }

        thingsDone = 0;
        QueueSomething();

        while (thingsDone < 1)
        {
            Thread.Sleep(100);
        }

        Console.WriteLine("Done");
    }

    // This data point is just so I could test it. Leaving it in so you can prove it yourself.
    private static int thingsDone = 0;

if this is code-only issue, the lock solution is good. But sometimes you run a DB transaction, where series of objects (records) have to be modified with no interference. Good example is when you re-run sequence enumeration of DB records. You can create a lock table in DB and lock a specific defined record in it for update first thing in the transaction. This will prevent other transactions created by your application (in the same code area) to even get to the table you updating. And second call will only proceed after the first one is done. Just a tip.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM