簡體   English   中英

在后台運行一個長時間運行的並行任務,同時允許小的異步任務更新前台

[英]Running a long-running parallel task in the background, while allowing small async tasks to update the foreground

我有大約 10 000 000 個任務,每個任務需要 1-10 秒才能完成。 我在一個強大的服務器上運行這些任務,使用 50 個不同的線程,每個線程選擇第一個未完成的任務,運行它,然后重復。

偽代碼:

for i = 0 to 50:
    run a new thread:
        while True:
            task = first available task
            if no available tasks: exit thread
            run task

使用此代碼,我可以在任何給定數量的線程上並行運行所有任務。

實際上,代碼使用 C# 的 Task.WhenAll,如下所示:

ServicePointManager.DefaultConnectionLimit = threadCount; //Allow more HTTP request simultaneously
var currentIndex = -1;
var threads = new List<Task>(); //List of threads
for (int i = 0; i < threadCount; i++) //Generate the threads
{
    var wc = CreateWebClient();
    threads.Add(Task.Run(() =>
    {
        while (true) //Each thread should loop, picking the first available task, and executing it.
        {
            var index = Interlocked.Increment(ref currentIndex);
            if (index >= tasks.Count) break;
            var task = tasks[index];
            RunTask(conn, wc, task, port);
        }
    }));
}

await Task.WhenAll(threads);

這就像我想要的那樣工作,但我有一個問題:由於這段代碼需要很多時間來運行,我希望用戶看到一些進展。 進度顯示在彩色位圖(代表矩陣)中,生成也需要一些時間(幾秒鍾)。

因此,我想在后台線程上生成此可視化。 但是這個其他后台線程永遠不會執行。 我懷疑它與並行代碼使用相同的線程池,因此被排隊,並且在並行代碼實際完成之前不會執行。 (這有點太晚了。)

這是我如何生成進度可視化的示例:

private async void Refresh_Button_Clicked(object sender, RoutedEventArgs e)
{
    var bitmap = await Task.Run(() => // <<< This task is never executed!
    {
        //bla, bla, various database calls, and generating a relatively large bitmap
    });

    //Convert the bitmap into a WPF image, and update the GUI
    VisualizationImage = BitmapToImageSource(bitmap);
}

那么,我怎樣才能最好地解決這個問題呢? 我可以創建一個Task列表,其中每個Task代表我的一個任務,並使用 Parallel.Invoke 運行它們,然后選擇另一個線程池(我認為)。 但隨后我必須生成 1000 萬個Task對象,而不僅僅是 50 個Task對象,運行我要做的一系列事情。 聽起來它使用的 RAM 比必要的多得多。 對此有什么聰明的解決方案嗎?

編輯:正如 Panagiotis Kanavos 在他的評論中建議的那樣,我嘗試用 ActionBlock 替換我的一些循環邏輯,如下所示:

// Create an ActionBlock<int> that performs some work. 
var workerBlock = new ActionBlock<ZoneTask>(
t =>
{
    var wc = CreateWebClient(); //This probably generates some unnecessary overhead, but that's a problem I can solve later.
    RunTask(conn, wc, t, port);
},
// Specify a maximum degree of parallelism. 
new ExecutionDataflowBlockOptions
{
    MaxDegreeOfParallelism = threadCount
});

foreach (var t in tasks) //Note: the objects in the tasks array are not Task objects
    workerBlock.Post(t);
workerBlock.Complete();

await workerBlock.Completion;

注意:RunTask 只是使用 WebClient 執行 Web 請求,並解析結果。 里面沒有任何東西可以造成死鎖。

這似乎像舊的並行代碼一樣工作,只是它需要一兩分鍾來執行初始 foreach 循環來發布任務。 這種延遲真的值得嗎?

盡管如此,我的進度任務似乎仍然被阻止。 暫時忽略 Progress<T> 建議,因為這個簡化的代碼仍然遇到同樣的問題:

private async void Refresh_Button_Clicked(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("This happens");
    var bitmap = await Task.Run(() =>
    {
        Debug.WriteLine("This does not!");
        //Still doing some work here, so it's not optimized away.
    };

    VisualizationImage = BitmapToImageSource(bitmap);
}

因此,只要並行任務正在運行,它看起來仍然不會執行新任務。 我什至將“MaxDegreeOfParallelism”從 50 減少到 5(在 24 核服務器上)以查看 Peter Ritchie 的建議是否正確,但沒有改變。 還有其他建議嗎?

另一個編輯:

問題似乎是我用所有同時阻塞的 I/O 調用使線程池過載。 我用 HttpClient 及其異步函數替換了 WebClient,現在一切似乎都運行良好。

感謝大家的好建議! 盡管並非所有人都直接解決了問題,但我相信他們都改進了我的代碼。 :)

.NET 已經提供了一種機制來報告IProgress<T>Progress<T>實現的進度

IProgress 接口允許客戶端使用Report(T)類發布消息,而不必擔心線程。 該實現確保在適當的線程(例如 UI 線程)中處理消息。 通過使用簡單的IProgress< T>接口,后台方法與處理消息的人分離。

您可以在 4.5中的異步:在異步 API 中啟用進度和取消一文中找到更多信息。 取消和進度 API 並非特定於 TPL。 即使對於原始線程,它們也可用於簡化取消和報告。

Progress< T> 在創建它的線程上處理消息。 這可以通過在實例化類時傳遞處理委托或通過訂閱事件來完成。 從文章復制:

private async void Start_Button_Click(object sender, RoutedEventArgs e)
{
    //construct Progress<T>, passing ReportProgress as the Action<T> 
    var progressIndicator = new Progress<int>(ReportProgress);
    //call async method
    int uploads=await UploadPicturesAsync(GenerateTestImages(), progressIndicator);
}

其中ReportProgress是一種接受 int 參數的方法。 它還可以接受一個復雜的類,報告已完成的工作、消息等。

異步方法只需要使用 IProgress.Report,例如:

async Task<int> UploadPicturesAsync(List<Image> imageList, IProgress<int> progress)
{
        int totalCount = imageList.Count;
        int processCount = await Task.Run<int>(() =>
        {
            int tempCount = 0;
            foreach (var image in imageList)
            {
                //await the processing and uploading logic here
                int processed = await UploadAndProcessAsync(image);
                if (progress != null)
                {
                    progress.Report((tempCount * 100 / totalCount));
                }
                tempCount++;
            }

            return tempCount;
        });
        return processCount;
}

這將后台方法與接收和處理進度消息的人分離。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM