异步期间的线程安全问题,如何解决这个问题

Posted

技术标签:

【中文标题】异步期间的线程安全问题,如何解决这个问题【英文标题】:Thread safety issue during Async, how to get around this 【发布时间】:2014-03-30 10:38:47 【问题描述】:

我需要使用相同的 HttpClient 实例从不同的方法执行 4 个 HttpClient 请求,我认为这会导致线程安全问题。让我再解释一下。

在我正在开发的 Windows Phone 应用程序中,有一个“MainViewModel”类。这个类有一个异步方法来从网络服务器获取数据并处理响应。 Async 方法名为“LoadData”。我什至在这个类中还有 2 个异步方法(“ScheduleArrayAsync”和“CurrActivityAsync”)来帮助处理我从服务器获取的数据。

从“LoadData”方法中,我执行了 3 个 HttpClient 请求(这部分工作起来很像魅力),在处理所有这三个请求的响应时,我需要调用“ScheduleArrayAsync”方法。从那里我必须提出一个新的 HttpClient 请求(问题来了)。最后一个请求永远不会生成,即使我使用 try/catch 语句也不会生成错误代码。

让我认为这是一个线程问题的原因是,如果我将最后一个 HttpClient 请求移至“LoadData”方法,就像测试一样,它会再次起作用。

MainViewModel 类:-

public class MainViewModel : INotifyPropertyChanged

    public MainViewModel()
    
        this.Employees = new ObservableCollection<Employee>();
    

    public ObservableCollection<Employee> Employees  get; private set; 

    private JsonTextWriter jsonW;

    private string Owner;

    private RequestResponse reqPList;

    public bool IsDataLoaded
    
        get;
        private set;
    
    public async void LoadData()
    
        var baseUri = new Uri("https://uri/");

        await CookieHandler.GetCookies(baseUri); // A separate request to get some cookies

        reqPList = new RequestResponse();  // The class that handle the Httpclinet

        await reqPList.GetResponse(baseUri, pList); // First request
        XmlConvertor.ConvertToXml(reqPList.Response);
        var phoneListResponse = XmlConvertor.XmlString;

        await reqPList.GetResponse(baseUri, currActiv); // Second request
        XmlConvertor.ConvertToXml(reqPList.Response);
        var currActivResponse = XmlConvertor.XmlString;

        await reqPList.GetResponse(baseUri, sched);  // Third request
        XmlConvertor.ConvertToXml(reqPList.Response);
        var schedResponse = XmlConvertor.XmlString;

        //await reqPList.GetSlotInforPOST("154215");
        var handler = new DataHandler();
        await handler.phoneListHandler(phoneListResponse);
        await handler.CurrActivitiesHandler(currActivResponse);
        await handler.ScheduleHandler(schedResponse);
        /// Do some processing included call this line 

                    #region Current activity
                    CurrActivityAsync(item, handler.currActivitiesJSON);
                    #endregion


        this.IsDataLoaded = true;
    

    private async void CurrActivityAsync(JToken token, string jString)
    
        // Some processing
    

    private async void ScheduleArrayAsync(JToken token, string jString)
    
        try
        
            // Do some more processing and call the fourth request 
                            if (addedInfo[0].Contains("slotInfo"))
                                await reqPList.GetSlotInforPOST(addedInfo[1]);
                            else if (addedInfo[0].Contains("vacationInfo"))
                                await reqPList.GetVacationSlotInfoPOST(addedInfo[1], addedInfo[2]);

        
        catch (Exception exp)
        

            var d = exp.Message;
        
    

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(String propertyName)
    
        PropertyChangedEventHandler handler = PropertyChanged;
        if (null != handler)
        
            handler(this, new PropertyChangedEventArgs(propertyName));
        
    

下面是 RequestResponse 类:-

public class RequestResponse

    public string Response  get; private set; 

    private HttpClient client = new HttpClient(new HttpClientHandler()
    
        UseCookies = true,
        CookieContainer = CookieHandler.Cookiejar,
        AllowAutoRedirect = false,
        AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
    );

    public async Task<string> GetResponse(Uri baseuri, string uriString)
    
        if (client.BaseAddress == null)
        
            client.BaseAddress = baseuri; 
        
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/xhtml+xml"));
        var response = await client.GetAsync(baseuri + uriString);
        string webresponse = null;
        if (response.IsSuccessStatusCode)
        
            var resp = await response.Content.ReadAsByteArrayAsync();
            var encode = Encoding.GetEncoding("iso-8859-1");
            var respString = encode.GetString(resp, 0, resp.Length - 1);
            webresponse = respString;
        
        return Response = webresponse;
    

    public async Task<string> GetSlotInforPOST(string timeId)
    
        /// If the method is called from inside 'LoadData' it works.
        /// But if it is called from inside ScheduleArrayAsync, it will break at line marked with //***
        try
        
            var baseUri = new Uri("https://uri/");
            const string slotInfo = "cgi-bin/slotInfo.pl";

            if (client.BaseAddress == null)
                client.BaseAddress = baseUri;
            client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
            HttpContent content = new FormUrlEncodedContent(new[]
            
                new KeyValuePair<string,string>("timeId",timeId)
            );
            var response = await client.GetAsync(baseUri + slotInfo);  // ***
            string webresponse;
            if (response.IsSuccessStatusCode)
            
                var respo = await client.PostAsync(baseUri + slotInfo, content);
                var resp = await respo.Content.ReadAsByteArrayAsync();
                var encode = Encoding.GetEncoding("iso-8859-1");
                var respString = encode.GetString(resp, 0, resp.Length - 1);
                webresponse = respString;

            
        
        catch (Exception exp)
        
            var s = exp.Message;
        
        return Response;
    

    public async Task<string> GetVacationSlotInfoPOST(string vacationId, string date)
    
        // Do some HttpClient Request
    

我对问题的理解正确吗?如何克服它?因为我真的需要从“ScheduleArrayAsync”而不是“LoadData”发出最后一个 HttpClient 请求

编辑 只想提一下,在我努力解决问题的过程中,我在 RequestResponse 类的每个方法中都有不同的 HttpClient 实例。除了我上面提到的,当我从“LoadData”方法内部调用第四个请求时,即使它是 httpclient 的一个实例,一切都按预期工作。

【问题讨论】:

using the same HttpClient instance。你为什么这样做?你是否以某种方式依附于它?您可以创建一个新的,而不会遇到任何这些问题。 @nvoigt - 几乎是我要发布的内容:) @DreamNet 确实如此。但在几乎所有情况下,“正确结果”都胜过“性能”:) @nvoigt 因为 HttpClient 被设计为可重用,这样做有很多好处。重用HttpClient没有问题。但是,这些类中发生了很多奇怪的事情,可能会导致线程问题。 @DreamNet 您可能有 async void 作为签名。这将吞噬任何引发的异常。将其更改为异步任务。 【参考方案1】:

这可能是您在某些方法中作为签名的 async void。使用 async void 将防止调用函数捕获 throw 异常。从异步 void 方法抛出的异常只能由全局异常处理程序捕获。

【讨论】:

虽然我同意async void 的使用是问题的根源,但这并不是因为它吞下了异常。恰恰相反; async void 将非常大声地显示异常。 @StephenCleary 我的修订听起来更好吗?【参考方案2】:

感谢 Darrel,我终于修好了。

为了解决这个问题,我需要更改“CurrActivityAsync”方法的签名。

来自

private async void CurrActivityAsync(JToken token, string jString)

private void CurrActivityAsync(JToken token, string jString)

由于此方法内部没有等待,这就是为什么它不需要在签名中使用异步。糟糕的是,我在原始帖子中没有提到这一点。

这是我唯一需要做的事情,但我什至更改了我的“ScheduleArrayAsync”的签名 对异步更正确。

来自

private async void ScheduleArrayAsync(JToken token, string jString)

private async Task ScheduleArrayAsync(JToken token, string jString)

Darrel请您写下您的最后一条评论作为答案,以便您获得积分。

【讨论】:

以上是关于异步期间的线程安全问题,如何解决这个问题的主要内容,如果未能解决你的问题,请参考以下文章

多线程-安全性问题的解决

多线程-安全性问题的解决

C++ 中的异步线程安全日志记录(无互斥体)

如何将ThreadLocal传递到子线程

java 22 - 12 多线程之解决线程安全问题的实现方式1

关于使用 D-Bus 异步方法调用的 Python 中的线程安全