Можно ли безопасно использовать WPF API в службе WCF?

У меня есть требование взять XAML на стороне клиента (из Silverlight) и создать растровое изображение, объединенное с ресурсом на стороне сервера (изображение с высоким разрешением), и может сделать это довольно легко с помощью WPF (DrawingContext и т.д.). Было упомянуто, что серверная сторона (размещенная в IIS WCF), использующая WPF, сродни работе с Office на сервере и действительно плохой идеей.

Является ли WPF построенным для работы на сервере? Каковы альтернативы (особенно с xaml)? Что мне нужно для поиска (утечки памяти, потоки и т.д.)?

Ответы

Ответ 1

Использование WPF на стороне сервера за WCF не эквивалентно запуску на стороне сервера Office! WPF в целом представляет собой всего лишь несколько DLL, и на самом деле не отличается от использования любой другой серверной серверной части. Это полностью отличается от Word или Excel, где вы загружаете целое приложение за кулисами, включая пользовательские интерфейсы, надстройки, язык сценариев и т.д.

Я использую WPF на сервере за WCF в течение многих лет. Это очень элегантное и эффективное решение:

  • Реализация программного обеспечения DirectX используется, потому что вы не используете фактическое устройство отображения, но процедуры рендеринга программного обеспечения в DirectX были сильно оптимизированы, поэтому производительность и потребление ресурсов будут такими же хорошими, как любое решение для рендеринга, которое вы может найти и, вероятно, намного лучше.

  • Экспрессия WPF позволяет создавать сложную графику с использованием оптимизированного кода DirectX, а не делать это вручную.

Практически говоря, использование WPF из вашей службы WCF добавит около 10 МБ к вашей памяти.

У меня не было проблем с утечкой памяти при запуске WPF на стороне сервера. Я также использую XamlReader для анализа XAML в деревьях объектов и обнаружил, что когда я перестаю ссылаться на дерево объектов, сборщик мусора собирает его без проблем. Я всегда думал, что если бы я столкнулся с утечкой памяти в WPF, я бы обошел ее, запустив отдельный AppDomain, который вы иногда перерабатывали, но я никогда не сталкивался с ним.

Одна проблема с потоками, с которой вы столкнетесь, - это то, что WPF требует потоков STA, а WCF использует потоки MTA. Это не является серьезной проблемой, поскольку вы можете иметь пул потоков STA для получения той же производительности, что и в потоках MTA. Я написал небольшой класс STAThreadPool, который обрабатывает переход. Вот он:

// A simple thread pool implementation that provides STA threads instead of the MTA threads provided by the built-in thread pool
public class STAThreadPool
{
  int _maxThreads;
  int _startedThreads;
  int _idleThreads;
  Queue<Action> _workQueue = new Queue<Action>();

  public STAThreadPool(int maxThreads)
  {
    _maxThreads = maxThreads;
  }

  void Run()
  {
    while(true)
      try
      {
        Action action;
        lock(_workQueue)
        {
          _idleThreads++;
          while(_workQueue.Count==0)
            Monitor.Wait(_workQueue);
          action = _workQueue.Dequeue();
          _idleThreads++;
        }
        action();
      }
      catch(Exception ex)
      {
        System.Diagnostics.Trace.Write("STAThreadPool thread threw exception " + ex);
      }
  }

  public void QueueWork(Action action)
  {
    lock(_workQueue)
    {
      if(_startedThreads < _maxThreads && _idleThreads <= _workQueue.Count)
        new Thread(Run) { ApartmentState = ApartmentState.STA, IsBackground = true, Name = "STAThreadPool#" + ++_startedThreads }.Start();
      _workQueue.Enqueue(action);
      Monitor.PulseAll(_workQueue);
    }
  }

  public void InvokeOnPoolThread(Action action)
  {
    Exception exception = null;
    using(ManualResetEvent doneEvent = new ManualResetEvent(false))  // someday:  Recycle these events
    {
      QueueWork(delegate
      {
        try { action(); } catch(Exception ex) { exception = ex; }
        doneEvent.Set();
      });
      doneEvent.WaitOne();
    }
    if(exception!=null)
      throw exception;
  }

  public T InvokeOnPoolThread<T>(Func<T> func)
  {
    T result = default(T);
    InvokeOnPoolThread(delegate
    {
      result = func();
    });
    return result;
  }
}

Ответ 2

Развернувшись на том, что сказал Rayburns, я использую STAthread, WPF и Asp.net WebApi. Я использовал параллельные расширения, в частности, этот файл ниже.

