草庐IT

.NET异步编程模式(二)

wang2009 2023-03-28 原文

在 C#1 的时候就包含了APM,在 APM 模型中,异步操作通过 IAsyncResult 接口实现,包括两个方法 BeginOperationName 和 EndOperationName ,分别表示开始和结束异步操作。

Demo

我们先来看一个同步示例。新建WPF程序,在界面上放一个按钮。点击按钮访问外网,会有一定时间的阻塞。

private void SyncBtn_Click(object sender, RoutedEventArgs e)
{
    // 记录时间
    Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() + 
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);

    // 访问外网网站网站
    var req = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");
    req.GetResponse();

    // 记录时间
    Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() +
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}

当我们点击按钮后,因为web请求是同步的,会阻塞UI线程一定时间。从输出日志上看阻塞时间是 1 秒钟左右,此时界面呈卡死状态。

日志输出如下:

13:16:09.5031834,ThreadID = 1

13:16:10.5220362,ThreadID = 1

从运行效果和日志,我们可以看出:

  • WebRequest方法调用前后都是在同一个线程上执行-UI线程
  • WebReqeust方法阻塞了UI线程,导致“假死”现象

WebRequest也提供了异步方法,BeginGetResponse,EndGetResponse。我们修改一下代码,新增一个按钮。

private void APM_Btn_Click(object sender, RoutedEventArgs e)
{
    // 记录时间
    Debug.WriteLine("1-" + DateTime.Now.TimeOfDay.ToString() +
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);

    // 访问外网网站网站
    var req = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");
    req.BeginGetResponse(new AsyncCallback(t => { WebRequestCallback(t,req); }), null);

    // 记录时间
    Debug.WriteLine("3-" + DateTime.Now.TimeOfDay.ToString() +
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}

/// <summary>
/// 异步回调
/// </summary>
/// <param name="result"></param>
private void WebRequestCallback(IAsyncResult result, WebRequest request)
{
    var response = request.EndGetResponse(result);
    // 获取返回数据流
    var stream = response.GetResponseStream();

    using(StreamReader reader = new StreamReader(stream))
    {
        StringBuilder sb = new StringBuilder();
        while(!reader.EndOfStream)
        {
            var content = reader.ReadLine();
            sb.Append(content);
        }

        // 记录时间
        Debug.WriteLine("2-" + DateTime.Now.TimeOfDay.ToString() +
                        ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
    }
}

运行效果如下:

日志输出如下:

1-13:10:01.7734197,ThreadID = 1

3-13:10:01.8826176,ThreadID = 1

2-13:10:03.2614022,ThreadID = 14

从运行效果和日志,我们可以看出:

  • 异步方法不会阻塞调用方法,调用后立刻返回
  • 异步方法会在另外一个线程上执行

IAsyncResult

BeginOperationName 方法会返回一个实现了 IAsyncResult 接口的对象。该对象存储了关于异步操作的信息。

转到定义,我们可以看到接口中都包含哪些内容:

自定义异步方法

实现该接口,定义自己的异步方法。

public class MyWebRequestResult : IAsyncResult
{
    /// <summary>
    /// 用户定义属性,可以存放数据
    /// </summary>
    public object? AsyncState => throw new NotImplementedException();
    /// <summary>
    /// 获取用于等待异步操作完成的 WaitHandle
    /// </summary>
    public WaitHandle AsyncWaitHandle => throw new NotImplementedException();
    /// <summary>
    /// 表示异步操作是否是同步完成
    /// </summary>
    public bool CompletedSynchronously => throw new NotImplementedException();
    /// <summary>
    /// 表示异步操作是否完成
    /// </summary>
    public bool IsCompleted => throw new NotImplementedException();
}

我们需要新建一个回调函数:

public class MyWebRequestResult : IAsyncResult
{
    /// <summary>
    /// 异步回调函数
    /// </summary>
    private AsyncCallback _callback;
    public string Result { get; private set; }
    // 构造函数
    public MyWebRequest(AsyncCallback asyncCallback, object state)
    {
        _callback = asyncCallback;
    }
    // 设置结果
    public void SetComplete()
    {
        AsyncState = result;
        Result = result;
        if(null != _callback)
        {
            _callback(this);
        }
    }
    // ...
}

在次之后就可以自定义 APM 异步模型了:

public IAsyncResult BeginMyWebRequest(AsyncCallback callback)
{
    // 1. 先给 IAsyncResult 进行赋值
    var myResult = new MyWebRequestResult(callback, null);
    var request = WebRequest.Create("https://docs.newrelic.com/docs/apm/agents/net-agent/getting-started/net-agent-compatibility-requirements-net-framework/");
    // 2. 新建线程,执行耗时任务
    new Thread(() => {
        using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream()))
        {
            var str = sr.ReadToEnd();
            // 3. 耗时任务结束后 调用回调函数 & 保存结果
            myResult.SetComplete(str);
        }
    }).Start();

    return myResult;
}

public string EndMyWebRequest(IAsyncResult asyncResult)
{
    MyWebRequestResult myResult = asyncResult as MyWebRequestResult;
    return myResult.Result;
}

新增一个按钮,进行调用:

private void MyAPM_Btn_Click(object sender, RoutedEventArgs e)
{
    // 记录时间
    Debug.WriteLine("1-" + DateTime.Now.TimeOfDay.ToString() +
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);

    // 调用 Begin 方法
    BeginMyWebRequest(new AsyncCallback(MyAPM_Callback));

    // 记录时间
    Debug.WriteLine("3-" + DateTime.Now.TimeOfDay.ToString() +
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}

private void MyAPM_Callback(IAsyncResult result)
{
    // 从这里可以获得 异步操作的结果
    var myResult = result as MyWebRequestResult;
    var msg = EndMyWebRequest(myResult);

    // 记录时间
    Debug.WriteLine("2-" + DateTime.Now.TimeOfDay.ToString() +
                    ",ThreadID = " + Thread.CurrentThread.ManagedThreadId);
}

运行效果如下:

日志输出如下:

1-14:48:42.7278184,ThreadID = 1

3-14:48:42.7311174,ThreadID = 1

2-14:48:45.1049069,ThreadID = 6

结合效果和日志,我们可以得出如下结论:

  • 自定义的异步方法没有导致 UI 卡顿
  • APM就是把耗时的任务交给新线程去做,然后利用委托进行回调

普通方法的异步

如果是普通方法,也可以通过 委托异步(BeginInvoke, EndInvoke):

public void MyAction()
{
    var func = new Func<string, string>(t => {
        Thread.Sleep(2000);
        return t;
    });

    func.BeginInvoke("inputStr", t => {
        string result = func.EndInvoke(t);
    },null);
}

总结

  1. APM 模型是基于IAsyncResult来实现异步操作的
  2. 异步操作开始时,把委托传递给 IAsyncResult
  3. 在新线程上执行耗时操作
  4. 耗时操作结束后,修改 IAsyncResult 里的结果数据,并调用 IAsyncResult 里的委托回调
  5. 在回调里获取 异步操作 的结果

有关.NET异步编程模式(二)的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  5. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  6. ruby - 如何模拟 Net::HTTP::Post? - 2

    是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou

  7. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  8. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  9. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  10. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

随机推荐