代码之家  ›  专栏  ›  技术社区  ›  dmedine

是否可以使异步C/C++函数在C#中不可用,而无需求助于Task。跑

  •  0
  • dmedine  · 技术社区  · 3 年前

    我有一个做网络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进程已经是异步的,但实际上是自下而上异步的?不等待任何底层的、预先存在的异步方法?

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

    0 回复  |  直到 3 年前
        1
  •  3
  •   Stephen Cleary    3 年前

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

    这是我多年来的“博客”列表。大多数人不需要它。

    简而言之,正如您所怀疑的那样,您需要某种回调系统。

    Win32 API(以及密切相关的API)的常见方法是使用 overlapped I/O 这个NET包装器,然后使用 ThreadPool.BindHandle 绑定 HANDLE 到线程池中内置的I/O完成端口。最后,异步操作在传递 OVERLAPPED 结构到非托管代码(例如,使用 Overlapped.Pack ). 此回调可以完成 TaskCompletionSource<T> ,避免了对 IAsyncResult 彻底地

    这是内置在Windows中的系统的常见方法,但对于第三方非托管库并不常见。其中一个原因是,编写真正异步的非托管代码相当困难。大多数非托管库根本不关心异步代码;少数人经常使用定制回调或类似的定制方法。如果C++API使用类似于简单回调的方法,则可以封送完成 任务完成源<T> 直接地

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

    这可以使用 任务完成源<T> 。棘手的部分(尤其是封送至非托管代码时)是如何设置回调,以便 任务完成源<T> 完成。

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

    没有办法整理 async / await 跨越互操作边界(尚未)。非托管C++世界没有标准的、通用的Promise/Future类型(即。, Task<T> / 任务完成源<T> ),所以图书馆使用他们提出的任何东西。然后,被管理的世界接受了这个解决方案,并(最终)将其包裹起来 任务完成源<T> ,生成 任务<T> 可以是 等候 预计起飞时间。