64

I am confused about the difference between sending items through Post() or SendAsync(). My understanding is that in all cases once an item reached the input buffer of a data block, control is returned to the calling context, correct? Then why would I ever need SendAsync? If my assumption is incorrect then I wonder, on the contrary, why anyone would ever use Post() if the whole idea of using data blocks is to establish a concurrent and async environment.

I understand of course the difference technically in that Post() returns a bool whereas SendAsync returns an awaitable Task of bool. But what implications does that have? When would the return of a bool (which I understand is a confirmation whether the item was placed in the queue of the data block or not) ever be delayed? I understand the general idea of the async/await concurrency framework but here it does not make a whole lot sense because other than a bool the results of whatever is done to the passed-in item is never returned to the caller but instead placed in an "out-queue" and either forwarded to linked data blocks or discarded.

And is there any performance difference between the two methods when sending items?

Leeor
  • 19,260
  • 5
  • 56
  • 87
Matt
  • 7,004
  • 11
  • 71
  • 117
  • A relevant quote from [this blog](https://devblogs.microsoft.com/premier-developer/dissecting-the-actionblock-a-short-story-about-a-nasty-deadlock/): *1) The client of an action block may provide a queue size (in the constructor). 2) When a queue is full the Post method returns false and SendAsync method “blocks” until the queue will get a free spot.* – noseratio Jun 28 '20 at 21:55

2 Answers2

72

To see the difference, you need a situation where blocks will postpone their messages. In this case, Post will return false immediately, whereas SendAsync will return a Task that will be completed when the block decides what to do with the message. The Task will have a true result if the message is accepted, and a false result if not.

One example of a postponing situation is a non-greedy join. A simpler example is when you set BoundedCapacity:

[TestMethod]
public void Post_WhenNotFull_ReturnsTrue()
{
    var block = new BufferBlock<int>(new DataflowBlockOptions {BoundedCapacity = 1});

    var result = block.Post(13);

    Assert.IsTrue(result);
}

[TestMethod]
public void Post_WhenFull_ReturnsFalse()
{
    var block = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 1 });
    block.Post(13);

    var result = block.Post(13);

    Assert.IsFalse(result);
}

[TestMethod]
public void SendAsync_WhenNotFull_ReturnsCompleteTask()
{
    // This is an implementation detail; technically, SendAsync could return a task that would complete "quickly" instead of already being completed.
    var block = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 1 });

    var result = block.SendAsync(13);

    Assert.IsTrue(result.IsCompleted);
}

[TestMethod]
public void SendAsync_WhenFull_ReturnsIncompleteTask()
{
    var block = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 1 });
    block.Post(13);

    var result = block.SendAsync(13);

    Assert.IsFalse(result.IsCompleted);
}

[TestMethod]
public async Task SendAsync_BecomesNotFull_CompletesTaskWithTrueResult()
{
    var block = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 1 });
    block.Post(13);
    var task = block.SendAsync(13);

    block.Receive();

    var result = await task;
    Assert.IsTrue(result);
}

[TestMethod]
public async Task SendAsync_BecomesDecliningPermanently_CompletesTaskWithFalseResult()
{
    var block = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 1 });
    block.Post(13);
    var task = block.SendAsync(13);

    block.Complete();

    var result = await task;
    Assert.IsFalse(result);
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • ok but given your explanation, then what is the logic behind `Task`? If it cannot submit immediately due to postponement but the task completes later whats the difference between the bool being true and false? – Matt Nov 28 '12 at 13:36
  • 1
    The block may eventually decide to decline that message (e.g., if you `Complete` the block), in which case the result of the task will be `false`. See updated answer. – Stephen Cleary Nov 28 '12 at 13:40
  • great, this now makes complete sense, that possibility completely slipped my mind. Thanks a lot. – Matt Nov 28 '12 at 13:51
  • Sorry but last question that just popped up: So, if the data block decides, after postponement that it will further accept messages, I take it that SendAsync will return Task being true? This would imply that the message is kept somewhere, correct? Is it kept in the Task which was spun off? If that is the case then I also have an explanation of why sometimes SendAsync consumes a lot more memory, something I observed. It seems that Post() will either deliver (returning true) or discard (returning false) if my understanding is correct... – Matt Nov 28 '12 at 13:55
  • 3
    Yes, Yes, and Not quite. If the block postpones the message, `SendAsync` will construct a "message holder" that will hold the message until the block receives or rejects it. The `Task` is actually a part of the message holder. Also, when dealing with postponement, the target block has a data structure to keep track of its postponed message sources (which would include the "message holder"). – Stephen Cleary Nov 28 '12 at 14:04
  • Understand, thanks for explaining but what is your take on my last sentence re Post()? Will Post return false immediately if the acceptance is postponed and the message is lost, or will a "message holder" be created as well? If yes there is no task in which to hold that message, where would it be? Thanks – Matt Nov 28 '12 at 14:07
  • 3
    `Post` returns immediately; so it will return `false` if the block would postpone. There's no "message holder" for `Post`. There's a great document [Guide to Implementing Custom TPL Dataflow Blocks](http://download.microsoft.com/download/1/6/1/1615555D-287C-4159-8491-8E5644C43CBA/Guide%20to%20Implementing%20Custom%20TPL%20Dataflow%20Blocks.pdf) that goes more in-depth on how Dataflow blocks actually work. – Stephen Cleary Nov 28 '12 at 14:11
  • Great, thanks, so you are basically saying the message within Post() will be lost if the method returns false? – Matt Nov 28 '12 at 14:14
  • 1
    Correct. So you need to check for `Post` returning `false` and do something else with the message if you don't want to drop it. – Stephen Cleary Nov 28 '12 at 14:33
  • Thanks a lot for your patience. Would up vote multiple times if I could – Matt Nov 28 '12 at 14:39
  • 6
    So does this mean everybody should *always* use SendAsync unless they are ok with getting messages dropped? – Shital Shah Oct 25 '14 at 01:43
  • 1
    Re "Post returns immediately; so it will return false if the block would postpone." Are you sure? If true, then either the architectural design or the implementation is fundamentally broken. Part of the beauty of dataflow in previous implementations such as LabView, is that you simply wire blocks together and it works - the buffering of inter-block data is handled for you. A correct implementation of `Post` would not return until it could determine whether the input could be handled or not. Rejection of input should *only* occur under severe circumstances or improper data. – ToolmakerSteve Apr 15 '18 at 06:29
  • @ToolmakerSteve The 'severe circumstances' in S.Cleary's example is that a). the receiving block is set to accept only one message at a time b) there are no other blocks to accept the rejected message (aka a round-robin block design) and c) in one of the examples the block is 'completed' meaning no more input should be accepted ever. But without all those circumstances (usually default configuration) the receiving block will buffer incoming messages or, as S.Clearly explanined, there is a 'postponed-handling' infrastructure there as well. – mdisibio Mar 13 '21 at 23:08
  • @mdisibio - "the receiving block will buffer incoming messages or, as S.Clearly explanined, there is a 'postponed-handling' infrastructure there as well." Exactly. So the Post doesn't need to return "False" in that case, as the block is capable of buffering it (assuming there is room). That's why I was questioning "return false if the block would postpone". Seems to me the block doesn't "postpone"; it "declines the item" (by returning false). Reading elsewhere, maybe this is "postponing the accept/decline decision" - but I don't see how caller distinguishes "reject" from "postponed decision". – ToolmakerSteve Mar 15 '21 at 01:51
  • @ToolmakerSteve I struggle to understand the nuances of TPL Dataflow as well and am no expert, but there are circumstances in which the receiving block can first say _'not now maybe later'_, so the posting block buffers the message(s) and waits...but at any time the receiving block can issue a _'not now and not ever'_ signal that it is closed to new messages, and that can happen if the block encounters a debilitating exception, or the outer control issues a pre-emptive `Complete()` to the receiver block. (Normally it would issue `Complete()` to the head block and let it cascade thru.) – mdisibio Mar 16 '21 at 02:12
22

The documentation makes this reasonably clear, IMO. In particular, for Post:

This method will return once the target block has decided to accept or decline the item, but unless otherwise dictated by special semantics of the target block, it does not wait for the item to actually be processed.

And:

For target blocks that support postponing offered messages, or for blocks that may do more processing in their Post implementation, consider using SendAsync, which will return immediately and will enable the target to postpone the posted message and later consume it after SendAsync returns.

In other words, while both are asynchronous with respect to processing the message, SendAsync allows the target block to decide whether or not to accept the message asynchronously too.

It sounds like SendAsync is a generally "more asynchronous" approach, and one which is probably encouraged in general. What isn't clear to me is why both are required, as it certainly sounds like Post is broadly equivalent to using SendAsync and then just waiting on the result. As noted in comments, there is one significant difference: if the buffer is full, Post will immediately reject, whereas SendAsync doesn't.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    thanks, it made it a bit clearer though your last sentence sums up my remaining confusion. From my own tests, when a data block refuses to accept a message I did not see any advantage in using SendAsync over Post, both did not attempt to re-deliver the message when the data block signals that it accepts messages at a later point. (both immediately return if the message is refused and both immediately return if the message is accepted). In that the semantics of "accepting" the message re Post vs SendAsync are still nebulous to me. – Matt Nov 28 '12 at 07:36
  • I guess I just do not understand how much latency could potentially be introduced in the "acceptance/decline" mechanism of new passed messages. So far I have never seen any measurable delays between the passing and arrival of a message in the input queue/rejection from the queue. But thanks anyway for putting the focus on the "acceptance/rejection" part of the issue. – Matt Nov 28 '12 at 07:40
  • 4
    @Freddy: Sure - but the difference is when a block *postpones* the accept/decline decision. Maybe the target block you're using never does that, of course. – Jon Skeet Nov 28 '12 at 07:40
  • 1
    ❝Post is broadly equivalent to using SendAsync and then just waiting on the result.❞ I don't think this is correct. In case of a full input buffer `Post(x)` does not wait, while `SendAsync(x).Wait()` does wait. – Theodor Zoulias Aug 14 '19 at 05:58
  • @TheodorZoulias: Will edit to highlight that difference. I did say "broadly" :) – Jon Skeet Aug 14 '19 at 06:42
  • Thanks! Even Stephen Cleary has this wrong in his [blog](https://blog.stephencleary.com/2012/11/async-producerconsumer-queue-using.html). :-) (❝Post will (synchronously) block once the throttling threshold is reached❞) – Theodor Zoulias Aug 14 '19 at 07:09