0
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
    public class AsyncMain
    {
        public static void Main()
        {
            // The asynchronous method puts the thread id here.
            int threadId;

            // Create an instance of the test class.
            AsyncDemo ad = new AsyncDemo();

            // Create the delegate.
            AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);

            // Initiate the asychronous call.
            IAsyncResult result = caller.BeginInvoke(3000,
                out threadId, null, null);

            Thread.Sleep(0);
            Console.WriteLine("Main thread {0} does some work.",
                Thread.CurrentThread.ManagedThreadId);

            // Call EndInvoke to wait for the asynchronous call to complete,
            // and to retrieve the results.
            string returnValue = caller.EndInvoke(out threadId, result);

            Console.WriteLine("The call executed on thread {0}, with return value \"{1}\".",
                threadId, returnValue);
        }
    }
}

EndInvoke might block the calling thread because it does not return until the asynchronous call completes.

I will implement EndInvoke in a API, to prevent the user call the API twice. Am I using correct way to prevent same user calling the API twice at the same time?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
John Walker
  • 1,121
  • 4
  • 26
  • 68
  • 2
    `Begin` and `End` type methods are an older APM model, this primarily deals with an older way to deal with asynchronous programming. It has nothing to do with thread safety, or locking, or preventing someone from calling a method twice concurrently. You will have to implement that yourself using something with a synchronization primitive like `lock` – TheGeneral Jan 12 '21 at 05:57
  • @JohnWalker First, try to wrap your APM async calls into TPL by using the `FromAsync` ([1](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/tpl-and-traditional-async-programming)). Then you can simply issue the same call twice to run concurrently, just like this: `await Task.WhenAll(task1, task2);` – Peter Csala Jan 12 '21 at 07:42
  • 1
    @JohnWalker Can you please share with us the `AsyncDemo` class definition? And how is your `AsyncMethodCaller` delegate defined? Are they defined like [this](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/calling-synchronous-methods-asynchronously#defining-the-test-method-and-asynchronous-delegate) ? – Peter Csala Jan 12 '21 at 08:31
  • 1
    What is the actual goal here? Do you have a non-threadsafe API you want to protect? If so a simple lock or a task-queue would be much more appropriate. – JonasH Jan 12 '21 at 09:22
  • `async` isn't a synchronization mechanism. It's syntactic sugar that allows your code to use the `await` keyword to await an already executing asynchronous (ie task-returning) operation to complete without blocking the caller. No threads are explicitly created. – Panagiotis Kanavos Jan 12 '21 at 09:29
  • The real solution here is to *not* use `BeginInvoke` but a proper asynchronous method. If the intention is to run `TestMethtod` in the background, use `var result=await Task.Run(()=>ad.TestMethod())`. – Panagiotis Kanavos Jan 12 '21 at 09:34
  • @PanagiotisKanavos your method will cause the whole system only can be executed one process on one time? or just the user browser only? – John Walker Jan 12 '21 at 14:20
  • It's not my own method - assuming you use the doc example (which you haven't verified yet), *your* code is doing the same thing, in a much more verbose way. `Task.Run` is what runs the method in the background, replacing almost all of the question's code. `await` is what awaits for that operation to complete without blocking, replacing `EndInvoke`. All of the code was replaced by a single. simple line. – Panagiotis Kanavos Jan 12 '21 at 14:30
  • Besides, with `BeginInvoke` combining multiple asynchronous operations is *very* hard. With `await` and tasks, you just make another call, eg `var page=await httpClient.GetStringAsync(url); await File.WriteAllTextAsync(path.page);` – Panagiotis Kanavos Jan 12 '21 at 14:35
  • @PanagiotisKanavos u are missunderstanding my question, i would like to know this method is blocking whole system only can process one by one? if a user open two browser at the same time, and call the API at the same time, is this solution will prevent the user call the API twice with different broswer? – John Walker Jan 12 '21 at 15:32
  • 1
    What browsers?? This is a console application. There are no browsers. Nothing is blocked, `await` awaits for an already existing operation to complete without blocking the caller – Panagiotis Kanavos Jan 12 '21 at 15:33
  • 1
    I suspect your real question has nothing to do with either BeginInvoke or tasks. You assumed BeginInvoke would be the solution, so you asked about that. What is the *actual* question? – Panagiotis Kanavos Jan 12 '21 at 15:35

1 Answers1

1

If I can suppose that the AsyncDemo is implemented as in this MSDN article:

public class AsyncDemo
{
    // The method to be executed asynchronously.
    public string TestMethod(int callDuration, out int threadId)
    {
        Console.WriteLine("Test method begins.");
        Thread.Sleep(callDuration);
        threadId = Thread.CurrentThread.ManagedThreadId;
        return string.Format("My call time was {0}.", callDuration.ToString());
    }
}

and the related AsyncMethodCaller delegate is defined as this:

public delegate string AsyncMethodCaller(int callDuration, out int threadId);

then you have two options depending on the .NET version:

.NET Framework

Here we can use the Task.FromAsync (1) to convert the APM model to TPL.
I had to define two helper methods (Begin, End) to pass the extra parameters to the TestMethod.
The End method returns with a tuple (string, int) in order to avoid out parameter. (1)

public static async Task Main()
{
    var ad = new AsyncDemo();
    AsyncMethodCaller caller = ad.TestMethod;

    IAsyncResult Begin(AsyncCallback cb, object state = null)
        => caller.BeginInvoke(3000, out _, cb, state);
    (string, int) End(IAsyncResult iar)
    {
        var result = caller.EndInvoke(out var threadId, iar);
        return (result, threadId);
    };

    var task1 = Task.Factory.FromAsync(Begin, End, null);
    var task2 = Task.Factory.FromAsync(Begin, End, null);

    await Task.WhenAll(task1, task2);

    var (result1, threadId1) = await task1;
    var (result2, threadId2) = await task1;

    Console.WriteLine($"{threadId1}: {result1}");
    Console.WriteLine($"{threadId2}: {result2}");
}

The output would be:

Test method begins.
Test method begins.
3: My call time was 3000.
3: My call time was 3000.

.NET Core

Here we can't use BeginInvoke because it would throw a PlatformNotSupportedException.
The workaround here is the usage of Task.Run.

static async Task Main(string[] args)
{
    int threadId1 = 0, threadId2 = 0;

    var ad = new AsyncDemo();
    AsyncMethodCaller caller = ad.TestMethod;

    var task1 = Task.Run(() => caller(3000, out threadId1));
    var task2 = Task.Run(() => caller(3000, out threadId2));

    await Task.WhenAll(task1, task2);

    Console.WriteLine($"{threadId1}: {await task1}");
    Console.WriteLine($"{threadId2}: {await task2}");
}

The output would be:

Test method begins.
Test method begins.
4: My call time was 3000.
5: My call time was 3000.
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • 1
    Or just use `Task.Run(()=>ad.TestMethod())` in both cases. – Panagiotis Kanavos Jan 12 '21 at 09:33
  • Yepp, that's easier :D – Peter Csala Jan 12 '21 at 09:38
  • @PeterCsala so this method only wait for the user completed the task, to prevent user call the API twice, and my system are able to multiple login, example user login in firefox, and login in google chrome, at the same time click the button to request the API. is this method can resolve the issue? – John Walker Jan 12 '21 at 14:25
  • @JohnWalker My provided example does NOT call any API. Because your original question did not specify what does `TestMethod` do that's why I can't answer to your new question. – Peter Csala Jan 12 '21 at 15:04