需要同步时导致死锁的异步事件

本文关键字:死锁 异步 事件 同步 | 更新日期: 2023-09-27 18:16:33

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

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(),但connection仍然为null,因为LogOnDialog的异步事件创建了连接,并在允许将连接提供给调用者之前检查是否成功登录。然而,由于连接是空的,因为Click事件还没有完成,所以调用了NullReferenceException。此外,如果我继续过去并忽略异常,则抛出ObjectDisposedException,因为我们现在在using块之外。

我试图通过从事件中删除async关键字并在登录方法上调用Wait()来强制登录是同步的。这导致了死锁。我还尝试使用下面的代码捕获任务,并对其进行spinwait:

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

这不会死锁,但是会永远旋转。每次我检查While条件上的断点时,Task的状态总是WAITINGFORACTIVATION,基本上锁定了应用程序。为了让它工作,我将为这种情况创建一些同步方法,但如何使其正常工作并始终保持异步呢?

编辑:LogOn()和getopoptions()的附加代码片段

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");
            }
    }
 }

与LogIn()方法格式相同的getopoptions(),除了它通过解析返回的XDocument返回Task<List<Options>>

需要同步时导致死锁的异步事件

问题就在这里:

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

分配DialogResult自动关闭您的表单与您传递的结果。去掉这一行就可以了(特别是如果你想让对话框永远不关闭的话)。