具有本机异步回调的TaskCompletionSource在C#中不起作用

时间:2017-09-05 15:41:44

标签: c# .net asynchronous

我有一个C库,它以异步方式执行操作并在提供的回调中返回结果(函数ptr)。说这是其中一项功能:

C代码:

void c_foo(void(*cb)(char*)) {
  // spawn a thread
  // sleep in the new-thread for sometime
  // invoke the callback: cb(some_null_terminated_string)
  // exit the thread
}

我还创建了一个类似的C#代码来测试:

C#代码:

public void TestManaged(Action<string> abcd) {
  var thread = new Thread(
    () => {
      Thread.Sleep(5000);
      abcd("Done");
    });
  thread.Start();
}

正如您所看到的,它会立即将控件返回给调用者,而稍后在由它生成的线程中调用回调。

我正试图从C#使用此功能但面临问题。我认为不应该被摧毁的物体正在被摧毁。以下是示例代码:

public class Checker {
  public string ABCD = Guid.NewGuid().ToString();

  ~Checker() {
     Debug.WriteLine("DESTROYING!!!!");
  }
}

public Task<string> Foo() {
  return Task.Run(
    () => {
      var tcs = new TaskCompletionSource<string>();
      var a = new Checker(); // --- (0)
      Debug.WriteLine("T-ID: Main - " + Thread.CurrentThread.ManagedThreadId);

      // Run Either: ---------- (1)
      // This DOES NOT work, `a` gets destroyed before the callback is invoked
      TestNative(
        s => {
          Debug.WriteLine("T-ID: CB - " + Thread.CurrentThread.ManagedThreadId);
          Debug.WriteLine(a.ABCD);
          tcs.SetResult(s);
        });

      // OR:  ------------------ (2)
      // This Works
      TestManaged((s) =>
      {
        Debug.WriteLine("T-ID: CB - " + Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine(a.ABCD);
        tcs.SetResult(s);
      });

      return tcs.Task;
    });
}

原生C定义的粘合剂:

public delegate void TestCb(string s);
[DllImport("mydll", EntryPoint = "c_foo")]
public static extern void TestNative(TestCb cb);

最后,我只是将设置调用为:

Debug.WriteLine("Result: " + await Foo());

在上面的代码中,我要么注释掉(1)并使用(2),要么用另一种方式执行,这样只有其中一个执行特定的运行。问题是,我看到在标记为a的点处创建的对象(0),如果通过C调用(1)代码,则会被销毁,但在{{1}时仍保留代码是通过C#调用的,尽管它们都会产生一个线程,并且只在异步一段时间后调用传递的回调。这显然会导致(2)崩溃。

此外,如果它有用 - 如果(1)代码没有产生一个线程但是在调用者的(C)线程中调用了给定的回调,即使使用{{1}也没关系} - 周围的上下文不会被破坏。

为什么会发生这种情况?使用这些C#库(从它们自己生成的线程调用回调)的方式是什么。

1 个答案:

答案 0 :(得分:2)

在.NET中没有自动概念挂起到托管对象的本机方法。您必须告知运行时此类情况。

参考:GCHandle Structure

在你的情况下,这应该这样做:

using System.Runtime.InteropServices;

      var callbackHandle = default(GCHandle);
      Action<string> nativeCallback = s => {
        try {
          Debug.WriteLine("T-ID: CB - " + Thread.CurrentThread.ManagedThreadId);
          Debug.WriteLine(a.ABCD);
          tcs.SetResult(s);
        }
        finally {
          if (callbackHandle.IsAllocated) {
            callbackHandle.Free();
          }
        }
      };
      callbackHandle = GCHandle.Alloc(nativeCallback);
      TestNative(nativeCallback);

您必须在回调上执行此操作,而不仅仅是a,因为回调本身将在TestNative返回后进行垃圾回收。在这种情况下,在回调中使用GCHandle就足够了,因为回调会捕获a,如果回调可以到达,则a可以访问。