【问题标题】:Async Events Causing Deadlock When Synchronization is Needed需要同步时导致死锁的异步事件
【发布时间】:2014-08-05 22:27:23
【问题描述】:

我创建了一个类来使用 REST API。我编写了这个类来与 Web 服务进行异步通信,因为我最初并不认为我需要让任何东西同步运行。现在我遇到了一种情况,我意识到使用异步方法对于我的应用程序中的一种特定情况并不理想,因为它运行不正常并导致异常,因为应用程序试图调用一个它还没有准备好的方法。我不是 100% 确定为什么会发生这种情况,但我认为这是由于在我的 UI 中的 async void 事件中调用了这些方法。下面是一些代码 sn-ps,显示了这种情况的示例:

class MyForm : Form
{
     private RestConnection connection;         
     private async void MyForm_Load(object sender, EventArgs e)
     {
          if(connection == null)
          using (LogOnDialog logOnDialog = new LogOnDialog())
          {
              var result = logOnDialog.ShowDialog(this);
              if(result == DialogResult.OK)
              {
                  connection = logOnDialog.Connection;
               }
           }

           formComboBox.DataSource = await connection.GetChoices();
     }
}

class LogOnDialog : Form
{
    public RestConnection Connection {private set;get;}
    private async void saveButton_Click(object sender, EventArgs e)
    {
          RestConnection conn = new RestConnection(userNameTB.Text, passwordTb.Text);
          await conn.LogIn();
          if(conn.LoggedIn) //issue here
          {
                Connection = conn;
                DialogResult = DialogResult.OK;
                this.Close();
           }
          else
          {
              Connection = null;
              DialogResult = DialogResult.Abort;
              MessageBox.Show("Invalid Credentials, Try Again.");
           }
      }
}

发生的情况是应用程序正在尝试调用 connection.GetOptions(),但连接仍然为空,因为 LogOnDialog 的异步事件创建了连接并在允许向调用者提供连接之前检查是否成功登录。但是,由于 Click 事件尚未完成,因此连接为 null,因此调用了 NullReferenceException。此外,如果我继续过去并忽略异常,则会引发 ObjectDisposedException,因为我们现在位于 using 块之外。

我试图通过从事件中删除 async 关键字并在 login 方法上调用 Wait() 来强制同步登录。这导致了僵局。我还尝试使用以下代码捕获任务,并为它旋转等待:

Task t = conn.LogOn();
while(!t.IsCompleted)
    Thread.Sleep(50);

这并没有死锁,但它确实永远旋转。每次我在 While 条件下检查断点时,任务的状态总是 WAITINGFORACTIVATION 并且基本上锁定了应用程序。为了让它工作,我将为这种情况创建一些同步方法,但是什么能让它正常工作并一直保持异步?

编辑:LogOn() 和 GetOptions() 请求的附加代码片段

class RestConnection
{
      private string user;
      private string password

     private XDocument convertToXDoc(string functionName, IDictionary<string,string> parameters) {} //not shown, but this just creates an XML document in the required format for the REST service to consume.
     private async Task<XDocument> SendCommand(XDocument commandDocument)
     {
          XDocument responseData = null;
          byte[] data = Encoding.UTF8.GetBytes(commandDoc.ToString());

        HttpWebRequest request = WebRequest.CreateHttp(this.serverUrl);
        request.Method = "POST";
        request.ContentType = "text/xml";
        request.ContentLength = data.Length;
        using (var requestStream = await request.GetRequestStreamAsync())
        {
            await requestStream.WriteAsync(data, 0, data.Length);
        }

            HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse;

            using (var responseStream = response.GetResponseStream())
            {
                responseData = XDocument.Load(responseStream);
            }

           return responseData;
    }

   public async Task LogIn()
    {
        var parameters = new Dictionary<string, string>();
        parameters.Add("USERNAME", userName);
        parameters.Add("PASSWORD", passWord);
        parameters.Add("CORELICTYPE", String.Empty);
        parameters.Add("REMOTEAUTH", "False");

        var xmlCommand = ConvertMethodToXml("LoginUserEx3", parameters);

        var response = await SendCommand(xmlCommand);

        //read response
            switch (response.Root.Element("RESULTS").Element("RESULTVAL").Value)
            {
                case "0":
                    sessionId = response.Root.Element("SESSIONID").Value;
                    pingRequired = response.Root.Element("PINGTIME").Value != "0";
                    if (pingRequired)
                    {
                        pingInterval = int.Parse(response.Root.Element("PINGTIME").Value);
                        pingTimer = new Timer(pingInterval);
                        pingTimer.Elapsed += PingServerRequired;
                        pingTimer.Start();
                    }
                    loggedIn = true;
                    break;
                //removed other cases for example since they all throw exceptions
                default:
                    loggedIn = false;
                    throw new ConnectionException("Error");
            }
    }
 }

GetOptions() 与 LogIn() 方法的格式相同,但它通过解析返回的 XDocument 返回 Task&lt;List&lt;Options&gt;&gt;

【问题讨论】:

  • 您可能应该在Resconnection.LogInResconnection.GetChoices 中包含代码。
  • @DanielKelley 当然,等一下。
  • @JNYRanger:在下面查看我的更新。如果您不想关闭表单,则不应使用 DialogResult = DialogResult.Abort;

标签: c# user-interface asynchronous async-await deadlock


【解决方案1】:

问题出在这里:

{
  Connection = null;
  DialogResult = DialogResult.Abort; //<<------ this
  MessageBox.Show("Invalid Credentials, Try Again.");
}

分配DialogResult自动关闭您的表单并返回您传入的结果。删除该行就可以了(尤其是如果您希望对话框永远不会关闭)。

【讨论】:

  • 问题是如果登录失败,我不想关闭对话框。我希望用户了解登录失败的原因,并允许他们重试。
  • @JNYRanger:好的,所以他们无法在没有实际登录的情况下退出 LogOnDialog。没问题,代码的工作方式相同,尽管您可以删除 if(connection != null)。 LogOnDialog 的Closed 事件在saveButton_Click 完成后调用,异步和一切。
  • 啊...我从来不知道分配 DialogResult 会关闭模态表单!实际上,我后来添加了它以防止它继续,因为它在按钮的 Click 事件完成之前以某种方式结束在 MyForm 中的 result == DialogResult.OK 块中,我设置了它!但是,我确实将该按钮分配为表单的 AcceptButton,所以这可能是问题的一部分?
  • @JNYRanger:不,更改DialogResult 会以任何方式关闭带有该结果的表单。
  • 感谢您的建议!我会做一些测试,然后希望之后能接受你的回答!
猜你喜欢
  • 2021-09-18
  • 2021-05-15
  • 1970-01-01
  • 2014-09-12
  • 2018-05-07
  • 1970-01-01
相关资源
最近更新 更多