Ayende @ Rahien

It's a girl

The contracts of Lazy<T> vs Task<T>

There was a question about our use of Task<T> and Lazy<T> in RavenDB, and I thought that this is a subtle thing that deserve more than a short email.

The basic difference between Lazy<T> and Task<T> are the kind of contracts that they express.

  • Lazy<T> represent a potentially expensive operation that has been deferred. The promise given is that the lazy’s value will be generated the first time that it is needed.
  • Task<T> represent a potentially expensive operation that is currently executing. The promise is that the task’s value will be available on request, hopefully already there by the time you asked.

The major difference is when we are actually starting the operation. Most often, when we return a task, we return a reference to an scheduled / executing task, which will complete whatever or not the task’s result will be accessed. Conversely, a lazy’s whose value was never accessed is not something that will ever execute.

The use cases for them tend to be quite different.

Lazy<T> is used a lot of the time as a way to handle once and only once creation (basically, a safe singleton pattern), and for actually deferring the execution of work. Sometimes you can avoid having to do something, and it is easier to give a caller a lazy object and only have to pay for that additional work if they access the data.

Task<T> is primarily used to actually parallelize the work, so you’ll have it running while you are doing other stuff. Usually this is used for I/O, since that can be natively parallelized.

With RavenDB, we use Task<T> as the return type of all our async operations, allowing of to take advantage on the async nature of I/O. And we use Lazy<T> to setup a deferred call for the server. When someone actually access one of lazy’s values, we have to provide you with the answer. At this point, we can go to the server with all  the pending lazy operations, and save a lot of effort just making remote calls to the server.

In fact, in RavenDB 3.0, we have lazy support in the async API. That means that we have methods that return Lazy<Task<T>>, which means: Give me a deferred operation, that when required, will perform in an async manner. That gives me both the ability to combine requests to the server and avoid blocking up a thread while that I/O is in progress.

Comments

Frank Quednau
06/02/2014 09:24 AM by
Frank Quednau

we can go to the server with all the pending lazy operations

What do you mean by that? If a user accesses the value of a lazy instance, it is done on that instance (one value) - what do you know about other lazy values?

markrendle
06/02/2014 09:30 AM by
markrendle

The other thing I like Lazy for is avoiding I/O operations or virtual calls in constructors without needing an Initialize method or a factory.

Rafal
06/02/2014 09:58 AM by
Rafal

Can't this be done using just the Task interface? I mean, task creation doesn't imply it has to run immediately and so it can also behave in a lazy manner by making a request to the server only if someone's awaiting for completion.

Ayende Rahien
06/02/2014 10:02 AM by
Ayende Rahien

Frank, You perform multiple lazy operations, such as "give me (lazily) document users/1" and "give me (lazily) the orders for users/1" This doesn't generate a server request. When you access one of those lazy values, we initiate a single server request, that execute both requests in a single round trip.

Ayende Rahien
06/02/2014 10:03 AM by
Ayende Rahien

Rafal, No, you cannot do that. There is a very distinct different between awaiting on the result of a pending task and requesting that it be calculated.

Frank Quednau
06/02/2014 01:46 PM by
Frank Quednau

Ayende, what I mean is when you have in code:

User u = lazyUser.Value; Orders o = lazyOrders.Value;

How can you bundle that, considering it is two distinct calls?

Ayende Rahien
06/02/2014 01:51 PM by
Ayende Rahien

Frank, It looks like this:

var lazyUser = session.Lazily.LoadUser; var lazyOrders = session.QueryOrder.Where(x=> x.User == "users/1").Lazily();

// no server was touched so far

var user = lazyUser.Value;

// server was contacted for BOTH operations, in a single round trip

var orders = lazyOrders.Value;

// the data is already there from the previous call, so we don't contact the server,

Duke
06/05/2014 04:39 PM by
Duke

So is the lazyOrders.Value lazy then? Does not appear so. What happens if the data changes in between initialising the second lazy?

Ayende Rahien
06/06/2014 06:04 AM by
Ayende Rahien

Duke, The lazyOrders.Value is getting the already retrieved value which we got in the lazyUser.Value call.

Comments have been closed on this topic.