1

I have a small VB.Net project with link to sql using web service (SOAP). I have to make sure that all forms are totally responsive no matter what, and it's working pretty well. My only problem is on loading the application! The main start-up form has only single line of code:

Private Async Sub frmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Await objWebService.GetCurrentSessionsAsync
End Sub

But while this "awaitable" code is being executed the form is unresponsive, frozen and wait cursor is displayed.

Any idea on what might be causing this issue and how to handle it?

svick
  • 236,525
  • 50
  • 385
  • 514
Saleem
  • 709
  • 2
  • 13
  • 34

3 Answers3

1

The key problem is that Async does not magically make your method asynchronous. It only lets compiler know that your method will have Await keywords, and that the code needs to be converted into a state machine. Any code that is not awaited is executed synchronously, even if the method is marked as Async. Consider the following example:

Private Async Sub Form1_Load(sender As Object,
                             e As EventArgs) Handles MyBase.Load
  Await LongRunning1() 'opens the form, then asynchronously changes
                       'Text property after 2 seconds
End Sub

Private Async Function LongRunning1() As Task
  Await Task.Factory.StartNew(Sub() Threading.Thread.Sleep(2000))
  Me.Text = "Finished LongRunning1"
End Function

Here a long running process, Thread.Sleep as an example, is wrapped into a Task, and there is an Await keyword. It tells the compiler to wait for the statements inside the task to finish, before executing the next line. Without the Await, the Text property would be set immediately.

Now suppose you have some long running synchronous code in your Async method:

Private Async Sub Form1_Load(sender As Object,
                             e As EventArgs) Handles MyBase.Load
  Await LongRunning2() 'synchronously waits 2 seconds, opens the form,
                       'then asynchronously changes Text property after 2 seconds
End Sub

Private Async Function LongRunning2() As Task
  Threading.Thread.Sleep(2000)
  Await LongRunning1()
  Me.Text = "Finished LongRunning2"
End Function

Notice in this case it synchronously waits for the Thread.Sleep to finish, so for the end user you app appears as hanging. Bottom line is - you have to know which method calls can be long running, and wrap them into a task based await model. Otherwise you may be seeing the problem you are seeing.

If this sounds too complicated, you can fire up a background worker (.NET 2.0+), or use TPL (.NET 4.0+) to start a task. If you wish to go into lower level, threading is available since .NET 1.1. Then display some wait/progress window/overlay on top of the form/control, for which the functionality is not yet available. Check these out:

Thanks to @PanagiotisKanavos for pointing me in the right direction.

Community
  • 1
  • 1
Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • 1
    This is incorrect. You *can* have asynchronous event handlers and the `async void` or `Async Sub` syntax exists for this reason. It's actually a bug to use this syntax for anything other than event handlers. – Panagiotis Kanavos Oct 21 '14 at 07:59
  • @PanagiotisKanavos: You are right. I gave this a bit more research, and edited my answer. Let me know what you think. – Victor Zakharov Oct 21 '14 at 13:47
  • 1
    I noticed that too later afrer I marked your answer! Thank you for the update. – Saleem Oct 21 '14 at 15:34
  • Guess what, my problem was only with the first call for objWebService! If I call more after the first one the form keep responsive! I'll provide the solution I made later so you can check it ;) – Saleem Oct 21 '14 at 19:32
  • @Saleem: One of the method calls inside the primary call is long running on first call, but all subsequent calls are instant/cached? – Victor Zakharov Oct 21 '14 at 19:33
  • Initializing the webservice was causing the unresponsive state. Maybe I'm not saying it clear enough, wait I'll post the code approach I used to fix the situation – Saleem Oct 21 '14 at 19:37
  • @Saleem: First call long running/timeout issue? We also have it with WCF. – Victor Zakharov Oct 21 '14 at 19:39
  • Yes right. Good to know that, I was seriously considering migrating to WCF at a point! – Saleem Oct 21 '14 at 19:40
  • 1
    @Saleem: Web service is usually a thin layer, that merely packages objects into a safe form, so ideally it should not take long for the first call. Regardless, WCF is old, you may want to consider Web API. http://www.reddit.com/r/csharp/comments/17oyq6/do_people_still_use_wcf_or_is_web_api_the_new/ It also seems to have first call issues, however - http://stackoverflow.com/questions/26160950/asp-net-web-api-2-ef6-first-call-initialization-performance, but those may be unrelated. – Victor Zakharov Oct 21 '14 at 19:44
  • Hi Neolisk, please check my solution. If you can explain more the fix I would appreciate (if there is a need to). Thanks. – Saleem Oct 22 '14 at 10:37
1

In regard to your answer, the code can be much cleaner if you don't combine different programming patterns, check this out:

Private Async Sub frmMain_Load(sender As Object,
                               e As EventArgs) Handles MyBase.Load
  Dim res = Await GetCurrentSessionsAsync()
End Sub

Private Async Function GetCurrentSessionsAsync() As Task(Of com.services.Server)
  Try
    Return Await Task.Factory.
      StartNew(Function() objWebService.GetCurrentSessions)
  Catch ex As Exception
    Glob.ErrorLog("GetCurrentSessions", ex, True)
    Return New com.services.Server
  End Try
End Function

References:

Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • 1
    Brilliant!Thank you, I had a feeling that I can achieve this in a more simple way, but since I'm not an expert yet in this area I was afraid I'll never get it :$ – Saleem Oct 22 '14 at 13:47
0

So here what it is (I have to say that the answer of Neolisk and Panagiotis led me to the solution): What made my loading form unresponsive is what appeared to be a bug in web services, only the first call of my web service would produce this issue. So If the first call was made after form load, on another event, I would face same problem. To fix this, I changed the way I call my first method through web service using TaskCompletionSource variable, and calling my first method using Thread. I'll post my before/after code to be sure I delivered my fix clearly.

Before:

Private Async Sub frmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Dim res = Await objWebService.GetCurrentSessionsAsync
End Sub

After:

Private Async Sub frmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Dim res = Await GetCurrentSessionsAsync()
End Sub

Dim _tcServer As New TaskCompletionSource(Of MyProject.com.services.Server)

Private Async Function GetCurrentSessionsAsync() As Task(Of com.services.Server)
    Try
        Dim th As New System.Threading.Thread(AddressOf GetCurrentSessions)
        th.Start()
        Return Await _tcServer.Task
    Catch ex As Exception
        Return New MyProject.com.services.Server
    End Try
End Function

Private Sub GetCurrentSessions()
    Try
        Dim res = objWebService.GetCurrentSessions
        _tcServer.SetResult(res)
    Catch ex As Exception
        Glob.ErrorLog("GetCurrentSessions", ex, True)
    End Try
End Sub

I hope this can help others in the future. Thank you.

Saleem
  • 709
  • 2
  • 13
  • 34