Является ли семафор подходящим инструментом для этой работы по захвату/сохранению видеоряда?

Я работаю над проектом WPF с С# (.NET 4.0), чтобы захватить последовательность из 300 видеокадров с высокоскоростной камеры, которые необходимо сохранить на диск (формат BMP). Видеокадры должны быть захвачены с почти точными временными интервалами, поэтому я не могу сохранять кадры на диск по мере их захвата — дисковый ввод-вывод непредсказуем и сбивает временные интервалы между кадрами. Карта захвата имеет около 60 доступных кадровых буферов.

Я не уверен, что лучший подход для реализации решения этой проблемы. Мои первоначальные мысли заключаются в том, чтобы создать поток «BufferToDisk», который сохраняет изображения из буферов кадров по мере их появления. В этом сценарии основной поток захватывает буфер кадра, а затем сигнализирует потоку, чтобы указать, что можно сохранить кадр. Проблема в том, что кадры захватываются быстрее, чем поток может сохранить файлы, поэтому для решения этой проблемы требуется какая-то синхронизация. Я думал, что семафор будет хорошим инструментом для этой работы. Однако я никогда не использовал семафор таким образом, поэтому я не уверен, как действовать дальше.

Это разумный подход к этой проблеме? Если да, может ли кто-нибудь опубликовать код, чтобы я начал?

Любая помощь горячо приветствуется.

Изменить: после просмотра связанного отрывка из книги "Threading in C# - Part 2" я решил реализовать решение, адаптировав пример класса "ProducerConsumerQueue". Вот мой адаптированный код:

class ProducerConsumerQueue : IDisposable
{
    EventWaitHandle _wh = new AutoResetEvent(false);
    Thread _worker;
    readonly object _locker = new object();
    Queue<string> _tasks = new Queue<string>();

    public ProducerConsumerQueue()
    {
        _worker = new Thread(Work);
        _worker.Start();
    }

    public void EnqueueTask(string task)
    {
        lock (_locker) _tasks.Enqueue(task);
        _wh.Set();
    }

    public void Dispose()
    {
        EnqueueTask(null);     // Signal the consumer to exit.
        _worker.Join();         // Wait for the consumer's thread to finish.
        _wh.Close();            // Release any OS resources.
    }

    void Work()
    {
        while (true)
        {
            string task = null;
            lock (_locker)
                if (_tasks.Count > 0)
                {
                    task = _tasks.Dequeue();
                    if (task == null)
                    {
                        return;
                    }
                }
            if (task != null)
            {
                // parse the parameters from the input queue item
                string[] indexVals = task.Split(',');
                int frameNum = Convert.ToInt32(indexVals[0]);
                int fileNum = Convert.ToInt32(indexVals[1]);
                string path = indexVals[2];
                // build the file name
                string newFileName = String.Format("img{0:d3}.bmp", fileNum);
                string fqfn = System.IO.Path.Combine(path, newFileName);
                // save the captured image to disk
                int ret = pxd_saveBmp(1, fqfn, frameNum, 0, 0, -1, -1, 0, 0);
            }
            else
            {
                _wh.WaitOne();         // No more tasks - wait for a signal
            }
        }
    }
}

Использование класса в основной процедуре:

// capture bitmap images and save them to disk
using (ProducerConsumerQueue q = new ProducerConsumerQueue())
{
     for (int i = 0; i < 300; i++)
     {
         if (curFrmBuf > numFrmBufs)
         {
              curFrmBuf = 1;  // wrap around to the first frame buffer
         }

         // snap an image to the image buffer
         int ret = pxd_doSnap(1, curFrmBuf, 0);

         // build the parameters for saving the frame to image file (for the queue)
         string fileSaveParams = curFrmBuf + "," + (i + 1) + "," + newPath;
         q.EnqueueTask(fileSaveParams);

         curFrmBuf++;
    }
}

Довольно приятный класс — небольшое количество кода для этой функциональности.

Большое спасибо за предложения, ребята.


person PIntag    schedule 01.12.2010    source источник


Ответы (3)


Конечно, звучит разумно. Вы можете использовать семафоры или другие примитивы синхронизации потоков. Это звучит как стандартная проблема производителя/потребителя. Взгляните на здесь некоторый псевдокод.

person Szymon Rozga    schedule 01.12.2010
comment
Спасибо - это действительно похоже на проблему производителя/потребителя. Я просмотрю его и посмотрю, смогу ли я разработать решение на основе этого. - person PIntag; 02.12.2010

Что произойдет, если диск настолько медленный (например, какой-то другой процесс привязывает его), что 60 кадровых буферов недостаточно? Возможно, вам понадобится нить BufferToMemory и BufferToDisk или какая-то их комбинация. Вы хотите, чтобы основной поток (захват в буфер) имел наивысший приоритет, буфер BufferToMemory — средний, а BufferToDisk — самый низкий.

В любом случае, вернемся к семафорам. Я рекомендую вам прочитать это: http://www.albahari.com/threading/part2.aspx#_Semaphore. Семафоры должны помочь вам, хотя я бы рекомендовал SemaphoreSlim (.NET 4).

person Nelson Rothermel    schedule 01.12.2010
comment
Да, медленный дисковый ввод-вывод является серьезной проблемой, но это выделенная система, и наши тесты показали, что это очень маловероятно. - person PIntag; 02.12.2010
comment
О, кстати, спасибо за ссылку, Нельсон. Я использовал ProducerConsumerQueue для реализации решения по этой ссылке, и оно работает очень хорошо. Я понимаю, что может возникнуть проблема, если видеобуферы карты когда-либо слишком опережают сохранение файлов, но это не должно быть проблемой в среде моего приложения. Да, и карта захвата имеет 75 буферов, а не 60, как я изначально думал, так что все должно быть в порядке. - person PIntag; 02.12.2010

Поскольку вы рассматриваете это как проблему производителя/потребителя (судя по вашему ответу на ответ @siz), вы можете взглянуть на BlockingCollection<T>, который предназначен именно для такого сценария.

Он позволяет любому количеству потоков-производителей передавать данные в коллекцию и любому количеству потоков-потребителей извлекать их снова. В этом случае вам, вероятно, понадобится только один поток-производитель и один поток-потребитель.

BlockingCollection<T> выполняет всю работу по обеспечению того, чтобы поток-потребитель просыпался и процессы работали только после того, как поток-производитель сказал, что есть еще работа. И это также заботится о создании очереди работы.

person Ian Griffiths    schedule 02.12.2010
comment
Спасибо за предложение Ян. Я очень доволен решением ProducerConsumerQueue (опубликованным как редактирование в исходном сообщении), но я рассмотрю BlockingCollection‹T›. Положительные стороны пространства имен System.Collections.Concurrent в .NET 4, безусловно, значительно упрощают параллельное программирование. Давно пора, имхо. - person PIntag; 02.12.2010