3

i have a question about multithreading applications. I use the TaskFactory to start a cpu+time intensive method. This method is a call to SAP and needs a long time to finish. The user should have an option to cancel the task. Currently i am using thread.Abort(), but i know that this method isn't the best solution to cancel it. Does anyone have a recommendation for an alternative?

Code Example:

Form_LoadAction loadbox = new Form_LoadAction();
Thread threadsapquery = null;

Task.Factory.StartNew<>(() => 
{ 
   t = Thread.CurrentThread;
   Thread.sleep(10000000000); //represents time + cpu intensive method
}

loadbox.ShowDialog();
if (loadbox.DialogResult == DialogResult.Abort)
{
   t.Abort();
}
MarcelD
  • 43
  • 4
  • 1
    Does SAP provide a way of cancelling the task? – Polyfun Jan 28 '14 at 14:57
  • I don't know, but it would be too complicated, because i use a external component for calling a query in SAP. – MarcelD Jan 28 '14 at 15:03
  • @ShellShock The SAP function call through ERPConnect runs synchronously and there does not seem to be an asynchronous version that could be cancelled while it is being executed. – hangy Jan 28 '14 at 15:10
  • 2
    It is extremely unlikely that the CLR can abort the thread when the SAP interop code is running. But it will doggedly keep trying until it returns. Which really isn't any different from you putting a cancellation test in your own code. – Hans Passant Jan 28 '14 at 15:57

2 Answers2

5

The best option is to see if the method supports any kind of cooperative cancelation.

However, if that is not possible the next best option to canceling a long running process like that is use a 2nd executable that runs the long running process then communicate with that 2nd executable over some form of IPC (WCF over Named Pipes works great for intra-machine IPC) to "proxy" all of the calls. When you need to cancel your process you can kill the 2nd proxy exe and all handles will properly be released (where Thread.Abort() would not).

Here is a complete example. There are 3 files, a common library that is shared between both executables that holds the interfaces and implementations of the proxy, a hosting app and your client app. The hosting app and common library could potentially be combined in to a single assembly.

LibraryData.dll

//ISapProxy.cs
using System.Collections.Generic;
using System.ServiceModel;

namespace LibraryData
{
    [ServiceContract]
    public interface ISapProxy
    {
        [OperationContract]
        List<SapData> QueryData(string query);

        [OperationContract]
        void Close();
    }
}


//SapProxy.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;

namespace LibraryData
{
    public class SapProxy : ISapProxy
    {
        public List<SapData> QueryData(string query)
        {
            Thread.Sleep(new TimeSpan(0, 0, 5)); //represents time + cpu intensive method

            return new List<SapData>();
        }


        public void Close()
        {
            Application.Exit();
        }
    }
}


//SapData.cs
using System.Runtime.Serialization;

namespace LibraryData
{
    [DataContract]
    public class SapData
    {
    }
}

HostApp.exe

//Program.cs
using LibraryData;
using System;
using System.ServiceModel;
using System.Windows.Forms;

namespace HostApp
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            System.Diagnostics.Debugger.Launch();
            if (args.Length > 0)
            {
                var uri = new Uri("net.pipe://localhost");
                using (var host = new ServiceHost(typeof(SapProxy), uri))
                {
                    //If a client connection fails, shutdown.
                    host.Faulted += (obj, arg) => Application.Exit();

                    host.AddServiceEndpoint(typeof(ISapProxy), new NetNamedPipeBinding(), args[0]);
                    host.Open();
                    Console.WriteLine("Service has started and is ready to use.");

                    //Start a message loop in the event the service proxy needs one.
                    Application.Run();

                    host.Close();
                }
            }
        }
    }
}

YourProgram.exe

using LibraryData;
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.Threading.Tasks;

namespace SandboxConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            var connectionName = Guid.NewGuid().ToString();
            ProcessStartInfo info = new ProcessStartInfo("HostApp", connectionName);
            info.RedirectStandardOutput = true;
            info.UseShellExecute = false;

            var proxyApp = Process.Start(info);

            //Blocks till "Service has started and is ready to use." is printed.
            proxyApp.StandardOutput.ReadLine();

            var sapProxyFactory = new ChannelFactory<ISapProxy>(new NetNamedPipeBinding(), new EndpointAddress("net.pipe://localhost/" + connectionName));
            Task.Factory.StartNew(() =>
            {
                var sapProxy = sapProxyFactory.CreateChannel();

                try
                {
                    var result = sapProxy.QueryData("Some query");

                    //Do somthing with the result;
                }
                finally
                {
                    sapProxy.Close();
                }
            });

            Console.WriteLine("ready");

            //If you hit enter here before the 5 second pause in the library is done it will kill the hosting process forcefully "canceling" the operation.
            Console.ReadLine();

            proxyApp.Kill();

            Console.ReadLine();

        }
    }
}

One bug I could not squash completely is if you "Fail Fast" the client app (like by clicking the stop icon in visual studio) it never has the opportunity to tell the hosting app to shutdown.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
-2

What you want to use is a Cancellation Token which will allow you to cancel your task without having to muck with threads explicitly like you currently are.

So, you'd modify your call to StartNew like this:

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;
var task = Task.Factory.StartNew<>(() => //your logic
     , ts.Token);

Then, if you need to cancel, you just do this:

tokenSource2.Cancel();

try
{
    task.Wait();
}
catch(AggregateException aex)
{
    //handle TaskCanceledException here
}

As alluded to in the comments, unless you are in a loop (which given your description of a CPU intensive task is probably unlikely), you will have trouble cleaning up after your SAP task, but most likely no worse than your current implementation.

Here's a good MSDN reference on task cancellation.

EDIT
Thanks to Servy for pointing out that this approach won't work for OP's scenario since he is dealing with a single, long-running method and can't check the state of the token. I'll leave it up, but again, this won't work for the OP.

Sven Grosen
  • 5,616
  • 3
  • 30
  • 52
  • Given the situation, it seems that it's not possible for the work to check the `CancellationToken` as there is only a single method being called. – Servy Jan 28 '14 at 15:15