简体   繁体   中英

How to timeout a blocking call to unmanaged code (.NET)?

I have a call to an unmanaged code library that hangs if passed an incorrect parameter.

IWorkspaceFactory workspaceFactory = _workspaceFactory.OpenFromString(connectionString);

If connectionString has an invalid value, then OpenFromString hangs. If I break execution, I can see from the call stack that control is still with the unmanaged library. Unfortunately, this method doesn't offer a timeout. What I need to do is, within my managed code, cancel the call after a certain timeout period. I've tried a couple of approaches, including the following:

Task<IWorkspace> task = new Task<IWorkspace>(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
if (!task.Wait(10000))
{
    throw new TimeoutException();
}
IWorkspace workspace = task.Result;

In this case, task.Wait() always returns false, and task.Result() is always null , even if a valid value for connectionString is passed.

I've also tried the following approach:

IWorkspace workspace = null;
Thread thread = new Thread(() =>
    {
        workspace = _workspaceFactory.OpenFromString(connectionString);
    });
thread.Start();
if (!thread.Join(10000))
{
    throw new TimeoutException();
}

In this case, if a valid value for connectionString is passed, execution works as expected, but if an invalid value is passed, the execution still hangs in the unmanaged code, and thread.Join() never returns.

Any suggestions on how to do this?

In the first code fragment, you don't event start a task:

Task<IWorkspace> task = new Task<IWorkspace>(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
if (!task.Wait(10000))
{
    throw new TimeoutException();
}

You'd need to start it first with task.Run() . Check "Task.Factory.StartNew" vs "new Task(...).Start" and and Task.Run vs Task.Factory.StartNew .

Anyway, this won't solve the original problem:

if a valid value for connectionString is passed, execution works as expected, but if an invalid value is passed, the execution still hangs in the unmanaged code

You're dealing with some legacy and apparently buggy code here. Fixing it on the unmanaged side would be the best option, but presumably you don't have access to the sources.

You could use the technique described by Stephen Toub's in his "How do I cancel non-cancelable async operations?" However, in the scenario you described, you may exhaust the thread pool very quickly, not mentioning other system resources potentially being acquired by OpenFromString .

I think the best you could do in this case is to offload the WorkspaceFactory object to a separate helper process (rather than a thread within your own process) and call it via remoting. If the call times out, you'd kill and restart the helper process . The OS would do the proper job on cleaning up the resources.

You could wrap this process as self-hosted WCF service , or you could run it as an out-of-proc COM singleton .

Updated :

I understand your point about leaving threads running, but I think I can work with this

If you're happy to leave a pool thread blocked indefinitely, do just this:

Task<IWorkspace> task = Task.Run(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
if (!task.Wait(10000))
{
    throw new TimeoutException();
}
IWorkspace workspace = task.Result;

I have another concern though: your _workspaceFactory may not be thread-safe, which is often the case with legacy code. Ie, it may expect to be called only on the same thread it was originally created.

You should find out why the library hangs indefinitely, that sounds like a serious error. Perhaps there is a way to validate the connectionString and throw an exception?

That aside, there is an extension method that Stephen Toub wrote in his article .NET Memory Allocation Profiling with Visual Studio 2012 that may help you achieve what you want.

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();
    using (cancellationToken.Register(() => tcs.TrySetResult(true)))
    if (task != await Task.WhenAny(task, tcs.Task))
        throw new OperationCanceledException(cancellationToken);
    return await task;
}

The method creates a proxy task that can be cancelled, which wraps the original task. The extension method creates another task with a TaskCompletionSource<bool> and registers it with the CancellationToken , it then uses Task.WhenAny to await completion of either of the tasks.

The end result is that cancelling via CancellationToken will cancel the proxy task whilst the original task will keep running in the background and will consume a thread. If the unmanaged library hangs forever this thread will be consumed forever, which is really bad. But if the operation was to eventually timeout, this may be an acceptable solution. As far as the client is concerned the task will appear cancelled.

So you can add a method that returns a task:

public Task<IWorkspace> GetWorkscpaceAsync(string connectionString)
{
    Task<IWorkspace> task = new Task<IWorkspace>.Run(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
}

And then use the extension method like this:

var tcs = new CancellationTokenSource(1000);
IWorkspace result = await GetWorskpaceAsync(connectionString).WithCancellation(tcs.Token);

I you have two threads:

  1. From start

  2. The new, which calls the _workspacefactory.

After you have started thread 2 let thread 1 wait x milliseconds. If the _workspacefactory thread succeeds let it change a shared variable to true. When thread one continues after wait let it check the common variable. If the variable is false kill thread 2.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM