3

Having

public class ObjFromOtherAppDomain : MarshalByRefObject
{
    public async void Do(MarshalableCompletionSource<bool> source)
    {
        await Task.Delay(1000);
        source.SetResult(true);
    }
}

public class MarshalableCompletionSource<T> : MarshalByRefObject
{
    private readonly TaskCompletionSource<T> tsc = new TaskCompletionSource<T>();

    public void SetResult(T result) => tsc.SetResult(result);
    public void SetException(Exception[] exception) => tsc.SetException(exception);
    public void SetCanceled() => tsc.SetCanceled();

    public Task<T> Task => tsc.Task;
}

Doing

  • Create new AppDomain
  • Create an instance of ObjFromOtherAppDomain within the new AppDomain
  • invoke Do method passing MarshalableCompletionSource in order later to know when async Do method is completed.
  • Once Do method is completed, trying to Unload the AppDomain
public static async Task Main()
{
    var otherDomain = AppDomain.CreateDomain("other domain");
    var objFromOtherAppDomain = (ObjFromOtherAppDomain)otherDomain        
      .CreateInstanceAndUnwrap(
          typeof(ObjFromOtherAppDomain).Assembly.FullName, 
          typeof(ObjFromOtherAppDomain).FullName);

    var source = new MarshalableCompletionSource<bool>();
    objFromOtherAppDomain.Do(source);
    await source.Task;

    //await Task.Yield();

    AppDomain.Unload(otherDomain);
}

Getting

System.Threading.ThreadAbortException: 'Thread has aborted. (Exception from HRESULT: 0x80131530) exception

Fix

Uncomment await Task.Yield(); line and Unload works well.

Short analysis

Main thread enters Do method and on the line await Task.Delay(1000), Main thread returns back to Main method, while new background thread gets pulled from ThreadPool (it's happening in otherDomain) and continues execution of continuation, in this case, rest of the Do method.

After that, the same (background) thread starts executing rest of the Main method (the part after await source.Task)

At that moment background thread hits AppDomain.Unload(otherDomain), it should be done in otherDomain and happily unload it, but, apparently, it's not.

If i'll yield (release, set free) that background thread by await Task.Yield(), new Background thread comes into play and does AppDomain.Unload happily.

Why is that?

tchelidze
  • 8,050
  • 1
  • 29
  • 49
  • 1
    `async void` is only meant for event handlers. It results in a fire-and-forget task that can't be awaited. That task may still be running when the application or ... application domain that launched it terminates. Use `async Task` if you want an asynchronous function that doesn't return anything. If you *don't* want the application to terminate before the task does, you'll have to wait for it – Panagiotis Kanavos Aug 12 '19 at 13:28
  • 1
    You could replicate this problem quite easily by using an `async Task Main()` and inside it call an `async void` method that waits eg for 10 seconds before trying to access a global object. – Panagiotis Kanavos Aug 12 '19 at 13:30
  • @PanagiotisKanavos correct, `async void` is there to support event handlers, but you can't really use `Task` here instead since `Task` isn't serializable and can't be used cross app-domain. That's why I'm passing `MarshalableCompletionSource` to `Do` method to *Wait* for its completion. – tchelidze Aug 12 '19 at 14:04
  • You can't *not* use `async Task` or `async ValueTask`. That's the invariant. What are you trying to do in the first place? There are probably other, better ways to do it. If you want two whatevers to communicate, you can use [Channels](https://ndportmann.com/system-threading-channels/), similar to Go's channels – Panagiotis Kanavos Aug 12 '19 at 14:08
  • @PanagiotisKanavos I want to run some task in new AppDomain and signal original appdomain whenever its done. I'll check out Channels, looks interesting. Thought, i still wanna understand why above exception is thrown. – tchelidze Aug 13 '19 at 09:27
  • DId you get you MarshallableCompletionSource from https://social.msdn.microsoft.com/Forums/vstudio/en-US/28277f25-5f5d-4b7c-bf1f-402937fc9f31/tasks-across-appdomain?forum=parallelextensions ? Seems similar :) After I started using it, I have almost exactly the same problem with ThreadAbortExceptions - but a different kind of them. I get exceptions stating "Unable to reset abort because no abort was requested". Funny exception. I don't see much problem with cancelling something that doesn't exist, and yet the platform authors did. – quetzalcoatl Feb 14 '20 at 23:31

1 Answers1

4

With the help of my colleague, I found out the issue.

Short

When the result is set to TaskCompletionSource, continuation attached to TaskCompletionSource.Task runs on the same thread that called TaskCompletionSource.SetResult, causing Do method not to be completed at the time AppDomain.Unload is called.

Detailed

  1. Main -> objFromOtherAppDomain.Do(source); - Thread1 starts execution of Do method

  2. Do -> await Task.Delay(1000); - Thread1 returns back to Main method and awaits source.Task, Thread2 continues execution of Do method.

  3. Do -> source.SetResult(true); - Thread2 sets the result to MarshalableCompletionSource.Task and continues execution of Main method (Without finishing up the Do method)

  4. Main -> AppDomain.Unload(otherDomain); - Thread2 tries to unload the AppDomain, but since Do method isn't yet finished, unload fails.

On the other hand, if we do await Task.Yield(), it will cause Thread2 to return from Main method to Do method and finish it up, after that AppDomain can be unloaded.

Related

Calling TaskCompletionSource.SetResult in a non blocking manner

tchelidze
  • 8,050
  • 1
  • 29
  • 49