【问题标题】:is it possible to make an asynchronous C/C++ function awaitable in C# without resorting to Task.Run()?是否可以在 C# 中等待异步 C/C++ 函数而不求助于 Task.Run()?
【发布时间】:2021-10-14 13:06:19
【问题描述】:

我有一个用于网络 I/O 的 C++ 库。它使用 boost::asio 处理 TCP/IP 堆栈并使用 boost::spsc_queue 处理数据管道。它当然相当复杂,但它带有一个非常易于使用的 C++ API,可以将所有复杂性隐藏在底层。然而,这是有代价的。我目前花费时间的代价是处理库中异步 I/O 的底层函数在 API 中被公开为阻塞函数(带有超时)。也就是说,它们不再是异步的了。

C++ API 也有一个 C# 包装器。我想做的是在 C# 包装器中使用异步包装器方法公开底层库的固有异步性。但我不知道如何/是否可以为此准备我的 C++ API,我不知道如何/是否可以在我的 C# 包装器中装饰互操作样板,以便这些方法可以使用 async/await 模式。下面是一个完整的工作示例,其中使用 C# 模拟了消费者对象(为了简单起见,不是异步的)。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ASIOSimulation
{
    // pretend this is a C++ library based on Boost libraries:
    public class ConsumerBasedOnAsioAndSpscQueue
    {
        private bool _hasNewData = false;
        private bool _running = false;
        private Thread _thread;
        public bool HasNewData => _hasNewData;
        // pretend this is actually an asynchronous data consuming process:
        private void Consume()
        {
            while (_running)
            {
                _hasNewData = false;
                Thread.Sleep(500);
                _hasNewData = true;
                Thread.Sleep(500);
            }
        }

        public void LaunchConsumer()
        {
            _running = true;
            _thread = new Thread(Consume);
            _thread.Start();
        }

        public void KillConsumer()
        {
            _running = false;
            if (_thread != null)
            {
                if (_thread.IsAlive)
                    _thread.Join();
            }
        }
    }

    // pretend this is the C++ API for the library above
    public class APICpp : IDisposable
    {
        private readonly ConsumerBasedOnAsioAndSpscQueue _consumerBasedOnAsioAndSpscQueue = new();
        public APICpp()
        {
            _consumerBasedOnAsioAndSpscQueue.LaunchConsumer();
        }
        // the API for the background process gives me a blocking call
        // to fetch the 'data' with
        public int BlockingCallToConsumer()
        {
            while (!_consumerBasedOnAsioAndSpscQueue.HasNewData)
            {
                Thread.Sleep(10);
            }
            // new data has arrived! return the 'data':
            return 1;
        }
        // how do I/can I make a nonblocking version of the above that is awaitable?

        public void Dispose() => _consumerBasedOnAsioAndSpscQueue.KillConsumer();
    }


    // C# wrapper to C++ API---I will do my best to be async :/.
    public class APIWrapperCSharp
    {

        private readonly APICpp _apiCpp = new();
        // in reality we need something like:
        // [DllImport(APICpp, CallingConventions = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)]
        // public static extern int BlockingCallToConsumer();
        public int BlockingCallToConsumer() => _apiCpp.BlockingCallToConsumer();

        // what I want to implement:
        public Action<int> SomeFunctionToRunAfterCallReturns { get; set; }
        //public async Task NonBlockingCallAsync()
        //{
        //    // what I don't know how to do:
        //    int resutl = await _apiCpp.NonBlockingCallToConsumer();
        //    SomeFunctionToRunAfterCallReturns!.Invoke(result);
        //}

        // the 'wrong' way to do it
        public async Task BlockingCallAsync()
        {
            
            int result = await Task.Run(()=>_apiCpp.BlockingCallToConsumer());
            SomeFunctionToRunAfterCallReturns!.Invoke(result);
        }
    }

    // a client
    public class Client
    {
        public void CallIt()
        {
            APIWrapperCSharp client = new();
            int result = client.BlockingCallToConsumer();
            Console.WriteLine("A non-async call to a call that blocks on an asynchronous process just got new data. Result: {0}", result);
        }

        public async Task CallItAsync()
        {
            APIWrapperCSharp client = new();
            client.SomeFunctionToRunAfterCallReturns = PrintResult;
            await client.BlockingCallAsync();
        }

        private void PrintResult(int result)
        {
            Console.WriteLine("An async call to a call that blocks on an asynchronous process just got new data. Result: {0}", result);
        }
    }
    class Program
    {


        static void Main()
        {
            Client client = new();
            Console.WriteLine("wait for new data");
            client.CallIt();
            Console.WriteLine("Control returned to Main");
            Console.WriteLine("wait for new data");
            client.CallItAsync();
            Console.WriteLine("Control returned to Main");
        }
    }
}

