【问题标题】:How to handle a deadlock in third-party code如何处理第三方代码中的死锁
【发布时间】:2021-03-30 08:35:14
【问题描述】:

我们有一个第三方方法Foo,它有时会因为未知原因而陷入死锁。

我们正在执行一个单线程的 tcp-server,每 30 秒调用一次这个方法来检查外部系统是否可用。

为了缓解第三方代码中的死锁问题,我们将 ping 调用放在 Task.Run 中,以便服务器不会死锁。

喜欢

async Task<bool> WrappedFoo()
{
    var timeout = 10000; 

    var task = Task.Run(() => ThirdPartyCode.Foo());
    var delay = Task.Delay(timeout);

    if (delay == await Task.WhenAny(delay, task ))
    {
        return false;
    }
    else
    {
        return await task ;
    }
}

但这(在我们看来)有可能使空闲线程的应用程序匮乏。因为如果调用ThirdPartyCode.Foo 死锁,线程将永远无法从死锁中恢复,如果这种情况经常发生,我们可能会耗尽资源。

是否有一种处理死锁第三方代码的通用方法?

CancellationToken 不起作用,因为第三方 api 不提供任何取消选项。

更新: 手头的方法是从 SAP 提供的 SAPNCO.dll 中建立和测试到 sap-system 的 rfc-connections,因此该方法不是简单的 network-ping。为了避免进一步的误解,我重命名了问题中的方法

【问题讨论】:

  • 将 CancellationToken 与 Task.Run 一起使用
  • 让第 3 方修复他们的产品……但我猜你“现在”需要一些东西……
  • 什么是ThirdPartyCode?是图书馆吗?我想您可以尝试将其加载到separate domain,如果它停止,请重新加载。
  • “是否有一个通用的方法应该如何处理死锁的第三方代码?” - 是的,停止付款,停止使用他们的产品!
  • 所以,它是 SAP。您的公司可能会在支持计划上花一大笔钱。使用它。

标签: c# deadlock .net-5


【解决方案1】:

是否有一种处理死锁第三方代码的通用方法?

是的,但这并不容易或简单。

行为不端代码的问题在于它不仅会泄漏资源(例如线程),而且还可以无限期地占用重要资源(例如,一些内部“句柄”或“锁”)。

强制回收线程和其他资源的唯一方法是结束进程。该操作系统用于清理行为不端的进程,并且非常擅长。因此,这里的解决方案是启动一个子进程来进行 API 调用。您的主应用程序可以通过重定向 stdin/stdout 与其子进程通信,如果子进程超时,主应用程序可以终止它并重新启动它。

很遗憾,这是取消不可取消代码的唯一可靠方法。

【讨论】:

  • 请注意,除了标准输入/标准输出之外,进程还有许多其他通信方式。
  • 这将是 Erlang 监督机制的东西。应用程序通过使用目标代码启动一个单独的进程并在需要时将其终止,从而将自己与行为不端的代码隔离开来。
【解决方案2】:

您的代码没有取消被阻止的操作。使用 CancellationTokenSource 并将取消令牌传递给 Task.Run

var cts=new CancellationTokenSource(timeout);

try
{
    await Task.Run(() => ThirdPartyCode.Ping(),cts.Token);
    return true;
}
catch(TaskCancelledException)
{
    return false;
}

阻塞很可能是由于网络或 DNS 问题造成的,而不是实际的死锁。

这仍然浪费了等待网络操作完成的线程。您可以使用 .NET 自己的 Ping.SendPingAsync 异步 ping 指定超时:

var ping=new Ping();

var reply=await ping.SendPingAsync(ip,timeout);
return reply.Status==IPStatus.Success;

PingReply 类包含比简单的成功/失败更详细的信息。 Status property 单独区分路由问题、无法到达的目的地、超时等

【讨论】:

  • 不幸的是,一个简单的 ping 无法解决问题(有问题的更新),CancellationToken 也无济于事,因为 Task.Run() 方法仅检查取消 before 工作已开始但未执行时。
【解决方案3】:

取消任务是collaborative operation,因为您将CancellationToken 传递给所需的方法,并在外部使用CancellationTokenSource.Cancel

public void Caller()
{
     try
     {
          CancellationTokenSource cts=new CancellationTokenSource();
          Task longRunning= Task.Run(()=>CancellableThirdParty(cts.Token),cts.Token);
          Thread.Sleep(3000); //or condition /signal
          cts.Cancel();
     }catch(OperationCancelledException ex)
     {
          //treat somehow
     }
    
}
public void CancellableThirdParty(CancellationToken token)
{
    while(true)
    {
        // token.ThrowIfCancellationRequested()  -- if you  don't treat the cancellation here
        if(token.IsCancellationRequested)
        {
           // code to treat the cancellation signal
           //throw new OperationCancelledException($"[Reason]");
        }
    }
}

正如您在上面的代码中看到的,为了取消正在进行的任务,其中运行的方法必须围绕CancellationToken.IsCancellationRequested 标志或简单的CancellationToken.ThrowIfCancellationRequested 方法构建, 这样调用者只需发出CancellationTokenSource.Cancel

很遗憾,如果第三方代码不是围绕CancellationToken 设计的(它不接受CancellationToken 参数),那么您无能为力。

【讨论】:

  • CancellationTokenSource 可以在它生成的令牌或 链接 上设置超时,因此您可以删除等待某事的部分代码.你也可以调用[Token].ThrowIfCancellationRequested(),这样代码的其他部分就可以运行(以防Cancel()被调用或者可以在超时之前被调用)。
  • 我添加了await Delay 只是为了举例。我可以让不同的线程发出信号,或者基本上任何东西。这只是一个简单的例子。我也知道ThrowIfCancellationRequested 的方法,但是我使用该标志来表明如果他需要取消信号的自定义逻辑,它可以完成。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多