//--------------------------------------------------------------------------
// 
//  Copyright (c) Microsoft Corporation.  All rights reserved. 
// 
//  File: StaTaskScheduler.cs
//
//--------------------------------------------------------------------------

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace System.Threading.Tasks.Schedulers
{
    public static class ParallelExtensions
    {
        public static Task StartNew(this TaskFactory factory, Action action, TaskScheduler scheduler)
        {
            return factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, scheduler);
        }

        public static Task<TResult> StartNew<TResult>(this TaskFactory factory, Func<TResult> action, TaskScheduler scheduler)
        {
            return factory.StartNew<TResult>(action, CancellationToken.None, TaskCreationOptions.None, scheduler);
        }

        public static Task<TResult> StartNewSta<TResult>(this TaskFactory factory, Func<TResult> action)
        {
            return factory.StartNew<TResult>(action, sharedScheduler);
        }

        private static TaskScheduler sharedScheduler = new StaTaskScheduler(1);
    }

    /// <summary>Provides a scheduler that uses STA threads.</summary>
    public sealed class StaTaskScheduler : TaskScheduler, IDisposable
    {
        /// <summary>Stores the queued tasks to be executed by our pool of STA threads.</summary>
        private BlockingCollection<Task> _tasks;
        /// <summary>The STA threads used by the scheduler.</summary>
        private readonly List<Thread> _threads;

        /// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary>
        /// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
        public StaTaskScheduler(int numberOfThreads)
        {
            // Validate arguments
            if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");

            // Initialize the tasks collection
            _tasks = new BlockingCollection<Task>();

            // Create the threads to be used by this scheduler
            _threads = Enumerable.Range(0, numberOfThreads).Select(i =>
            {
                var thread = new Thread(() =>
                {
                    // Continually get the next task and try to execute it.
                    // This will continue until the scheduler is disposed and no more tasks remain.
                    foreach (var t in _tasks.GetConsumingEnumerable())
                    {
                        TryExecuteTask(t);
                    }
                });
                thread.IsBackground = true;
                thread.SetApartmentState(ApartmentState.STA);
                return thread;
            }).ToList();

            // Start all of the threads
            _threads.ForEach(t => t.Start());
        }

        /// <summary>Queues a Task to be executed by this scheduler.</summary>
        /// <param name="task">The task to be executed.</param>
        protected override void QueueTask(Task task)
        {
            // Push it into the blocking collection of tasks
            _tasks.Add(task);
        }

        /// <summary>Provides a list of the scheduled tasks for the debugger to consume.</summary>
        /// <returns>An enumerable of all tasks currently scheduled.</returns>
        protected override IEnumerable<Task> GetScheduledTasks()
        {
            // Serialize the contents of the blocking collection of tasks for the debugger
            return _tasks.ToArray();
        }

        /// <summary>Determines whether a Task may be inlined.</summary>
        /// <param name="task">The task to be executed.</param>
        /// <param name="taskWasPreviouslyQueued">Whether the task was previously queued.</param>
        /// <returns>true if the task was successfully inlined; otherwise, false.</returns>
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            // Try to inline if the current thread is STA

            return
                Thread.CurrentThread.GetApartmentState() == ApartmentState.STA &&
                TryExecuteTask(task);
        }

        /// <summary>Gets the maximum concurrency level supported by this scheduler.</summary>
        public override int MaximumConcurrencyLevel
        {
            get { return _threads.Count; }
        }

        /// <summary>
        /// Cleans up the scheduler by indicating that no more tasks will be queued.
        /// This method blocks until all threads successfully shutdown.
        /// </summary>
        public void Dispose()
        {
            if (_tasks != null)
            {
                // Indicate that no new tasks will be coming in
                _tasks.CompleteAdding();

                // Wait for all threads to finish processing tasks
                foreach (var thread in _threads) thread.Join();

                // Cleanup
                _tasks.Dispose();
                _tasks = null;
            }
        }
    }
}

Использование довольно простое. Просто используйте приведенный ниже код, чтобы использовать расширение

    Task<MemoryStream> Task1 = Task.Factory.StartNewSta(() =>
            {

                /* use wpf here*/

                BitmapEncoder PngEncoder =
                    new PngBitmapEncoder();
                PngEncoder.Frames.Add(BitmapFrame.Create(Render));

                //save to memory stream 
                var Ms = new MemoryStream();

                PngEncoder.Save(Ms);                
                return Ms;
          });
    Task.WaitAll(Task1);

    return Task1.Result;