如何阻止,直到异步作业完成

jquia 发布于 2018-05-19 asynchronous 最后更新 2018-05-19 15:55 102 浏览

我正在开发一个C#库,它使用NVIDIA的CUDA将某些工作任务卸载到GPU上。一个例子是使用扩展方法将两个数组一起添加:

float[] a = new float[]{ ... }
float[] b = new float[]{ ... }
float[] c = a.Add(b);
此代码中的工作是在GPU上完成的。但是,我希望它是异步完成的,只有当需要结果时才会在CPU模块上运行代码(如果结果尚未在GPU上完成)。为此,我创建了一个隐藏异步执行的ExecutionResult类。在使用中,这看起来如下所示:
float[] a = new float[]{ ... }
float[] b = new float[]{ ... }
ExecutionResult res = a.Add(b);
float[] c = res; //Implicit converter
在最后一行,如果数据已经准备好,程序会被阻塞。我不确定在ExecutionResult类中实现这种阻塞行为的最佳方式,因为我对于同步线程和这些类型的事情不是很熟练。
public class ExecutionResult<T>
{
    private T[] result;
    private long computed = 0;
internal ExecutionResult(T[] a, T[] b, Action<T[], T[], Action<T[]>> f)
    {
        f(a, b, UpdateData); //Asych call - 'UpdateData' is the callback method
    }
internal void UpdateData(T[] data)
    {
        if (Interlocked.Read(ref computed) == 0)
        {
            result = data;
            Interlocked.Exchange(ref computed, 1);
        }
    }
public static implicit operator T[](ExecutionResult<T> r)
    {
        //This is obviously a stupid way to do it
        while (Interlocked.Read(ref r.computed) == 0)
        {
            Thread.Sleep(1);
        }
return result;
    }
}
传递给构造函数的Action是一种执行GPU实际工作的异步方法。嵌套的Action是异步回调方法。 我主要关心的是如何最好地/最优雅地处理转换器中的等待,而且如果有更合适的方法来整体解决问题。如果需要进一步阐述或解释,请留下评论。
已邀请:

dut

赞同来自:

我想知道你是否不能在这里使用常规的Delegate.BeginInvoke/Delegate.EndInvoke?如果不是,那么等待句柄(例如ManualResetEvent)可能是一个选项:

using System.Threading;
static class Program {
    static void Main()
    {
        ThreadPool.QueueUserWorkItem(DoWork);
System.Console.WriteLine("Main: waiting");
        wait.WaitOne();
        System.Console.WriteLine("Main: done");
    }
    static void DoWork(object state)
    {
        System.Console.WriteLine("DoWork: working");
        Thread.Sleep(5000); // simulate work
        System.Console.WriteLine("DoWork: done");
        wait.Set();
    }
    static readonly ManualResetEvent wait = new ManualResetEvent(false);
}
请注意,如果您真的想要使用对象,可以这样做:
using System.Threading;
static class Program {
    static void Main()
    {
        object syncObj = new object();
        lock (syncObj)
        {
            ThreadPool.QueueUserWorkItem(DoWork, syncObj);
System.Console.WriteLine("Main: waiting");
            Monitor.Wait(syncObj);
            System.Console.WriteLine("Main: done");
        }
    }
    static void DoWork(object syncObj)
    {
System.Console.WriteLine("DoWork: working");
        Thread.Sleep(5000); // simulate work
        System.Console.WriteLine("DoWork: done");
        lock (syncObj)
        {
            Monitor.Pulse(syncObj);
        }
    }
}

tet

赞同来自:

我不清楚这是一个您正在实现的框架以及您调用其他代码的程度,但我会尽可能遵循.NET中的"normal" async pattern

jquia

赞同来自:

我发现问题的解决方案是将函数传递给ExecutionResult构造函数,该构造函数执行两件事。运行时,它启动异步工作,并且另外返回另一个函数,它返回所需的结果:

private Func<T[]> getResult;
internal ExecutionResult(T[] a, T[] b, Func<T[], T[], Func<T[]>> asynchBinaryFunction)
{
   getResult = asynchUnaryFunction(a);
}
public static implicit operator T[](ExecutionResult<T> r)
{
    return r.getResult();
}
'getResult'函数阻塞,直到数据已经从GPU中计算并提取出来为止。这与CUDA驱动程序API的结构非常协调。 这是一个相当干净和简单的解决方案。由于C#允许通过访问本地作用域来创建匿名函数,因此只需将传递给ExecutionResult构造函数的方法的阻塞部分替换为...
    ...
status = LaunchGrid(func, length);
//Fetch result
    float[] c = new float[length];
    status = CUDADriver.cuMemcpyDtoH(c, ptrA, byteSize);
    status = Free(ptrA, ptrB);
return c;
}
成为...
    ...
status = LaunchGrid(func, length);
return delegate
    {
        float[] c = new float[length];
        CUDADriver.cuMemcpyDtoH(c, ptrA, byteSize); //Blocks until work is done
        Free(ptrA, ptrB);
        return c;
    };
}

jalias

赞同来自:

使用cudaThreadSyncronize()或memcpy(),您可以执行同步操作 - 适用于Invoke()。

CUDA还允许您使用callAsync()/ sync() - 请求使用callAsync()适用于Begin/EndInvoke()来请求异步内存传输。