场景

  生产者和消费者队列, 生产者有多个, 消费者也有多个, 生产到消费需要异步.

下面用一个asp.netcore web-api项目来模拟

  创建两个api, 一个get(), 一个set(), get返回一个字符串, set放入一个字符串, get返回的就是set进去的字符串.

  实现如下:  

[route("api/[controller]/[action]")]
public class foocontroller : control
{
    imessagequeue _mq;
    public foocontroller(imessagequeue mq)
    {
        _mq = mq;
    }

    [httpget]
    public string get()
    {
        string str = _mq.readone<string>();
        return str;
    }

    [httpget]
    public void set(string v)
    {
        _mq.writeone(v);
    }
}

public interface imessagequeue
{
    t readone<t>();
    void writeone<t>(t value);
}

public class messagequeue: imessagequeue
{
    private object _value;

    public t readone<t>()
    {
        return (t)_value;
    }

    public void writeone<t>(t value)
    {
        _value = value;

    }
}

接着在startup中把imessagequeue给注入了.

services.addsingleton<imessagequeue, messagequeue>();

运行后, 先调用/api/foo/set/?v=xxx, 再调用/api/foo/get/

可以看到成功返回了xxx

第二步, value字段改为队列:

使set进去的值不会被下一个覆盖, get取队列最前的值

为了线程安全, 这里使用了concurrentqueue<t>

代码如下:

public class messagequeue: imessagequeue
{
    private readonly concurrentqueue<object> _queue = new concurrentqueue<object>();

    public t readone<t>()
    {
        _queue.trydequeue(out object str);
        return (t)str ;
    }

    public void writeone<t>(tvalue)
    {
        _queue.enqueue(value);
    }
}

那么此时, 只要get不断地轮询, 就可以取到set生产出来的数据了.

调用/api/foo/set/

三, 异步阻塞

再增加需求, 调换get和set的顺序,先get后set模拟异步, (我这里的demo是个web-api会有http请求超时之类的…假装不存在)我想要get调用等待有数据时才返回.

也就是说我想要在浏览器地址栏输入http://localhost:5000/api/foo/get/之后会不断地转圈直到我用set接口放入一个值

方案a: while(true), 根本无情简直无敌, 死等read() != null时break; 为防单核满转加个thread.sleep();

方案b: monitor, 一个wait()一个exit/release();

但是以上两个方案都是基于thread的, .net4.0之后伴随concurrentqueue一起来的还有个blockingcollection<t>相当好用

方案c: 修改后代码如下:

public class messagequeue : imessagequeue
{
    private readonly blockingcollection<object> _queue = new blockingcollection<object>(new concurrentqueue<object>());

    public t readone<t>()
    {
        var obj = _queue.take();
        return (t)obj;
    }

    public void writeone<t>(t value)
    {
        _queue.add(value);
    }
}

此时, 如果先get, 会阻塞等待set; 如果已经有set过数据就会直接返回队列中的数据. get不会无功而返了. 基于这个类型, 可以实现更像样的订阅模型.

扩展rpc

这里的set是生产者, get是消费者, 那如果我的这个生产者并不单纯产生数据返回void而是需要等待一个结果的呢? 此时订阅模型不够用了, 我需要一个异步的rpc .

比如有个ask请求会携带参数发起请求, 并等待, 知道另外有个地方处理了这个任务产生结果, ask结束等待返回这个结果answer. 

我可以回头继续用方案a或b, 但连.net4.0都已经过去很久了, 所以应该用更好的基于task的异步方案.

代码如下, 首先新增两个接口:

public interface imessagequeue
{
    void respond<trequest, tresponse>(func<trequest, tresponse> func);
    task<tresponse> rpc<trequest, tresponse>(trequest req);

    t readone<t>();
    void writeone<t>(t data);
}

接着定义一个特殊的任务类:

public class rpctask<trequest, tresponse>
{
    public taskcompletionsource<tresponse> tcs { get; set; }
    public trequest request { get; set; }
}

实现刚才新加的两个接口:

public task<tresponse> rpc<trequest, tresponse>(trequest req)
{
    taskcompletionsource<tresponse> tcs = new taskcompletionsource<tresponse>();
    _queue.add(new rpctask<trequest, tresponse> { request = req, tcs = tcs});
    return tcs.task;
}

public void respond<trequest, tresponse>(func<trequest, tresponse> func)
{
    var obj = _queue.take();
    if(obj is rpctask<trequest, tresponse> t)
    {
        var response = func(t.request);
        t.tcs.setresult(response);
    }
}

同样的, 写两个web api接口, 一个请求等待结果 一个负责处理工作

[httpget]
public async task<string> ask(string v)
{
    var response = await _mq.rpc<myrequest, myresponse>(new myrequest { id = v });
    return $"[{response.donetime}] {response.id}";
}

[httpget]
public void answer()
{
    _mq.respond<myrequest, myresponse>((req)=> new myresponse { id = req.id, donetime = datetime.now });
}

上面还随便写了两个class作为请求和返回

public class myrequest
{
    public string id { get; set; }
}
public class myresponse
{
    public string id { get; set; }
    public datetime donetime { get; set; }
}

测试一下, 用浏览器或postman打开三个选项卡, 各发起一个ask接口的请求, 参数v分别为1 2 3, 三个选项卡都开始转圈等待

然后再打开一个选项卡访问answer接口, 处理刚才放进队列的任务, 发起一次之前的三个选项卡之中就有一个停止等待并显示返回数据. 需求实现.

这里用到的关键类型是taskcompletionsource<t>.

再扩展

如果是个分布式系统, 请求和处理逻辑不是在一个程序里呢? 那么这个队列可能也是一个单独的服务. 此时就要再加个返回队列了, 给队列中传输的每一个任务打上id, 返回队列中取出返回之后再找到id对于的tcs.setresult()