第 3 方库中的 Rouge Task 挂起主线程并导致奇怪的行为。如何检测挂起并安全重试? C#
Rouge Task in 3rd party library hanging the main thread and causing strange behaviour. How can I detect the hang and retry safely? C#
我正在使用带有第 3 方库的 C# 控制台应用程序并等待第 3 方库正在 运行ning 的任务。我一直使用 Async await “一路向下”。
我遇到的问题的完整细节是 here,但那是相当长的 post 所以我想在这里简化问题并尝试从细节中抽象它。
基本上(非常简化)代码如下:
public static async Task<byte[]> CaptureRawData()
{
await Camera.Capture();
return Camera.CurrentStream.ToArray();
}
这是在我的 Raspberry pi 上捕获图像,然后 return 将图像作为字节数组存储在内存中。
它大约每 10 秒左右捕获一次图像。
运行ning 几个小时后,Camera.Capture();线路随机无限期挂起。
我读到这可能是由于间歇性电力不足引起的,但无论如何,我只是想能够检测到挂起并重试。它每隔几个小时才会发生一次,所以我真的不介意错过一张图片,我只想能够继续并重试,而不会无限期地冻结主线程。
我受到 this other SO question 的启发,尝试 运行 任务超时,这样我就可以在超时时重试。
我改编了提供的答案之一以提供以下内容:
public static async Task<bool> CancelAfterAsync(Task startTask, TimeSpan timeout)
{
using (var timeoutCancellation = new CancellationTokenSource())
{
var delayTask = Task.Delay(timeout, timeoutCancellation.Token);
Serilog.Log.Logger.Debug("await Task.WhenAny");
var completedTask = await Task.WhenAny(startTask, delayTask);
Serilog.Log.Logger.Debug("Finished await Task.WhenAny");
// Cancel timeout to stop either task:
// - Either the original task completed, so we need to cancel the delay task.
// - Or the timeout expired, so we need to cancel the original task.
// Canceling will not affect a task, that is already completed.
timeoutCancellation.Cancel();
if (completedTask == startTask)
{
// original task completed
Serilog.Log.Logger.Debug(" await startTask;");
await startTask;
return true;
}
else
{
Serilog.Log.Logger.Debug("Timed out");
// timeout
return false;
}
}
}
public static async Task<byte[]> CaptureRawData()
{
Serilog.Log.Logger.Debug("Running task");
if (await TaskUtils.CancelAfterAsync(Camera.Capture(), TimeSpan.FromSeconds(100)))
{
Serilog.Log.Logger.Debug("Got response, returning data");
return Camera.CurrentStream.ToArray();
}
else
{
Serilog.Log.Logger.Warning("Camera timed out, return null");
return null;
}
}
public static async Task<byte[]> CaptureImage()
{
byte[] data = null;
for (var i = 0; i < 10; i++)
{
data = await CaptureRawData();
if (data != null)
{
break;
}
else
{
Serilog.Log.Logger.Warning("Camera timed out, retrying");
}
}
if (data == null || data.Length == 0)
{
//Todo: Better exception message
throw new Exception("Image capture failed");
}
return data;
}
现在,挂起后,它应该检测挂起并重试最多 10 次。但是我得到了以下日志输出:
[13:54:54 DBG] 运行 任务
[13:54:54 DBG] 等待 Task.WhenAny
[13:56:34 DBG] 完成等待 Task.WhenAny
[13:56:34 DBG] 超时
[13:56:34 WRN] 相机超时,return null
然后它无限期地挂在“return null”行,它应该在该行之后直接记录“相机超时,重试”,但它永远不会,只是永远挂在“return 空"。
这没有任何意义,因为 CancelAfterAsync 方法已清楚地检测到挂起并且 returned false,但随后挂起的是父方法。
我怎样才能安全地检测到挂起并重试?
如前所述,它很少发生,在调用此方法数百次后每隔几个小时发生一次,所以我只想能够检测到它发生并重试而不锁定所有内容。
编辑:正如评论中所建议的那样,我尝试了 运行 在 Task.Run 中执行流氓任务,并从我的程序中删除了所有异步,如下所示:
public static class MemoryCapture
{
private static volatile bool _camProcessing = false;
public static byte[] CaptureRawData()
{
MMALCamera cam = MMALCamera.Instance;
MMALCameraConfig.Debug = true;
MMALCameraConfig.StillEncoding = MMALEncoding.BGR24;
MMALCameraConfig.StillSubFormat = MMALEncoding.BGR24;
using (var imgCaptureHandler = new MemoryStreamCaptureHandler())
using (var renderer = new MMALNullSinkComponent())
{
cam.ConfigureCameraSettings(imgCaptureHandler);
cam.Camera.PreviewPort.ConnectTo(renderer);
// Camera warm up time
Thread.Sleep(2000);
if (WaitForCam(cam))
{
var result = imgCaptureHandler.CurrentStream.ToArray();
return result;
}
else
{
Serilog.Log.Logger.Warning($"Reached timeout, returning null...");
return null;
}
}
}
private static bool WaitForCam(MMALCamera cam)
{
_camProcessing = true;
Serilog.Log.Logger.Debug("Running cam process task");
Task.Run(() =>
{
Serilog.Log.Logger.Debug($"cam.ProcessAsync");
cam.ProcessAsync(cam.Camera.StillPort).ConfigureAwait(false).GetAwaiter().GetResult();
Serilog.Log.Logger.Debug($"cam.ProcessAsync finished");
_camProcessing = false;
});
for (var i = 0; i < 1000; i++)
{
Thread.Sleep(100);
if (!_camProcessing)
{
Serilog.Log.Logger.Debug($"cam processing finished");
return true;
}
}
Serilog.Log.Logger.Warning($"Reached timeout, camera might have locked up");
return false;
}
public static byte[] CaptureImageHelper()
{
byte[] data = null;
for (var i = 0; i < 10; i++)
{
data = CaptureRawData();
if (data != null)
{
break;
}
Serilog.Log.Logger.Warning($"Retrying...");
}
if (data == null)
{
throw new Exception("Image capture failed");
}
return data;
}
}
}
该代码的日志输出如下:
[23:02:29 DBG] 运行 凸轮处理任务
[23:02:29 警告]cam.ProcessAsync
[23:04:09 WRN] 超时,相机可能已锁定
[23:04:09 WRN] 达到超时,returning null...
然后它永远挂起。
从 CaptureRawData 中 returning 时挂起,因此它可能在处理其中一个使用时挂起。
How can I detect the hang and retry safely?
对此只有一个答案:运行 该代码在一个单独的进程中。您可以重定向 stdin/stdout 以充当“command/response”通道,如果响应时间过长,则终止整个进程并重新启动它。这是相当重量级的,但这是唯一正确取消不可取消代码的方法。
之所以需要一个单独的进程是因为您的代码需要清理 API 正在做的所有事情,而在挂起 API 的情况下,唯一的方法是让 OS 介入并进行清理。在 API 可能对硬件资源具有某种独占锁的情况下尤其如此。
CancelAfterAsync
的问题在于它只取消了任务的等待。 Camera.Capture
方法仍在进行中,可能会无限期地保持任何硬件资源打开。因此,即使您可以正常工作,也不能保证开始 second Camera.Capture
会完全有效。在单独的进程中使用 Camera.Capture
会更干净,终止该进程(让 OS 进入并清理所有内容,包括硬件资源的句柄),然后重新启动它。
It's hanging when returning from CaptureRawData, so it's possible that it's hanging while disposing one of the usings.
很有可能。由于已经有一个 Camera.Capture
运行ning(并已挂起),因此处理可能正在等待访问无限期使用的硬件资源。同样,这应该通过使用单独的进程来解决,因为 OS 将介入并在进程被终止时强制关闭这些句柄。
我正在使用带有第 3 方库的 C# 控制台应用程序并等待第 3 方库正在 运行ning 的任务。我一直使用 Async await “一路向下”。 我遇到的问题的完整细节是 here,但那是相当长的 post 所以我想在这里简化问题并尝试从细节中抽象它。
基本上(非常简化)代码如下:
public static async Task<byte[]> CaptureRawData()
{
await Camera.Capture();
return Camera.CurrentStream.ToArray();
}
这是在我的 Raspberry pi 上捕获图像,然后 return 将图像作为字节数组存储在内存中。 它大约每 10 秒左右捕获一次图像。 运行ning 几个小时后,Camera.Capture();线路随机无限期挂起。
我读到这可能是由于间歇性电力不足引起的,但无论如何,我只是想能够检测到挂起并重试。它每隔几个小时才会发生一次,所以我真的不介意错过一张图片,我只想能够继续并重试,而不会无限期地冻结主线程。
我受到 this other SO question 的启发,尝试 运行 任务超时,这样我就可以在超时时重试。
我改编了提供的答案之一以提供以下内容:
public static async Task<bool> CancelAfterAsync(Task startTask, TimeSpan timeout)
{
using (var timeoutCancellation = new CancellationTokenSource())
{
var delayTask = Task.Delay(timeout, timeoutCancellation.Token);
Serilog.Log.Logger.Debug("await Task.WhenAny");
var completedTask = await Task.WhenAny(startTask, delayTask);
Serilog.Log.Logger.Debug("Finished await Task.WhenAny");
// Cancel timeout to stop either task:
// - Either the original task completed, so we need to cancel the delay task.
// - Or the timeout expired, so we need to cancel the original task.
// Canceling will not affect a task, that is already completed.
timeoutCancellation.Cancel();
if (completedTask == startTask)
{
// original task completed
Serilog.Log.Logger.Debug(" await startTask;");
await startTask;
return true;
}
else
{
Serilog.Log.Logger.Debug("Timed out");
// timeout
return false;
}
}
}
public static async Task<byte[]> CaptureRawData()
{
Serilog.Log.Logger.Debug("Running task");
if (await TaskUtils.CancelAfterAsync(Camera.Capture(), TimeSpan.FromSeconds(100)))
{
Serilog.Log.Logger.Debug("Got response, returning data");
return Camera.CurrentStream.ToArray();
}
else
{
Serilog.Log.Logger.Warning("Camera timed out, return null");
return null;
}
}
public static async Task<byte[]> CaptureImage()
{
byte[] data = null;
for (var i = 0; i < 10; i++)
{
data = await CaptureRawData();
if (data != null)
{
break;
}
else
{
Serilog.Log.Logger.Warning("Camera timed out, retrying");
}
}
if (data == null || data.Length == 0)
{
//Todo: Better exception message
throw new Exception("Image capture failed");
}
return data;
}
现在,挂起后,它应该检测挂起并重试最多 10 次。但是我得到了以下日志输出:
[13:54:54 DBG] 运行 任务
[13:54:54 DBG] 等待 Task.WhenAny
[13:56:34 DBG] 完成等待 Task.WhenAny
[13:56:34 DBG] 超时
[13:56:34 WRN] 相机超时,return null
然后它无限期地挂在“return null”行,它应该在该行之后直接记录“相机超时,重试”,但它永远不会,只是永远挂在“return 空"。
这没有任何意义,因为 CancelAfterAsync 方法已清楚地检测到挂起并且 returned false,但随后挂起的是父方法。
我怎样才能安全地检测到挂起并重试?
如前所述,它很少发生,在调用此方法数百次后每隔几个小时发生一次,所以我只想能够检测到它发生并重试而不锁定所有内容。
编辑:正如评论中所建议的那样,我尝试了 运行 在 Task.Run 中执行流氓任务,并从我的程序中删除了所有异步,如下所示:
public static class MemoryCapture
{
private static volatile bool _camProcessing = false;
public static byte[] CaptureRawData()
{
MMALCamera cam = MMALCamera.Instance;
MMALCameraConfig.Debug = true;
MMALCameraConfig.StillEncoding = MMALEncoding.BGR24;
MMALCameraConfig.StillSubFormat = MMALEncoding.BGR24;
using (var imgCaptureHandler = new MemoryStreamCaptureHandler())
using (var renderer = new MMALNullSinkComponent())
{
cam.ConfigureCameraSettings(imgCaptureHandler);
cam.Camera.PreviewPort.ConnectTo(renderer);
// Camera warm up time
Thread.Sleep(2000);
if (WaitForCam(cam))
{
var result = imgCaptureHandler.CurrentStream.ToArray();
return result;
}
else
{
Serilog.Log.Logger.Warning($"Reached timeout, returning null...");
return null;
}
}
}
private static bool WaitForCam(MMALCamera cam)
{
_camProcessing = true;
Serilog.Log.Logger.Debug("Running cam process task");
Task.Run(() =>
{
Serilog.Log.Logger.Debug($"cam.ProcessAsync");
cam.ProcessAsync(cam.Camera.StillPort).ConfigureAwait(false).GetAwaiter().GetResult();
Serilog.Log.Logger.Debug($"cam.ProcessAsync finished");
_camProcessing = false;
});
for (var i = 0; i < 1000; i++)
{
Thread.Sleep(100);
if (!_camProcessing)
{
Serilog.Log.Logger.Debug($"cam processing finished");
return true;
}
}
Serilog.Log.Logger.Warning($"Reached timeout, camera might have locked up");
return false;
}
public static byte[] CaptureImageHelper()
{
byte[] data = null;
for (var i = 0; i < 10; i++)
{
data = CaptureRawData();
if (data != null)
{
break;
}
Serilog.Log.Logger.Warning($"Retrying...");
}
if (data == null)
{
throw new Exception("Image capture failed");
}
return data;
}
}
}
该代码的日志输出如下:
[23:02:29 DBG] 运行 凸轮处理任务
[23:02:29 警告]cam.ProcessAsync
[23:04:09 WRN] 超时,相机可能已锁定
[23:04:09 WRN] 达到超时,returning null...
然后它永远挂起。
从 CaptureRawData 中 returning 时挂起,因此它可能在处理其中一个使用时挂起。
How can I detect the hang and retry safely?
对此只有一个答案:运行 该代码在一个单独的进程中。您可以重定向 stdin/stdout 以充当“command/response”通道,如果响应时间过长,则终止整个进程并重新启动它。这是相当重量级的,但这是唯一正确取消不可取消代码的方法。
之所以需要一个单独的进程是因为您的代码需要清理 API 正在做的所有事情,而在挂起 API 的情况下,唯一的方法是让 OS 介入并进行清理。在 API 可能对硬件资源具有某种独占锁的情况下尤其如此。
CancelAfterAsync
的问题在于它只取消了任务的等待。 Camera.Capture
方法仍在进行中,可能会无限期地保持任何硬件资源打开。因此,即使您可以正常工作,也不能保证开始 second Camera.Capture
会完全有效。在单独的进程中使用 Camera.Capture
会更干净,终止该进程(让 OS 进入并清理所有内容,包括硬件资源的句柄),然后重新启动它。
It's hanging when returning from CaptureRawData, so it's possible that it's hanging while disposing one of the usings.
很有可能。由于已经有一个 Camera.Capture
运行ning(并已挂起),因此处理可能正在等待访问无限期使用的硬件资源。同样,这应该通过使用单独的进程来解决,因为 OS 将介入并在进程被终止时强制关闭这些句柄。