7

During my job interview, I was given a task to create an asynchronous wrapper over some long running method, processing some data, but to create it so that only a single task could be running at a time. I was not very familiar with async/await pattern, so I did my best and wrote some mixup between task-style and event-style, so that my wrapper was holding a task currently being executed, and exposing a public method and a public event. Method took data to process as an argument, and if there was no task running, started one, if there was a task, it enqueued the data. Task was raising the public event upon completion, which was sending process results to subscribers and starting a new task if there is any enqueued.

So, as you could probably guess by that point, I failed an interview, but now that I did some research, I am trying to figure out how to properly do it (it should have also been thread-safe, but I was too busy worrying about that). So my question is, if I have

public class SynchronousProcessor
{
    public string Process(string arg)
    {
        Thread.Sleep(1500); //Work imitation
        return someRandomString;
    }
}

public class AsynchronousWrapper
{
    SynchronousProcessor proc = new SynchronousProcessor();

    public async Task<string> ProcessAsync(string arg)
    {
        return Task.Run(() => proc.Process(arg));
    } 
}

, or something like this, how do I properly handle calls to ProcessAsync(string) if there is already a task executing?

Dmitry Pavlushin
  • 612
  • 8
  • 23
  • 1
    @MickyD, there **is** a need for a wrapper, since my task was formulated like "Write a wrapper...". And `SynchronousProcessor` class should be treated like a library class, so its code remains unchanged – Dmitry Pavlushin Mar 10 '17 at 22:42
  • 1
    @MickyD how can you say that there is no need for AsyncWrapper? What if this method is called from UI and obviously you don't want to block it? – MistyK Mar 10 '17 at 22:51
  • @MickyD, anyways, I don't see how your comment solves the main problem of only allowing one task at a time and running next queued task upon completion, could you please explain that a little better? – Dmitry Pavlushin Mar 10 '17 at 22:52
  • @DmitryPavlushin imo your solution is fine. If you want it to be thread safe using lock here is the easiest solution – MistyK Mar 10 '17 at 22:53
  • @MickyD could you clarify? – MistyK Mar 10 '17 at 22:57
  • @Zbigniew `await` does not lock the whole thread, it just locks the calling `async` method, so locking down a UI is not the case here – Dmitry Pavlushin Mar 10 '17 at 22:58
  • @MickyD thanks for this article. Anything specific I should look on? – MistyK Mar 10 '17 at 22:59
  • @MickyD It really depends. If it was just about executing the workload in a new thread, I'd agree. However, here it's about providing a wrapper that ensures only one call is executed at a time. It's perfectly fine to put a `Task.Run` inside, because there will also be some non-trivial logic around it – Kevin Gosse Mar 10 '17 at 23:28
  • 1
    instead of using `lock` with a regular method call, use `SemaphoreSlim` with an async call. lock can't wrap an async call, but SemaphoreSlim can. https://msdn.microsoft.com/en-us/library/system.threading.semaphoreslim(v=vs.110).aspx, https://gist.github.com/tugberkugurlu/5917020 – ps2goat Mar 10 '17 at 23:32
  • @MickyD thanks for clarification. I was almost sure that you are referring to this article. Nevertheless as Kevin mentioned it's not always the case – MistyK Mar 11 '17 at 00:36
  • @MickyD thanks Micky, I love talking with people like you "code guru" :) I was aware of this blog but I don't always agree with everything that is written in blogs. Everything is dependent on your situation and you have to choose the solution that fits to your needs. My solution wasn't good because every created thread was locked but Kevin was able to explain it in one line (you didn't even notice the real problem there). If I have a service hosted on IIS and I know it's gonna be used by maximum of 5 people at a time then using Task.Run is generally not as bad as it's stated in this blog. – MistyK Mar 11 '17 at 09:51
  • @MickyD thanks mate, I know who Stephen Cleary is. I don't know more than experts but I don't agree with putting purism everywhere. – MistyK Mar 11 '17 at 10:03

3 Answers3

4

Many job interview questions are asked for a purpose other than to see you write the code. Usually, questions are a bit vague specifically to see what clarifying questions you ask - your questions determine how well you do. Writing code on a whiteboard is secondary at best.

I was given a task to create an asynchronous wrapper over some long running method, processing some data

First question: is this long-running method asynchronous? If so, then there would not be a need for Task.Run. But if not...

Followup question: if it's not asynchronous, should it be? I.e., is it I/O-based? If so, then we could invest the time to make it properly asynchronous. But if not...

Followup question: if we need a task wrapper (around CPU-based code or around blocking I/O code), is the environment agreeable to a wrapper? I.e., is this a desktop/mobile app and not code that would be used in ASP.NET?

create it so that only a single task could be running at a time.

Clarifying questions: if a second request comes in when one is already running, does the second request "queue up"? Or would it "merge" with an existing request? If merging, do they need to "key" off of the input data - or some subset of the input data?

Every one of these questions change how the answer is structured.

exposing a public method and a public event.

This could be what threw it. Between Task<T> / IProgress<T> and Rx, events are seldom needed. They really only should be used if you're on a team that won't learn Rx.

Oh, and don't worry about "failing" an interview. I've "failed" over 2/3 of my interviews over the course of my career. I just don't interview well.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
1

It depends on how fancy you want to get. One simple way is to store a task, and chain the subsequent tasks (with a bit of synchronization):

public class AsynchronousWrapper
{
    private Task previousTask = Task.CompletedTask;     
    private SynchronousProcessor proc = new SynchronousProcessor();

    public Task<string> ProcessAsync(string arg)
    {
        lock (proc)
        {
            var task = previousTask.ContinueWith(_ => proc.Process(arg));

            previousTask = task;

            return task;
        }           
    } 
}
VMAtm
  • 27,943
  • 17
  • 79
  • 125
Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • Out of interest, what difference does it make in compare to my answer? – MistyK Mar 11 '17 at 01:14
  • 1
    `previousTask` isn't a static variable, so other threads which are using other `AsynchronousWrapper` instance will execute in parallel. Task chaining means that the last one task have to wait all previous, which may be a surprise for consumers. And it do block the thread with `lock`, which isn't `async` really. – VMAtm Mar 11 '17 at 03:41
  • @VMAtm For the static, it's about how you interpret the requirements. A single "AsynchronousWrapper" will ensure that workload is processed synchronously on a single "SynchronousProcessor". If it's required to synchronize them globally, then indeed static is needed – Kevin Gosse Mar 11 '17 at 09:10
  • @VMAtm The lock isn't an issue, because it's just there to protect the assignment, which is a small overhead. The method is still async because the actual workload will run outside of the lock. – Kevin Gosse Mar 11 '17 at 09:10
  • 1
    @VMAtm `Task chaining means that the last one task have to wait all previous, which may be a surprise for consumers` I don't see how it is an issue, since it's the purpose of this class to begin with – Kevin Gosse Mar 11 '17 at 09:11
  • 1
    @Zbigniew The main difference is that you use the lock inside of the task. Therefore, you'll start as many threads as there is consumers, and they will all wait until it's their turn. Technically, you fill OP's requirements, but in a very inefficient way – Kevin Gosse Mar 11 '17 at 09:23
  • @KevinGosse sorry it was very late yesterday. Yes, you are absolutely right. – MistyK Mar 11 '17 at 09:31
0

As @MickyD already said, you need to know the Best Practices in Asynchronous Programming to solve such problems right way. Your solution has a code smell as it provide async wrapper with Task.Run for a synchronous code. As you were asked about the library development, it will be quite impacting your library consumers.

You have to understand that asynchronous isn't multithreading, as it can be done with one thread. It's like waiting for a mail - you don't hire a worker to wait by the mailbox.

Other solutions here aren't, well, async, because break other rule for async code: do not block async action, so you should avoid the lock construct.

So, back to your problem: if you face a task which states

only a single task could be running at a time

It is not about the lock (Monitor), it is about Semaphore(Slim). If for some reason in future you'll need to improve your code so more than one task can be executed simultaneously, you'll have to rewrite your code. In case of Semaphore usage you'll need to change only one constant. Also it has an async wrappers for waiting methods

So your code can be like this (note that the Task.Run is removed, as it is a client responsibility to provide an awaitable):

public class AsynchronousWrapper
{
    private static SemaphoreSlim _mutex = new SemaphoreSlim(1);

    public async Task<T> ProcessAsync<T>(Task<T> arg)
    {
        await _mutex.WaitAsync().ConfigureAwait(false);

        try
        {
            return await arg;
        }
        finally
        {
            _mutex.Release();
        }
    }
}
Community
  • 1
  • 1
VMAtm
  • 27,943
  • 17
  • 79
  • 125
  • 1
    Thank-you. I thought I woke up in some _it's ok to use Task.Run() everywhere_ universe. ;) +1 –  Mar 11 '17 at 04:05
  • 1
    @MickyD I know how it feels :) – VMAtm Mar 11 '17 at 04:12
  • So, I don't want to be snarky, but let's reuse your comment here: "It means that the last one task have to wait all previous, which may be a surprise for consumers". It's still the case with your implementation. And it's a good thing, since that's what OP asked for. – Kevin Gosse Mar 11 '17 at 09:14
  • 1
    Also (ok, I'm being snarky this time), you know that SemaphoreSlim.WaitAsync uses a lock to handle the race condition, right? So refering to your comment, your method isn't async. Funny how using something explicitly brushes people off, but they don't realize it's actually what is done internally. https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,4772c1d9756a4a9a – Kevin Gosse Mar 11 '17 at 09:19
  • 1
    Oh, and I forgot. "_mutex isn't a static variable, so other threads which are using other AsynchronousWrapper instance will execute in parallel". So basically you make 3 comments on my answer, then apply none of them on yours. OK. – Kevin Gosse Mar 11 '17 at 09:26
  • Lastly, please note that `ProcessAsync(Task arg)` can't be used directly because `Process(string arg)` is synchronous per OP's requirements. So either you consider that the caller needs to use `ProcessAsync(Task.Run(synchronousProcessor.Process("hello"))`, but in that case we get one more jump to the threadpool for nothing, or you should change the signature to `ProcessAsync(Func)`. Then, once the mutex is acquired, you can call the delegate inline because you're already in the threadpool (thanks to `.ConfigureAwait(false)`) – Kevin Gosse Mar 11 '17 at 11:32
  • 1
    @KevinGosse Yep, static variable - my bad. About lock inside `Semaphore` - it's slim version, so it uses lock-free implementation with `Interlocked` class. And yes, the **client** decide to use or not to use the thread pool for this – VMAtm Mar 11 '17 at 15:35
  • 1
    @KevinGosse no, using `async` doesn't create a queue, it create **concurrent** state machines, not the linked list of tasks. Thank u for your comments – VMAtm Mar 11 '17 at 15:37
  • @VMAtm `Wait` spins, but `WaitAsync` doesn't. Check the code yourself: https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,4772c1d9756a4a9a not that it really matters, using a lock around a small unit of work is perfectly fine – Kevin Gosse Mar 11 '17 at 15:37
  • @VMAtm `async` doesn't create a queue, but SemaphoreSlim will. Which lead to the same end result – Kevin Gosse Mar 11 '17 at 15:38
  • 2
    @KevinGosse was under impression from Stephen Cleary' article. Ok, not that much differences then – VMAtm Mar 11 '17 at 15:40
  • @VMAtm Yup. Don't get me wrong, I think your solution is good. It has probably a bit more overhead than mine (though I would need a benchmark to confirm), but it makes-up for it by being future-proof (as you said yourself, `If for some reason in future you'll need to improve your code so more than one task can be executed simultaneously`). I was just being in a bit of a bad mood because my answers has been downvoted based on flawed comments, and I tried to rectify that. Hopefully, I haven't been offensive. – Kevin Gosse Mar 11 '17 at 15:45