Как заблокировать, пока асинхронная работа не закончится - PullRequest
2 голосов
/ 31 октября 2008

Я работаю над библиотекой C #, которая выгружает определенные рабочие задачи в графический процессор с помощью NVIDIA CUDA. Примером этого является добавление двух массивов вместе с использованием методов расширения:

float[] a = new float[]{ ... }
float[] b = new float[]{ ... }
float[] c = a.Add(b);

Работа в этом коде выполняется на графическом процессоре. Тем не менее, я хотел бы, чтобы это было сделано асинхронно, так что только тогда, когда необходим результат, будет выполняться код, выполняющийся в блоке ЦП (если результат еще не завершен на 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;
    }
}

Действие, переданное конструктору, является асинхронным методом, который выполняет фактическую работу на GPU. Вложенное действие - это метод асинхронного обратного вызова.

Моя главная задача - как лучше и элегантнее справиться с ожиданием, выполненным в конвертере, а также, если есть более подходящие способы решения проблемы в целом. Просто оставьте комментарий, если есть что-то, что мне нужно уточнить или объяснить дальше.

Ответы [ 4 ]

6 голосов
/ 31 октября 2008

Мне не ясно, насколько это интегрированная среда, которую вы реализуете, и сколько вы вызываете в другой код, но я бы следовал «нормальному» асинхронному шаблону в .NET, насколько насколько возможно.

3 голосов
/ 03 ноября 2008

Решение, которое я нашел, заключается в передаче функции конструктору 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 блокируются до тех пор, пока данные не будут рассчитаны и извлечены из графического процессора. Это хорошо работает с тем, как структурирован API драйвера CUDA.

Это довольно чистое и простое решение. Поскольку 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;
    };
}
1 голос
/ 31 октября 2008

Интересно, не могли бы вы использовать здесь обычный 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);
        }
    }

}
0 голосов
/ 20 сентября 2009

С помощью cudaThreadSyncronize () или memcpy () вы можете предварительно выполнять синхронные операции - подходит для Invoke ().

CUDA также позволяет запрашивать асинхронную передачу памяти с помощью callAsync () / sync () - подходит для Begin / EndInvoke () с помощью callAsync ().

...