> ## Content Index
> Fetch the complete content index at: https://devinside.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# One published value can hide two factories.
- URL: https://devinside.cloud/lazy-t-on-a-hot-path-factories-contention-exceptions-and-allocations/
- Published: 2026-11-12T15:49:29.000Z
- Updated: 2026-11-13T00:58:18.000Z
- Description: Four callers reach the same lazily initialized value at nearly the same time. All four eventually receive the same instance. How many times did the factory run?
- Author: Marcin Borecki
- Tags: Engineering, Performance & Runtime, #eng05, #devinside-review

Historical edition · Sources and examples scoped to 2025-07-03.

Four callers reach the same lazily initialized value at nearly the same time. All four eventually receive the same instance. How many times did the factory run?

The answer depends on the initialization policy. Observing one published value does not prove one execution. A policy can permit competing factories, select one result and discard the others. That may be acceptable for a cheap, side-effect-free computation. It is a different proposition when the factory opens a connection, registers a callback or starts work outside the process.

This is why I would not begin a `Lazy<T>` performance review with the size of a wrapper or the speed of an already initialized property read. I would begin with the factory's contract: what may it do, may it run more than once, what happens if it fails, and which callers can arrive before initialization finishes?

The worked example below treats a factory invocation as visible work. Change the number of first readers, the publication policy and the failing attempts. The model calculates executions, discarded candidates and the result seen by later reads. It does not run the CLR or present a timing result.

Compare `ExecutionAndPublication` with `PublicationOnly` under simultaneous first access. Then make the factory fail. The interesting difference is not only the successful value; it is whether another access can cause another attempt and what state remains after the failure.

## There are two meanings of “once”

One published value is a statement about the object visible after initialization. One factory execution is a statement about work performed while trying to establish that object. A design can promise the first without promising the second.

```csharp
var shared = new Lazy<ConnectionInfo>(
    CreateConnectionInfo,
    LazyThreadSafetyMode.ExecutionAndPublication);

```

With this factory-based form, `ExecutionAndPublication` coordinates initialization so the selected factory path is executed under its synchronization policy. If the factory throws, exception caching is part of the behavior to understand. The implementation is not simply “try again every time until it works.”

`PublicationOnly` permits multiple threads to run a factory while initialization is in progress. Publication chooses a value, but other candidates may have been created along the way. Exceptions on those attempts are not cached in the same way as the factory-based execution-and-publication path. A later access can therefore be another opportunity to initialize.

