100MP文件的图像处理时间过长,如何加快速度

时间:2017-11-22 11:33:23

标签: c# asp.net multithreading

我正在开发一个ASP.NET应用程序,它可以对大图片文件(最高100MP!)进行一些成像操作。这些图片的处理实际上相对较快(不到一秒),但内存使用量可以理解为巨大(每张图像大约500MB)。当多个图片同时上传到服务器时,服务器开始同时接受所有请求,并且主机内存不足。

1)如何最大限度地减少这种内存影响? 2)如果内存影响最小化,仍然会有一个限制。那么,我是否还可以限制同时处理的图像的绝对数量?

我自己的想法和想法......

因为执行时间允许一些等待(如果请求需要几秒钟就没问题)我想通过排队图像转换函数来解决这个问题,并且只允许同时执行多达2或3张图片时间。这样,内存使用量约为1.5GB,这很好。在转向生产时,我想增加这个数字,因为那里有更多的内存。

也许:我如何应用C#多线程类(例如ConcurrentQueue,BlockingCollection,Interlocked)来确保ASP.NET请求处理程序调用的单个方法只能并行执行有限数量的实例?

请注意,昂贵的线程操作在这里并不是真正的问题,因为与转换图像的第二次操作相比,开销可以忽略不计。

public ActionResult UploadLargePicture()
{
    // Some trivial stuff like authorization

    var result = VeryMemoryIntensiveFunction(); // This is the part of the code that should have limited concurrency

    return Json(...);
}

2 个答案:

答案 0 :(得分:2)

1)减少对图像处理的内存需求

您的最终问题是由于内存限制而无法处理并发请求。

我怀疑你使用.Net函数来操作源图像。考虑以不同方式加载图像,以便它不会缓存在内存中。处理可能(或可能不)花费更长时间,但它比构建排队功能更简单可靠。

而不是

Bitmap bmp = new Bitmap(pathToFile);

使用

var bmp = (Bitmap)Image.FromStream(sourceFileStream, false, false);

请参阅https://stackoverflow.com/a/47424968/887092

仍然存在限制:根据系统的使用方式,您仍可能需要限制并发请求的数量。

2)内存减少应该足够了,但这里有一些额外的方法来保持并发限制

选项A)拒绝服务器是否过载。

当每个请求到达时,跟踪已经运行的当前请求的数量。如果它高于您定义的限制,则在RequestResponse中返回错误。在这种情况下,您可能应该使用HTTP状态代码429 - Too Many Requests

try
{
    var currentImageCounter = Interlocked.Increment(ref imageCounter);
    if (currentImageCounter > 3)
    {
        throw new Exception(""); //Should be caught and result in HttpResponse.Status = 429
    }

    //Image processing code here

}
finally
{
    Interlocked.Decrement(ref ImageCounter);
}

选项A最适合响应速度,但如果超载,用户将收到错误消息。

选项B:数据库中的队列

当每个新图像到达时,将其保存到磁盘,并将记录添加到数据库,然后触发批处理。

有一个批处理进程(控制台),每隔X秒或触发时检查表是否有不完整的工作(可以是localhost HTTP请求)。确保批处理一次只运行一个实例(使用命名的Mutex / Semaphore)。

选项B缩放最多,但没有快速响应

选项C:A和B的组合

而不是在达到阈值时拒绝(3),这些应该在数据库中排队作为后备。

选项D:我之前写的选项B的更详细的实现

  • 上传工作负载的网址(即POST ./imageProcessing/Upload)
  • 使用随机文件名将图像保存为App_Data中的文件(注意:您需要稍后添加更多功能以确保最终删除孤立文件),以及存储文件名的关系数据库中的任何其他元数据。
  • 另一个处理单个工作项的URL。 (即GET ./imageProcessing/Process?id=737)
  • 控制台应用程序(最终可以用于(Windows)服务),它使用参数调用进程URL。控制台应用程序将使用工作项处理数据库表,在处理开始和结束时标记(StartAt,EndAt)。
  • 控制台应用程序可能只有单独的线程来调用ProcessURL。该主题将通过ID主键获得下一个前1个工作项目顺序。您可以使用.Net Lock,这样一次只能有一个线程获取下一个项目。您可以控制一次运行多少个线程。
  • 稍后,您可以从Threads更改为Async / Await。但这并不会给你带来任何显着的性能好处。
  • 控制台应用程序可以在桌面上手动运行,最终打包为Windows服务。

答案 1 :(得分:0)

我最终采用了一些使用一些多线程结构的集成方法。请注意,如果您的项目不是必须立即处理图像(即请求不等待内存密集型功能),那么@Todd应答是可行的方法。下面的解决方案可以工作,但是当同时上传大量图像时,可以增加超出请求超时限制的等待时间。

using System;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Threading;

namespace MyApp.Services
{
    /// <summary>
    /// This service is meant to allow for scheduling memory intensive tasks,
    /// a maximum number of these types of tasks is defined, scheduling via
    /// this service guarantees no more than the max number of tasks are executed
    /// at once.
    /// </summary>
    public class MemoryIntensiveTaskService
    {
        private class ExecutionObject
        {
            public Func<object> Task { get; set; }
            public AutoResetEvent Event { get; set; }
            public object Result { get; set; }

            public T CastResult<T>()
            {
                return (T)Result;
            }
        }

        private static readonly int MaxConcurrency = 2;

        private static BlockingCollection<ExecutionObject> _queue = new BlockingCollection<ExecutionObject>();

        static MemoryIntensiveTaskService()
        {
            // Load MaxConcurrency number of consumers
            for (int i = 0; i < MaxConcurrency; i++)
                Task.Factory.StartNew(Consumer);
        }

        public T ScheduleTaskAndWait<T>(Func<T> action)
        {
            var executionObject = new ExecutionObject
            {
                Task = () => action(),
                Event = new AutoResetEvent(false),
                Result = null
            };

            // Add item to queue, will be picked up ASAP by a
            // consumer
            _queue.Add(executionObject);

            // Wait for completion
            executionObject.Event.WaitOne();

            return executionObject.CastResult<T>();
        }

        private static void Consumer()
        {
            while (true)
            {
                var executionObject = _queue.Take();

                // Execute task, store result
                executionObject.Result = executionObject.Task();

                // Fire event to signal to producer that execution
                // has finished
                executionObject.Event.Set();
            }
        }
    }
}