C# 将多个 Web 请求异步发送到 DataTable - 工作似乎因未知原因停止

C# Async Multiple Web Requests to DataTable - Work appears to stop for unknown reasons

我正在开发一个单一表单应用程序,按下按钮时,它会进行一个简单的库存数据库 API 查询,并检查每个 returned ItemID# 是否有相应的图像,这可能会也可能不会存在于由 ID# 组成的 URL 处。我目前正在通过为每个 URL、return 发送 HttpWebRequest.Method = "HEAD" 请求来执行此操作,除非触发了 catch 块。

数据库查询可能 return 50 - 150 个部件号,以这种方式向每个单独发送 HEAD 请求大约需要 5 分钟,而且效率不高。

我正在尝试使用 async 和 await 对这个过程进行多任务处理。当我单击该按钮时,它工作正常,以大约每秒 2 次的速度将行一一异步加载到我的 DataGridView 中(这还不错,但如果可能的话我仍然想加快速度)。

然而:在找到 2 个成功的 URL 响应后,它停止加载行并且似乎只是放弃,原因我不知道???重新启用 UI 的 syncContext 块永远不会执行,因为工作永远不会完成。任何人都可以看到可能导致这种情况发生的原因吗?

我一直在根据这个文档松散地工作:

"How to: Make Multiple Web Requests in Parallel by Using async and await (C#)" https://msdn.microsoft.com/en-us/library/mt674880.aspx

namespace ImageTableTest
{
public partial class ImageTableTestForm : Form
{
    //P21 Authentication Variables
    private static Token P21token = null;
    private static RestClientSecurity rcs;

    //Create Tables and bindingSource
    DataTable itemDataIMG = new DataTable();
    DataTable itemDataNOIMG = new DataTable();
    DataTable itemDataComplete = new DataTable();
    BindingSource bindingSource = new BindingSource();

    private readonly SynchronizationContext synchronizationContext;


    public ImageTableTestForm()
    {
        InitializeComponent();

        //Create syncContexct on UI thread for updating UI
        synchronizationContext = SynchronizationContext.Current;

        //authenticate database API function
        authenticateP21();     

        //Designing DataTables
        itemDataIMG.Columns.Add("MPN#", typeof(string));
        itemDataIMG.Columns.Add("IMG", typeof(bool));
        itemDataIMG.Columns[1].ReadOnly = true;

        itemDataNOIMG.Columns.Add("MPN#", typeof(string));
        itemDataNOIMG.Columns.Add("IMG", typeof(bool));
        itemDataNOIMG.Columns[1].ReadOnly = true;

        itemDataComplete.Columns.Add("MPN#", typeof(string));
        itemDataComplete.Columns.Add("IMG", typeof(bool));
        itemDataComplete.Columns[1].ReadOnly = true; 

        //bind to DataGridView itemView
        bindingSource.DataSource = itemDataComplete;          
        itemView.DataSource = bindingSource;
        itemView.AutoGenerateColumns = false;
    }



    private async void testBtn_Click(object sender, EventArgs e)
    {
        //When button is clicked, disable UI and
        //start background work:
        testBtn.Enabled = false;
        loadSpinner.Visible = true;

        await Task.Run(() =>
        {
            getItemView();
        });
    }


    private async void getItemView()
    {
        try
        {
            //This executes the query and returns an array of Part objects:
            PartResourceClient prc = new PartResourceClient(ConfigurationManager.AppSettings["P21.BaseURI"], rcs);
            prc.QueryFilter("add_to_ebay eq 'Y'");
            Part[] pResults = prc.Resource.GetParts();              

            int numParts = pResults.Length;                
            Task<bool>[] taskArray = new Task<bool>[numParts];
            bool[] IMGboolArray = new bool[numParts];

            //For each part, create CheckImageURL task and add to task Array
            //Then Await execution
            for (int i = 0; i < numParts; i++)
            {
                taskArray[i] = CheckImageURL(pResults[i].ItemId);
                IMGboolArray[i] = await taskArray[i];
            }                
        }
        catch (Exception e)
        {
            MessageBox.Show(e.ToString());
        }

        //When all Tasks finish, remove loadSpinner, re-enable UI
        //(This never executes for unknown reasons.)
        synchronizationContext.Post(new SendOrPostCallback(o =>
        {              
            loadSpinner.Visible = false;
            testBtn.Enabled = true;
        }), null);

        MessageBox.Show("<DONE>");
    }


    async Task<bool> CheckImageURL(string MPN)
    {
        //Here I am forming and executing the web HEAD request,
        //If there is there is a 'NOT FOUND' response it goes to 'catch' block:
        string URL = "https://s3-us-west-2.amazonaws.com/www.crosscreektractor.com/ebay-images/" + MPN + "_e.png";
        HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(URL);
        request.Method = "HEAD";
        try
        {
            await request.GetResponseAsync();
            synchronizationContext.Post(new SendOrPostCallback(o =>
            {
                addDataRows(MPN, true);
            }), null);

            return true;
        }
        catch
        {
            synchronizationContext.Post(new SendOrPostCallback(o =>
            {
                addDataRows(MPN, false);
            }), null);

            return false;
        }
    }

    private void addDataRows(string MPN, bool IMG)
    {
        //Add data to respective table:
        if (IMG)
        {
            itemDataIMG.Rows.Add(MPN, IMG);
        }
        else
        {
            itemDataNOIMG.Rows.Add(MPN, IMG);
        }

        //Here I am sorting the IMG and NOIMG tables,
        //then merging them into the Complete table which
        //The DataGridView is bound to, so that IMG entries are on top:
        itemDataIMG.DefaultView.Sort = ("MPN# DESC");
        itemDataNOIMG.DefaultView.Sort = ("MPN# DESC");

        itemDataComplete.Clear();
        itemDataComplete.Merge(itemDataIMG);
        itemDataComplete.Merge(itemDataNOIMG);
        itemView.Refresh();
    }

getItemView() 方法更改为 Task 返回,如下所示:

private async Task getItemView()

然后不使用 Task.Run 而是简单地 await 在点击事件处理程序中这样调用:

await getItemView();

感谢您对我的 TAP 模式提出的建议,肯定学到了很多关于 TAP 的知识。

我的问题的解决方案是 HttpWebRequest 连接限制。对于成功的请求,它不会自动关闭连接,您必须通过获取 WebResponse 并关闭它(仅在成功连接时需要)来这样做:

WebResponse response = await request.GetResponseAsync();
{do stuff}
response.Close();