TPL Dataflow, whats the functional difference between Post() and SendAsync()?
Asked Answered
C

2

66

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?

Candescent answered 28/11, 2012 at 6:44 Comment(1)
A relevant quote from this blog: 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.Eldrid
L
74

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);
}
Lao answered 28/11, 2012 at 13:17 Comment(15)
ok but given your explanation, then what is the logic behind Task<bool>? If it cannot submit immediately due to postponement but the task completes later whats the difference between the bool being true and false?Candescent
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.Lao
great, this now makes complete sense, that possibility completely slipped my mind. Thanks a lot.Candescent
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<bool> 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...Candescent
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").Lao
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? ThanksCandescent
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 that goes more in-depth on how Dataflow blocks actually work.Lao
Great, thanks, so you are basically saying the message within Post() will be lost if the method returns false?Candescent
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.Lao
Thanks a lot for your patience. Would up vote multiple times if I couldCandescent
So does this mean everybody should always use SendAsync unless they are ok with getting messages dropped?Salot
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.Gracye
@Gracye 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.G
@G - "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".Gracye
@Gracye 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.)G
D
24

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.

Dismantle answered 28/11, 2012 at 7:9 Comment(6)
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.Candescent
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.Candescent
@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.Dismantle
❝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.Nominate
@TheodorZoulias: Will edit to highlight that difference. I did say "broadly" :)Dismantle
Thanks! Even Stephen Cleary has this wrong in his blog. :-) (❝Post will (synchronously) block once the throttling threshold is reached❞)Nominate

© 2022 - 2024 — McMap. All rights reserved.