【问题标题】:Can this "Synchronous" Code Cause Deadlocks?这个“同步”代码会导致死锁吗?
【发布时间】:2020-01-07 07:31:46
【问题描述】:

我的公司有一个他们编写的 Nuget 包,可以轻松地为您完成各种常见任务。其中之一是发出 HTTP 请求。通常我总是使我的 HTTP 请求异步,但是在这个 Nuget 包中是以下代码:

    protected T GetRequest<T>(string requestUri)
    {
        // Call the async method within a task to run it synchronously
        return Task.Run(() => GetRequestAsync<T>(requestUri)).Result;
    }

调用这个函数:

    protected async Task<T> GetRequestAsync<T>(string requestUri)
    {
        // Set up the uri and the client
        string uri = ParseUri(requestUri);
        var client = ConfigureClient();

        // Call the web api
        var response = await client.GetAsync(uri);

        // Process the response
        return await ProcessResponse<T>(response);
    }

我的问题是,这段代码真的是通过将 GetRequestAsync(requestUri) 包装在 Task.Run 中并在返回的任务上调用 .Result 来实现同步运行的吗?这似乎是一个等待发生的死锁,我们在应用程序的某些区域看到了问题,这些问题在以较高负载运行时使用此功能。

【问题讨论】:

  • Task 上调用.Result 将导致它同步等待直到完成。
  • 每个人都在说不要使用那个库,或者至少不要使用那个方法。另一个应该按照礼仪使用ConfigureAwait(false)。发出 HTTP 请求并不难,如果必须正确,请自行完成。

标签: c# asp.net async-await


【解决方案1】:

访问Task.Result会阻塞当前线程,直到Task完成,所以它不是异步的。

至于死锁,这不应该发生,因为Task.RunGetRequestAsync 使用另一个线程,该线程没有被Result 的调用阻塞。

【讨论】:

  • 好的,这和在Task上使用.configureAwait(false)类似吗?
  • @JohanDonne 不,该方法返回T 而不是Task。当Task 完成时,T 将可用,在此之前,线程被阻塞。
  • @deruitda 它有类似的用途。 ConfigureAwait(false) 允许异步方法在阻塞情况下在另一个线程上恢复。
【解决方案2】:

不会导致死锁的原因是因为Task.Run会推动委托在线程池线程中执行。线程池线程没有 SynchronizationContext,因此不会发生死锁,因为在异步方法 GetRequestAsync 和调用者之间没有要锁定的同步上下文。就像您可以直接在实际的异步方法上以及在 Task.Run() 块中调用 .Result 一样,这也不会导致死锁。

当你冻结 1 个线程时效率非常低,即。 1 CPU 中的核心什么也不做,只是等待异步方法和其中的 I/O 调用完成。这可能就是您在高负载情况下看到冻结的原因。

如果由于捕获同步上下文和异步调用阻塞而导致同步/异步死锁问题,无论单个调用的负载如何,都会发生死锁..

【讨论】:

    【解决方案3】:

    这不会导致死锁。但这肯定是一种资源浪费,因为其中一个线程可能被阻塞。

    如果GetRequest 看起来像这样,则可能会出现死锁:

    protected T GetRequest<T>(string requestUri)
    {
        var task = GetRequestAsync<T>(requestUri);
        return task.Result;
    
        // or
        // return GetRequestAsync<T>(requestUri).Result;
    }
    

    在上面的示例中,您可以看到我在当前线程中调用了GetRequestAsync。让我们给线程一个数字 0。考虑GetRequestAsync - await client.GetAsync(uri) 中的这一行。 .GetAsync 由线程 1 执行。.GetAsync 完成后,默认任务调度程序会尝试将执行流程返回到执行该行的线程 - 到线程 0。但是执行该行 (0) 的线程是现在被阻塞,因为在我们执行GetRequestAsync() 之后,我们用task.Result 阻塞它(线程0)。因此,我们的线程 0 仍然被阻塞,因为在完成 await client.GetAsync(uri) 之后它不能继续执行 GetRequestAsync,也不能给我们结果。

    这是一个很常见的错误,当被问及僵局时,我想您指的是这个错误。您的代码没有导致它,因为您正在另一个线程中执行GetRequestAsync

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-27
      • 1970-01-01
      • 2017-11-16
      • 2020-08-20
      • 2021-11-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多