如何超时阻止调用非托管代码(.NET)?

时间:2014-03-30 15:49:37

标签: .net asynchronous timeout task-parallel-library unmanaged

我调用了一个非托管代码库,如果传递了错误的参数,它会挂起。

IWorkspaceFactory workspaceFactory = _workspaceFactory.OpenFromString(connectionString);

如果 connectionString 的值无效,则 OpenFromString 会挂起。如果我中断执行,我可以从调用堆栈中看到控件仍然是非托管库。不幸的是,这种方法没有提供超时。我需要做的是,在我的托管代码中,在一段特定的超时时间后取消呼叫。我尝试了几种方法,包括以下内容:

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

在这种情况下, task.Wait()始终返回false,而 task.Result()始终为null,即使有效值为传递 connectionString

我也尝试了以下方法:

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

在这种情况下,如果传递 connectionString 的有效值,则执行按预期工作,但如果传递的值无效,则执行仍会在非托管代码中挂起,并且线程.Join()永远不会回来。

有关如何执行此操作的任何建议吗?

3 个答案:

答案 0 :(得分:1)

在第一个代码片段中,您没有事件启动任务:

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

您需要先使用task.Run()启动它。检查"Task.Factory.StartNew" vs "new Task(...).Start"Task.Run vs Task.Factory.StartNew

无论如何,这不会解决原来的问题:

  

如果传递了connectionString的有效值,则执行为   预期,但如果传递的值无效,则执行仍会挂起   在非托管代码中

你在这里处理一些遗留的和明显错误的代码。将其修复到非托管端将是最佳选择,但可能您无法访问源。

您可以使用Stephen Toub所描述的技术"How do I cancel non-cancelable async operations?"但是,在您描述的场景中,您可能会非常快地耗尽线程池,而不会提及OpenFromString可能获取的其他系统资源

我认为在这种情况下你能做的最好的事情就是将WorkspaceFactory对象卸载到单独的帮助程序(而不是你自己进程中的一个线程)并通过远程处理调用它。如果呼叫超时,您终止并重新启动帮助程序。操作系统将在清理资源方面做得很好。

您可以将此流程打包为self-hosted WCF service,也可以将其作为out-of-proc COM singleton运行。

<强>更新

  

我理解你关于让线程运行的观点,但我想我   可以使用这个

如果您乐意无限期地阻止池线程,请执行以下操作:

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

我还有另外一个问题:您的_workspaceFactory可能不是线程安全的,这通常是遗留代码的情况。也就是说,它可能只在最初创建的同一个线程上被调用。

答案 1 :(得分:1)

你应该找出库无限期挂起的原因,这听起来像是一个严重的错误。也许有一种方法可以验证 connectionString 并抛出异常?

除此之外,Stephen Toub在他的文章.NET Memory Allocation Profiling with Visual Studio 2012中写了一个可以帮助你达到你想要的东西的扩展方法。

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

该方法创建一个可以取消的代理任务,该任务包装原始任务。扩展方法使用TaskCompletionSource<bool>创建另一个任务,并将其注册到CancellationToken,然后使用Task.WhenAny await完成任一任务。

最终结果是通过CancellationToken取消将取消代理任务,而原始任务将继续在后台运行并将消耗一个线程。如果非托管库永久挂起线程将永远消耗,这是非常糟糕的。但如果操作最终超时,这可能是一个可接受的解决方案。就客户而言,任务将被取消。

因此,您可以添加一个返回任务的方法:

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

然后使用这样的扩展方法:

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

答案 2 :(得分:0)

我有两个主题:

  1. 从头开始

  2. new,调用_workspacefactory。

  3. 启动线程2后,让线程1等待x毫秒。如果_workspacefactory线程成功,则将共享变量更改为true。当线程1在等待之后继续时,让它检查公共变量。如果变量是假杀死线程2。