L’API WPF può essere utilizzata in modo sicuro in un servizio WCF?

Ho l’obbligo di prendere il lato client XAML (da Silverlight) e creare una bitmap fusa con una risorsa lato server (immagine ad alta risoluzione) e posso farlo abbastanza facilmente usando WPF (DrawingContext, ecc.). È stato detto che l’utilizzo del WPF da parte del server (ospitato in IIS WCF) è simile a quello di Office sul server e una pessima idea.

WPF è stato creato per funzionare sul server? Quali sono le alternative (in particolare con xaml)? Cosa devo cercare (perdite di memoria, threading, ecc.)?

L’utilizzo di WPF lato server dietro WCF non equivale all’esecuzione di Office sul lato server! WPF nel suo complesso è solo un paio di DLL, e non è in realtà diverso da usare qualsiasi altra libreria lato server. Questo è completamente diverso da Word o Excel, dove si sta caricando un’intera applicazione dietro le quinte, comprese le interfacce utente, i componenti aggiuntivi, il linguaggio di scripting, ecc.

Ho usato WPF sul server dietro WCF per anni. È una soluzione molto elegante ed efficiente:

  • Il rendering del software DirectX viene utilizzato perché non si sta disegnando su un dispositivo di visualizzazione effettivo, ma le routine di rendering del software in DirectX sono state ottimizzate in modo tale che le prestazioni e il consumo delle risorse siano pari a qualsiasi soluzione di rendering che si possa trovare e probabilmente molto meglio.

  • L’espressività di WPF consente di creare grafica complessa utilizzando il codice DirectX ottimizzato anziché eseguirlo a mano.

In pratica, l’utilizzo di WPF all’interno del servizio WCF aggiungerà circa 10 MB al footprint RAM.

Non ho avuto problemi di perdita di memoria con l’esecuzione del lato server WPF. Sto anche usando XamlReader per analizzare XAML in alberi di oggetti e ho scoperto che quando smetto di fare riferimento alla struttura ad oggetti il ​​garbage collector lo raccoglie senza problemi. Ho sempre pensato che se mi fossi imbattuto in una perdita di memoria in WPF, avrei risolto il problema eseguendo in un AppDomain separato che avresti occasionalmente riciclato, ma non l’ho mai visto.

Un problema di threading che si verificherà è che WPF richiede thread STA e WCF utilizza thread MTA. Questo non è un problema significativo poiché è ansible avere un pool di thread STA per ottenere le stesse prestazioni dei thread MTA. Ho scritto una piccola class di STAThreadPool che gestisce la transizione. Ecco qui:

// 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 _workQueue = new Queue(); 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(Func func) { T result = default(T); InvokeOnPoolThread(delegate { result = func(); }); return result; } } 

Espandere ciò che i rayburn hanno detto qui è come sto usando STAthread, WPF e Asp.net WebApi. Ho usato le estensioni del parallelo, in particolare questo file qui sotto.

 //-------------------------------------------------------------------------- // // 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 StartNew(this TaskFactory factory, Func action, TaskScheduler scheduler) { return factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, scheduler); } public static Task StartNewSta(this TaskFactory factory, Func action) { return factory.StartNew(action, sharedScheduler); } private static TaskScheduler sharedScheduler = new StaTaskScheduler(1); } /// Provides a scheduler that uses STA threads. public sealed class StaTaskScheduler : TaskScheduler, IDisposable { /// Stores the queued tasks to be executed by our pool of STA threads. private BlockingCollection _tasks; /// The STA threads used by the scheduler. private readonly List _threads; /// Initializes a new instance of the StaTaskScheduler class with the specified concurrency level. /// The number of threads that should be created and used by this scheduler. public StaTaskScheduler(int numberOfThreads) { // Validate arguments if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel"); // Initialize the tasks collection _tasks = new BlockingCollection(); // 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()); } /// Queues a Task to be executed by this scheduler. /// The task to be executed. protected override void QueueTask(Task task) { // Push it into the blocking collection of tasks _tasks.Add(task); } /// Provides a list of the scheduled tasks for the debugger to consume. /// An enumerable of all tasks currently scheduled. protected override IEnumerable GetScheduledTasks() { // Serialize the contents of the blocking collection of tasks for the debugger return _tasks.ToArray(); } /// Determines whether a Task may be inlined. /// The task to be executed. /// Whether the task was previously queued. /// true if the task was successfully inlined; otherwise, false. 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); } /// Gets the maximum concurrency level supported by this scheduler. public override int MaximumConcurrencyLevel { get { return _threads.Count; } } ///  /// Cleans up the scheduler by indicating that no more tasks will be queued. /// This method blocks until all threads successfully shutdown. ///  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; } } } } 

L’utilizzo è piuttosto semplice. Basta usare il codice qui sotto per utilizzare l’estensione

  Task 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;