The historical [.NET 9.0.4 Lazy implementation](https://github.com/dotnet/runtime/blob/v9.0.4/src/libraries/System.Private.CoreLib/src/System/Lazy.cs?ref=devinside.cloud) is a useful source because it exposes the different states instead of compressing all of them into the word lazy. Constructor choice matters too: claims about a supplied value factory should not be generalized carelessly to every constructor and failure path.

![Execution and publication are different promises](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/execution-vs-publication.svg)

[Inspect the full diagram ↗](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/execution-vs-publication.svg)Swipe the drawing to inspect it.

## A discarded candidate may still have consequences

Suppose a factory creates an object that registers itself with an event publisher. Under a competing-factory policy, several candidates may register before one becomes the published value. Discarding an unselected candidate from the `Lazy<T>` does not automatically undo its registration.

The same issue appears with external work. A factory that sends a request or creates a remote resource can have an effect before returning a candidate. Selecting only one candidate does not make the other attempts disappear from the remote system.

That does not make `PublicationOnly` inherently unsafe. It means the factory needs an appropriate contract. Pure computation with disposable local candidates is a very different case from an initializer with externally visible side effects. Decide which one you have before choosing a policy because it appears to reduce contention.

The model makes discarded work visible as separate candidates. Its counts are derived from an explicitly declared simultaneous-start scenario. A real runtime may start fewer factories if some callers arrive after publication. The model shows what the policy permits under its chosen schedule, not a guarantee that every invocation pattern produces the maximum number of candidates.

## The factory can retain more than its result

A lambda can capture state from its surrounding scope. That captured state can include an owner, a request object or a large buffer. The returned object may be small while the factory or closure extends the lifetime of something much larger.

```csharp
byte[] input = LoadConfigurationBytes();
var parsed = new Lazy<Settings>(() => Parse(input));

```

The lifetime question is not answered by the size of `Settings`. Before initialization, the lazy instance needs enough state to invoke the factory. If an application retains the lazy instance for a long time, it can also retain the captured input for that period.

What happens after initialization is an implementation detail worth inspecting for the exact version rather than assuming. More broadly, the factory can return a result that itself retains the input or an owner can retain both objects independently. Cutting one reference does not establish that the entire graph became collectible.

![Deferring work can extend a captured lifetime](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/factory-ownership.svg)

[Inspect the full diagram ↗](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/factory-ownership.svg)Swipe the drawing to inspect it.

A good optimization review therefore asks both how often initialization runs and what remains reachable while it is deferred. Moving work later can move memory retention earlier and extend it. That is sometimes the right tradeoff, but it should be a tradeoff the design acknowledges.

## Warm access and first access are different workloads

Once a lazy value is initialized, reading it follows a different path from constructing the wrapper and running its factory. Combining those operations into one number can hide the cost you actually wanted to investigate.

A warm-access experiment asks what repeated observation of an already available value costs. A first-access experiment includes initialization and potentially synchronization. A wrapper-construction experiment may include delegate and closure creation before any value is requested. A contention experiment changes which callers overlap at the initialization boundary.

Those are four useful questions. They should not be presented as four names for the same benchmark. If a result improves only in the warm case, that may matter for a hot property. It says little about startup work or a factory whose cost dominates the first access.

![Cold, warm and contended access are distinct workloads](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/workload-boundaries.svg)

[Inspect the full diagram ↗](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/workload-boundaries.svg)Swipe the drawing to inspect it.

A direct precomputed value can be a control, but it is not always a replacement. Eager computation changes when failures happen and whether unused work is performed. A branch that avoids constructing a value at all can be the real benefit of laziness, even when accessing an initialized wrapper is slightly more expensive than reading a field.

## Failure policy belongs to the product decision

Imagine initialization reads a local configuration that is permanently malformed. Repeatedly attempting the same expensive parse on every caller can be wasteful and confusing. Caching the failure can make the state stable and easier to diagnose.

Now imagine a transient dependency that was unavailable during the first attempt but may recover later. Permanently retaining the first failure can prevent recovery unless the application replaces the lazy instance or uses a different mechanism. The same exception-caching behavior that helps one problem can harm the other.

Do not hide the recovery policy inside a type chosen mainly for convenience. If the application needs retries, backoff, cancellation or refresh, a single lazy initialization may be the wrong abstraction. It is often clearer to model a stateful resource owner explicitly than to repeatedly replace lazy wrappers while pretending the state is still one-time initialization.

![Failure behavior belongs to the contract](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/failure-policy.svg)

[Inspect the full diagram ↗](https://devinside.cloud/content/images/2026/11/devinside/v4/ENG05/failure-policy.svg)Swipe the drawing to inspect it.

The demonstration exposes the failure count so you can compare these policies. A failed execution-and-publication attempt remains a cached failure in the modeled factory-based case. Publication-only attempts can fail without establishing a published value, and another read can initiate another wave. That difference follows the chosen contract; it is not an inferred performance advantage.

## Thread safety does not make arbitrary recursion safe

Synchronization around initialization is not a general solution for every relationship inside the factory. A factory can re-enter the same lazy value, depend on another lazy value that depends on it, or hold a lock required by a competing initialization path.

These relationships deserve their own review. Reentrant access to an initialization in progress is not the same as a later read of an initialized value. Cross-object cycles can be harder to diagnose because each object looks reasonable in isolation.

`LazyThreadSafetyMode.None` is not a way to ask the runtime to resolve those relationships faster. It is a choice that gives up the relevant synchronization guarantee. Use it only when the ownership and access pattern establish that the value is not concurrently initialized. A benchmark with one thread does not justify using the same policy for a shared service singleton.

The model refuses to classify concurrent `None` access as a safe deterministic result. Assigning a neat number to an unsupported concurrency contract would be a misleading demonstration, even if one observed run happened to produce that number.

## Test the factory, the callers and the cleanup

For semantic tests, make factory entry observable through a counter or controlled gate. Arrange the caller overlap deliberately rather than hoping several threads happen to race. Confirm the selected value, the number of attempted executions under that schedule, and the behavior of failures.

Then inspect unselected candidates. If they own resources, who disposes them? If they registered callbacks or produced effects, what prevents those effects from outliving the candidate? A successful `Value` read is not evidence that this cleanup happened.

For a factory-based failure case, request the value again and verify the intended retry or cached-exception behavior. Test both a failure before any successful publication and a competing scenario where one attempt fails while another succeeds. Caller outcomes need not be identical merely because a later read sees one published value.

Performance measurements come after those contracts are established. Record cold and warm states separately, include the factory and wrapper costs relevant to the use case, and preserve contention and failure cases. The current companion contains later experiments without pretending they were performed at this historical context date.

The design question is not whether laziness is cheap. It is whether deferring this work, under this publication and failure policy, is worth the lifetime and coordination responsibilities it creates.

## Historical source boundary

The API and implementation discussion is pinned to .NET 9.0.4, available before this July 2025 edition. The browser uses a declared schedule and synthetic candidates to show permitted outcomes. It is not a CLR race recording or a benchmark. Current laboratory artifacts remain separate and retain their actual provenance.

GO FURTHER

## Inspect the contemporary laboratory.

This is a newly reconstructed historical edition, not a claim of earlier authorship. Later executed experiments, their exact environments and original results are preserved in a separate notebook.

[Open the current laboratory notebook ↗](https://devinside.cloud/lab-notebooks/v4/ENG05.html)

[Download the current reconstruction sources ↗](https://devinside.cloud/content/files/v4/ENG05-reconstruction.zip)