[Canonical page](<https://docs.dhtmlx.com/planning-engine/net/how-to/share-a-project-between-threads/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Share a project between threads

Run work on one solved project from several threads at once, such as trying different edits in parallel, without corrupting it.

A [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) isn't thread-safe. Even calls that look read-only write to it: [`Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>) sets copy-on-write flags on the source. So give each thread its own clone, and make all the clones on one thread before the workers start. `Clone()` is cheap. A clone shares the data with its source until one of them is edited, and the first edit copies it.

## Prerequisites

- A [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) solved with [`Solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>).
- Nothing else uses the project while you clone it.

## Steps

1. Solve the project on the thread that owns it, and keep the result:

   ```csharp
   var project = new Project();
   var plan = project.Solve(data, new SolveOptions());
   ```

2. On the same thread, make one clone per worker:

   ```csharp
   var drafts = variants.Select(_ => project.Clone()).ToArray();
   ```

3. Start the workers and give each one its own clone. A worker can call anything on its clone: [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>), [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>), [`Preview`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>), [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>). It must not touch `project` or another worker's clone:

   ```csharp
   Parallel.For(0, variants.Length, i =>
   {
       var draft = drafts[i];
       var changes = new SchedulingChanges { BaselineId = plan.BaselineId };
       changes.Tasks.Update.Add(new TaskUpdate("design") { Duration = variants[i] });
       draft.Apply(changes);
       var result = draft.Solve(null, new SolveOptions());
       // ...
   });
   ```

   Every clone starts with the source's [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#baselineid>), so `plan.BaselineId` passes the stale check on each of them. After an edit, each clone mints its own token.

4. Treat every result as read-only. A clone that hasn't changed returns the same cached [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>) object as its source, so several threads can end up reading `plan` at once. Call `DeepClone()` on a result before you modify it.

5. Keep the variant the user picks: `project = drafts[i];`. Do this on the owning thread after the workers finish. If `project` was edited after you cloned it, the drafts started from an older state, so clone again.

To share one instance instead of cloning, put every call on it, including [`Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>), inside the same `lock`:

```csharp
var gate = new object();

lock (gate)
{
    project.Apply(changes);
    project.Solve(null, new SolveOptions());
}
```

The calls then run one at a time. Clones run in parallel, so use them when the threads need throughput.

The full example tries three durations for a task on three threads and prints when the next task ends:

```csharp
using Plen;

// Monday, August 10, 2026, on the default calendar.
var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("design") { StartDate = monday, Duration = 2.0 },
        new TaskData("build") { StartDate = monday, Duration = 3.0 },
    ],
    Links = [new LinkData { Id = "l1", Source = "design", Target = "build" }],
};

var project = new Project();
var plan = project.Solve(data, new SolveOptions());

// Durations of "design" to try, one per worker.
double[] variants = [3.0, 4.0, 5.0];

// Make every clone here, on the owning thread.
var drafts = variants.Select(_ => project.Clone()).ToArray();

var builds = new Mutation?[variants.Length];
Parallel.For(0, variants.Length, i =>
{
    var draft = drafts[i];
    var changes = new SchedulingChanges { BaselineId = plan.BaselineId };
    changes.Tasks.Update.Add(new TaskUpdate("design") { Duration = variants[i] });
    var report = draft.Apply(changes);
    if (report.Rejected.Count != 0)
    {
        throw new InvalidOperationException(report.Rejected[0].Message);
    }
    var result = draft.Solve(null, new SolveOptions());
    builds[i] = result.Updates.FirstOrDefault(u => u.Id == "build");
});

for (var i = 0; i < variants.Length; i++)
{
    Console.WriteLine($"design {variants[i]} days: build ends {builds[i]?.EndDate}");
}

var original = project.Solve(null, new SolveOptions());
Console.WriteLine($"original baseline kept: {original.BaselineId == plan.BaselineId}");
```

## Result

Each thread reports its own variant, and the original project is untouched:

```text
design 3 days: build ends 2026-08-18 08:00
design 4 days: build ends 2026-08-19 08:00
design 5 days: build ends 2026-08-20 08:00
original baseline kept: True
```

`Parallel.For` throws an `AggregateException` if a worker throws, for example an [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) from [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>).

To score alternative change sets in parallel, you don't need your own threads. [`EvaluateWith(candidates, options, Execution.Parallel())`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md#evaluatewith>) clones and scores the candidates on worker threads and returns the results in input order. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/evaluating-alternatives.md>). To keep long calls off a request thread in async code, see [Call from async code](<https://docs.dhtmlx.com/planning-engine/net/how-to/call-from-async-code.md>). For how clones behave, see [Cloning a project](<https://docs.dhtmlx.com/planning-engine/net/live-projects/previewing-changes.md#cloning-a-project>).
