调用包含循环的异步方法而不阻塞用户界面的正确方式是什么?

huangapple go评论79阅读模式
英文:

What is the proper way to call an async method containing a loop without blocking the UI?

问题

以下是您提供的代码的翻译部分:

当运行以下代码时,会阻止UI更新

ProcessDocumentRange方法的作用是循环遍历一个范围,并调用BuildDocumentList方法,为循环的每次迭代添加一个文档引用到List<Document>中。

public List<Document> _queuedDocuments = new List<Document>();
public string _instID = "";
static String _dest = "c:\\test";
static Int32 _startId = 50001;
static Int32 _endId = 65001;

private async void _btnStartClick(object sender, EventArgs e)
{
    await ProcessRecords();
}

private async Task ProcessRecords()
{
    // 调用会冻结UI的方法
    await ProcessDocumentRange(_startId, _dest);
}

public async Task ProcessDocumentRange(int _startID, string _root)
{
    int year = 28;
    for (int i = _startID; i < _endId; i++) // 冻结UI的循环开始
    {
        _instID = year.ToString() + i.ToString().PadLeft(5, '0');
        await BuildDocumentList(year, i, _instID, _root);
    }
}

private async Task BuildDocumentList(int year, int image, string instID, string root)
{
    string documentIndexFile = root + year.ToString() + "\\" + instID + ".txt";

    if (!File.Exists(documentIndexFile))
        _queuedDocuments.Add(new Document { Year = year, Image = image });
}

我尝试以以下方式调用该方法,UI不会冻结,但循环会退出,并且不会构建列表:await Task.Run(() => ProcessDocumentRange(_startId, _dest));

任何帮助将不胜感激。

更新代码,现在引发异常:"调用线程必须是STA,因为许多UI组件要求如此。"
public partial class MainWindow : Window
{
    public List<Document> _queuedDocuments = new List<Document>();

    public MainWindow()
    {
        InitializeComponent();
        _tbURL.Text = _url;
        _tbDest.Text = _dest;
        _tbStartId.Text = _startId.ToString();
        _tbEndID.Text = _endId.ToString();
    }

    private async void _btnStartClick(object sender, EventArgs e)
    {
        await ProcessDocumentRange(_startId, _dest);
    }

    public async Task ProcessDocumentRange(int _startID, string _root)
    {
        // 在下面的await Task.Run中引发异常。
        // 异常可能与将项目添加到列表有关
        await Task.Run(() =>
        {
            int year = 28;
            for (int i = _startID; i < _endId; i++)
            {
                _instID = year.ToString() + i.ToString().PadLeft(5, '0');
                BuildDocumentList(year, i, _instID, _root);
            }
        });
    }

    private void BuildDocumentList(int year, int image, string instID, string root)
    {
        string documentIndexFile = root + year.ToString() + "\\" + instID + ".txt";

        if (!File.Exists(documentIndexFile))
            _queuedDocuments.Add(new Document { Year = year, Image = image });
    }
}

// Document类
public class Document : MainWindow
{
    private int _year = -1;
    public int Year
    {
        get
        {
            return _year;
        }
        set
        {
            _year = value;
        }
    }

    private int _image = -1;
    public int Image
    {
        get
        {
            return _image;
        }
        set
        {
            _image = value;
        }
    }
}

请注意,这只是提供给您的代码的翻译部分,不包括问题的回答或其他内容。如果您需要进一步的帮助或解释,请告诉我。

英文:

I have the below code when run blocks the UI from updating.

The ProcessDocumentRange method’s job is to loop through a range and call the BuildDocumentList method to add a document reference to List<Document> for each iteration of the loop.

 public List&lt;Document&gt; _queuedDocuments = new List&lt;Document&gt;();