输出如预期:

wait for new data
A non-async call to a call that blocks on an asynchronous process just got new data. Result: 1
Control returned to Main
wait for new data
Control returned to Main
An async call to a call that blocks on an asynchronous process just got new data. Result: 1

我想有两个问题。第一个是我可以在 C# 包装器中公开一个实际上是异步的 C/C++ 函数吗?如果可以,我该怎么做?第二个更理论化。我如何/我可以从头开始编写一个异步的方法——即不是仅仅调用已经异步的 .NET 进程,而是实际上是自下而上的异步?不等待任何底层的、预先存在的异步方法?

如果我理解正确,如果后台任务是同步的,则意味着它在后台线程上运行,并在安全的情况下提供对共享对象的访问。另一方面,如果它是异步的,则意味着在某些时候必须有某种回调或事件处理机制,当共享对象准备就绪时,它会为对象带来生命。那么我该如何创建这样一种机制,我可以使用方便的 async/await 模式向客户端公开呢?

【问题讨论】:

  • C# 包装器是否支持 IAsyncResult ?可以吗?
  • @HenkHolterman 不。实际上,直到现在我才知道那是什么。我需要一些时间来理解它是否可以......但我不明白为什么不。
  • 当您拥有 IAsyncResult API 时,您可以查看 Task.FromAsync()。这就是 .net 中大多数异步 I/O 的实现方式。
  • WinRT 使用 IAsyncOperationIAsyncAction
  • 要使IAsyncResult 工作,C++ 代码需要实际传递请求并返回,而不是阻塞。所以它需要某种机制来Begin 一个请求和End 它带有一个回调

标签: c# c++ async-await boost-asio


【解决方案1】:

第一个问题是,我可以在 C# 包装器中公开一个实际上是异步的 C/C++ 函数吗?如果可以,我该怎么做?

多年来,这一直在我的“博客”列表中。大多数人不需要它。

简短的回答是,正如您所怀疑的那样,您需要某种回调系统。

Win32 API(和密切相关的 API)的常用方法是使用 overlapped I/O。然后.NET 包装器使用ThreadPool.BindHandleHANDLE 绑定到线程池中内置的I/O 完成端口。最后,异步操作在将OVERLAPPED 结构传递给非托管代码时指定一个回调(例如,使用Overlapped.Pack)。这个回调可以完成一个TaskCompletionSource&lt;T&gt;,完全避免了IAsyncResult的需要。

这是内置于 Windows 的系统的常用方法,但对于第三方非托管库并不常见。一个原因是因为编写真正异步的非托管代码非常困难。大多数非托管库根本不关心异步代码。少数经常使用定制回调或一些类似的自定义方法。如果 C++ API 使用类似简单回调的东西,那么您可以编组一个直接完成 TaskCompletionSource&lt;T&gt; 的委托。

第二个更具理论性。我如何/我可以从头开始编写一个异步的方法——即不是仅仅调用已经异步的 .NET 进程,而是实际上从最底层开始是异步的?不等待任何底层的、预先存在的异步方法?

这可以使用TaskCompletionSource&lt;T&gt; 来完成。棘手的部分(尤其是在编组到非托管代码时)是如何设置回调以完成 TaskCompletionSource&lt;T&gt;

那么我该如何创建这样一种机制,以便我可以使用方便的 async/await 模式向客户公开呢?

目前还没有办法跨互操作边界编组async/await。非托管 C++ 世界没有标准的通用 Promise/Future 类型(即Task&lt;T&gt; / TaskCompletionSource&lt;T&gt;),因此库使用它们提供的任何东西。然后托管世界采用任何解决方案,并(最终)将其包装在TaskCompletionSource&lt;T&gt; 中,这会产生一个Task&lt;T&gt;,它可以是awaited。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-10
  • 2017-04-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-07-06
相关资源
最近更新 更多