Search code examples
c#multithreadingasync-awaitresumesuspend

C# Suspending Thread until Server responds


I am trying to make a function that when called returns back information to the caller that is on a server. What I want in this function, is that it creates a thread that issues the command to the server, and then suspends itself until the server responds back with the answer.

public AccountState GetAccount(string key)
{
  AccountState state = null;
  Thread t = new Thread(() =>
  {
    _connection.SomeCommandSentToServer(key);
    accountRequests.TryAdd(key, (Thread.CurrentThread, null));
    //Suspend current thread until ServerReponseHere is called
    Thread.CurrentThread.Suspend();
    //We have been resumed, value should be in accountRequests now
    accountRequests.TryRemove(key, out var item);
    state = item.AccountState;
  });
  t.Start();

  return state;
}


public ConcurrentDictionary<string, (Thread Thread, AccountState AccountState)> accountRequests = new ConcurrentDictionary<string, (Thread Thread, AccountState AccountState)>();

///Once server is done with processed command, call to this function made
public void ServerReponseHere(string key, AccountState state)
{
  accountRequests.TryGetValue(username, out var item);
  accountRequests.TryUpdate(username, (item.Thread, new AccountState()), item);
  item.Thread.Resume();
}

My Idea then is that in a different function, when server responds back, it calls the ResumeThread function shown above.

C# says that Suspend / Resume are deprecated functions however, -- what is a better way to do this?


UPDATE

Clarification about "SomeCommandSentToServer" -- This just sends a command to the server via TCP sockets.

In that call, all that is really happening is a transmission to the server. I'm using a library that uses WinSock2.h call of "Send()" -- Yes I know it is a deprecated library... but the library I'm using requires it.

I have a separate thread that polls input from the server. So I have no way to "await" on this SomeCommandSentToServer -- I would need to await on some sort of call back function (aka the resume function I was mentioning) -- to make this work.

I am unsure how to do that


Solution

  • With all the information available from the question, here is what you should aim for when using the async / await pattern:

    public async Task<AccountState> GetAccountAsync(string key)
    {
        // The method SomeCommandSentToServerAsync must be changed to support async.
        AccountState state = await _connection.SomeCommandSentToServerAsync(key);
    
        return state;
    }
    

    It is highly unlikely that you need anything else. By that, I mean you will not have to manipulate threads directly, put them in a concurrent dictionary and manually suspend or resume them because it looks horrible from a maintenance perspective ;)

    .NET will take care of the threading part, meaning the magic of the async infrastructure will most likely release the current thread (assuming a call is actually made to the server) until the server returns a response.

    Then the infrastructure will either use the existing synchronization context -if you are on a UI thread for instance- or grab a thread from the thread pool -if not- to run the rest of the method.

    You could even reduce the size of the method a bit more by simply returning a Task with a result of type AccountState:

    public Task<AccountState> GetAccountAsync(string key)
    {
        // The method SomeCommandSentToServerAsync must be changed to support async.
        return _connection.SomeCommandSentToServerAsync(key);
    }
    

    In both example, you will haver to make the callers async as well:

    public async Task TheCallerAsync()
    {
        // Grab the key from somewhere.
        string key = ...;
    
        var accountState = await <inst>.GetAccountAsync(key);
    
        //  Do something with the state.
        ...
    }
    

    Turning a legacy method into an async method

    Now, regarding the legacy SomeCommandSentToServer method. There is a way to await that legacy method. Yes, you can turn that method into an asynchronous method that can be used with the async / await.

    Of course, I do not have all the details of your implementation but I hope you will get the idea of what needs to be done. The magical class to do that is called TaskCompletionSource.

    What it allows you to do is to give you access to a Task. You create the instance of that TaskCompletionSource class, you keep it somewhere, you send the command and immediately return the Task property of that new instance.

    Once you get the result from your polling thread, you grab the instance of TaskCompletionSource, get the AccountState and call SetResult with the account state. This will mark the task as completed and do the resume part you were asking for :)

    Here is the idea:

        public Task<AccountState> SomeCommandSentToServerAsync(string key)
        {
            var taskCompletionSource = new TaskCompletionSource<AccountState>();
    
            //  Find a way to keep the task in some state somewhere
            //  so that you can get it the polling thread.
    
            //  Do the legacy WinSock Send() command.
    
            return taskCompletionSource.Task;
        }
    
        // This would be, I guess, your polling thread.
        // Again, I am sure it is not 100% accurate but 
        // it will hopefully give you an idea of where the key pieces must be.
        private void PollingThread()
        {
             while(must_still_poll)
             {
                 //  Waits for some data to be available.
    
                 //  Grabs the data.
    
                 if(this_is_THE_response)
                 {
                     // Get the response and built the account state somehow...
    
                     AccountState accountState = ...
    
                     // Key piece #1
                     // Grab the TaskCompletionSource instance somewhere.
    
                     // Key piece #2
                     // This is the magic line:
                     taskCompletionSource.SetResult(accountState);
    
                     // You can also do the following if something goes wrong:
                     // taskCompletionSource.SetException(new Exception());
                 }
             }
        }