public string _instID = &quot;&quot;;        
static String _dest = &quot;c:\\test&quot;;
static Int32 _startId = 50001;
static Int32 _endId = 65001;
private async void _btnStartClick(object sender, EventArgs e)
{
await ProcessRecords();
}
private async Task ProcessRecords()
{
// Call to Method which freezes UI
await ProcessDocumentRange(_startId, _dest);
}
public async Task ProcessDocumentRange(int _startID, string _root)
{
int year = 28;
for (int i = _startID; i &lt; _endId; i++) //Start of loop that freezes UI
{
_instID = year.ToString() + i.ToString().PadLeft(5, &#39;0&#39;);
await BuildDocumentList(year, i, _instID, _root);
}
}
private async Task BuildDocumentList(int year, int image, string instID, string root)
{
string documentIndexFile = root + year.ToString() + &quot;\\&quot; + instID + &quot;.txt&quot;;
if (!File.Exists(documentIndexFile))
_queuedDocuments.Add(new Document { Year = year, Image = image });
}

I have tried calling the Method like the following and the UI doesn't freeze, but the loop exits and never builds the list: await Task.Run(() =&gt; ProcessDocumentRange(_startId, _dest));

Any help would b greatly appreciated.

Update to code that now throws Exception: "The calling thread must be STA, because many UI components require this."

    public partial class MainWindow : Window
    {
        public List&lt;Document&gt; _queuedDocuments = new List&lt;Document&gt;();
        public MainWindow()
        {
            InitializeComponent();
            _tbURL.Text = _url;
            _tbDest.Text = _dest;
            _tbStartId.Text = _startId.ToString();
            _tbEndID.Text = _endId.ToString();
        }
        private async void _btnStartClick(object sender, EventArgs e)
        {
            await ProcessDocumentRange(_startId, _dest);
        }
        public async Task ProcessDocumentRange(int _startID, string _root)
        {
            //Exception thrown at await Task.Run below.
            //Exception may have something to do with adding an item to the list
            await Task.Run(() =&gt;
            {
                int year = 28;
                for (int i = _startID; i &lt; _endId; i++)
                {
                    _instID = year.ToString() + i.ToString().PadLeft(5, &#39;0&#39;);
                    BuildDocumentList(year, i, _instID, _root);

                }
            });
        }
        private void BuildDocumentList(int year, int image, string instID, string root)
        {
            string documentIndexFile = root + year.ToString() + &quot;\\&quot; + instID + &quot;.txt&quot;;

            if (!File.Exists(documentIndexFile))
                _queuedDocuments.Add(new Document { Year = year, Image = image });
        }
//Document Class
    public class Document : MainWindow
    {        
        private int _year = -1;
        public int Year
        {
            get
            {
                return _year;
            }
            set
            {
                _year = value;
            }
        }


        private int _image = -1;
        public int Image
        {
            get
            {
                return _image;
            }
            set
            {
                _image = value;
            }
        }
   }

答案1

得分: 1

以下是您要翻译的代码部分:

Your methods are executing synchronously because none of them returns an actual `Task`. But most importantly, you failed to reach your goal because you did not off-load the CPU intensive work to relieve the UI thread. It's the loop that still exhausts the UI thread and therefore freezes the UI.
To move it to a different thread, a background thread, you must call `Task.Run`:

private async void _btnStartClick(object sender, EventArgs e)
{
  // 调用不再冻结UI的方法。
  // 使用await异步等待方法返回
  await ProcessDocumentRangeAsync(_startId, _dest);

  // TODO::对已填充的列表执行某些操作
}

public async Task ProcessDocumentRangeAsync(int startID, string root)
{
  // 在后台线程上执行循环
  await Task.Run(() =>
  {
    int year = 28;
    for (int i = startID; i < _endId; i++)
    {
      _instID = year.ToString() + i.ToString().PadLeft(5, '0');
      BuildDocumentList(year, i, _instID, root);
    }
  });
}

private void BuildDocumentList(int year, int image, string instID, string root)
{
  string documentIndexFile = root + year.ToString() + "\\" + instID + ".txt";
  if (File.Exists(documentIndexFile))
  {
    return;
  }

  // 因为Document扩展自MainWindow,
  // 我们必须在原始的Dispatcher线程(UI线程)上创建实例。
  // 这是因为MainWindow继承自Window的DispatcherObject。
  // DispatcherObject实例具有线程亲和性(Dispatcher亲和性)。
  // 在不是关联的Dispatcher线程上创建它们或从它们访问它们
  // 会引发跨线程异常(InvalidOperationException)。
  // 为了解决这个问题,我们必须使用Dispatcher在Dispatcher线程上执行实例化。
  // 我们使用DispatcherPriority.Background来防止循环耗尽UI线程。
  // 如果我们可以摆脱Dispatcher.Invoke,我们可以提高性能,
  // 比如避免将Document设置为窗口。
  // 而是应该是一个简单的数据模型(而不是控件)。
  this.Dispatcher.InvokeAsync(
    () => CreateDocument(year, image), 
    DispatcherPriority.Background);
}

private void CreateDocument(int year, int image)
{
  var newDocument = new Document { Year = year, Image = image };
  _queuedDocuments.Add(newDocument);
}

如果您需要进一步的翻译或有其他问题,请告诉我。

英文:

Your methods are executing synchronously because non of them returns an actual Task. But most important, you failed to reach your goal because you did not off-load the CPU intensive work to relief the UI thread. It's the loop that still exhausts the UI thread and therefore freezes the UI.
To move it to a different thread, a background thread, you must call Task.Run:

private async void _btnStartClick(object sender, EventArgs e)
{
  // Call to Method which no longer freezes the UI.
  // Use await to wait asynchronously for the method to return
  await ProcessDocumentRangeAsync(_startId, _dest);

  // TODO::Do something with the populated list
}

public async Task ProcessDocumentRangeAsync(int startID, string root)
{
  // Execute loop on a background thread
  await Task.Run(() =&gt;
  {
    int year = 28;
    for (int i = startID; i &lt; _endId; i++)
    {
      _instID = year.ToString() + i.ToString().PadLeft(5, &#39;0&#39;);
      BuildDocumentList(year, i, _instID, root);
    }
  });
}

private BuildDocumentList(int year, int image, string instID, string root)
{
  string documentIndexFile = root + year.ToString() + &quot;\\&quot; + instID + &quot;.txt&quot;;
  if (File.Exists(documentIndexFile))
  {
    return;
  }

  // Because Document extends MainWindow, 
  // we must create the instance on the original Dispatcher thread (UI thread).
  // This is required because MainWindow inherits the DispatcherObject from Window.  
  // DispatcherObject instances have thread affinity (Dispatcher affinity).
  // Creating them on or accessing them from a thread that is not the associated Dispatcher thread
  // will throw a cross-thread exception (InvalidOperationException).
  // To solve this, we must execute the instantiation on the Dispatcher thread using the Dispatcher.
  // We use DispatcherPriority.Background to prevent the loop from exhausting the UI thread.
  // We could improve the performance if we could get rid of the Dispatcher.Invoke,
  // for example by avoiding making Document a Window. 
  // Instead it should be a simple data model (and not a control).
  this.Dispatcher.InvokeAsync(
    () =&gt; CreateDocument(year, image), 
    DispatcherPriority.Background);
}

private void CreateDocument(int year, int image)
{
  var newDocument = new Document { Year = year, Image = image };
  _queuedDocuments.Add(newDocument);
}

If you don't have anything to do after the list is populated, you don't have to use await. Simply create the background thread and return to the UI thread and continue:

private async void _btnStartClick(object sender, EventArgs e)
{
  // Call to Method which no longer freezes the UI.
  // Because of we don&#39;t await anything here 
  // the method is a pure concurrent method and we can 
  // immediately continue to do work on the current thread.
  ProcessDocumentRange(_startId, _dest);

  // TODO::The list is not populated at this point. 
  // The operation is still executing in parallel-
}

public void ProcessDocumentRange(int startID, string root)
{
  // Execute loop concurrently on a background thread 
  // and return immediately to the caller (click handler) 
  Task.Run(() =&gt;
  {
    int year = 28;
    for (int i = startID; i &lt; _endId; i++)
    {
      _instID = year.ToString() + i.ToString().PadLeft(5, &#39;0&#39;);
      BuildDocumentList(year, i, _instID, root);
    }
  });
}

private BuildDocumentList(int year, int image, string instID, string root)
{
  string documentIndexFile = root + year.ToString() + &quot;\\&quot; + instID + &quot;.txt&quot;;
  if (File.Exists(documentIndexFile))
  {
    return;
  }

  // Because Document extends MainWindow, 
  // we must create the instance on the original Dispatcher thread (UI thread).
  // This is required because MainWindow inherits the DispatcherObject from Window.  
  // DispatcherObject instances have thread affinity (Dispatcher affinity).
  // Creating them on or accessing them from a thread that is not the associated Dispatcher thread
  // will throw a cross-thread exception (InvalidOperationException).
  // To solve this, we must execute the instantiation on the Dispatcher thread using the Dispatcher.
  // We use DispatcherPriority.Background to prevent the loop from exhausting the UI thread.
  // We could improve the performance if we could get rid of the Dispatcher.Invoke,
  // for example by avoiding making Document a Window. 
  // Instead it should be a simple data model (and not a control).
  this.Dispatcher.InvokeAsync(
    () =&gt; CreateDocument(year, image), 
    DispatcherPriority.Background);
}

private void CreateDocument(int year, int image)
{
  var newDocument = new Document { Year = year, Image = image };
  _queuedDocuments.Add(newDocument);
}

huangapple
  • 本文由 发表于 2023年5月8日 02:42:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76195697.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定