Workers That Stay Workers
Objo Studio 26.8.5 introduces a new project type: Worker.
Workers let your Desktop and Command Line apps move CPU-intensive or blocking jobs into isolated processes. Add several processes to a pool and those jobs can run simultaneously across multiple CPU cores, while the controlling application remains responsive.
That's the short version.
The more interesting version is that we've made multiprocessing feel like a natural part of an Objo solution, not an awkward collection of helper executables, build settings and crossed fingers.
A Good Idea With an Awkward Precedent
Objo Studio is not the first tool to have this idea. Xojo recognised that hobbyist developers should be able to use multiple processor cores without becoming experts in process management and inter-process communication.
Unfortunately, their implementation was poor.
Xojo’s own documentation explains that Workers are simulated with threads while debugging, but become separate console helper applications in a built app. That means the program you debug does not have quite the same execution model as the program you ship. It works but it's a hack.
Objo Workers Are Always Workers
Objo takes a simpler position:
A Worker is an isolated operating-system process during development, debugging and deployment.
Each Worker process contains its own Objo VM and internal state. For example, four Worker processes really are four processes, whether the application is running locally, under the debugger, through the Remote Debugger or as a published app.
There is no lightweight stand-in during development and no last-minute costume change when you click Publish.
This matters for performance testing, but it matters even more for correctness. Process boundaries expose assumptions about global state, resources, errors and data ownership. Personally, I'd much rather discover those assumptions while stepping through code in Studio than after somebody installs the application.
A Worker Is a Project
In Objo Studio, a Worker is a first-class project inside the solution.
A Worker project has its own App class, source files and resources. A Desktop or Command Line project selects the Workers it needs under Project Settings > Workers. Studio then compiles and deploys those projects automatically whenever the controlling application is run, debugged, profiled, built or published.
The Worker is clearly visible in the Solution Navigator. It is not a special object hidden inside a desktop project, nor is it an unrelated command-line application that happens to be lurking beside the main executable.
Autocomplete knows which Workers are available. Misspellings and missing references are caught at compile time. References use stable project IDs, so renaming Worker projects does break things.
Sharing Code Without Pretending to Share Memory
Solutions already provide Shared Code for classes, interfaces and modules used by multiple projects. A shared parser, data model or validation library can therefore be compiled into both the controlling application and its Worker projects without copying source files or maintaining linked duplicates.
For example, a desktop search application and its indexing Worker can both use the same parser from Shared Code:
Class App Inherits WorkerApplication
Event Run(job As WorkerJobContext) As WorkerMessage
job.ThrowIfCancellationRequested()
job.ReportProgress("Indexing " + job.Message.Text)
Var result As String = SharedIndexer.Index(job.Message.Text)
Return New WorkerMessage(result)
End Event
End Class
This is code sharing, not memory sharing. The controller and Workers still have separate heaps and separate instances of their globals. That distinction is deliberate. Shared mutable objects crossing invisibly between processes would be convenient right up until they became terrifying.
Passing Data Is Explicit, but Not Difficult
All job data crosses the process boundary in a WorkerMessage. A message contains copied bytes and can be created from either a String or a MemoryBlock.
That gives applications a simple default while leaving the data format open. A Worker can receive UTF-8 text, JSON, MessagePack, image data, compressed records or a custom binary format. The runtime does not insist that every problem is secretly a string.
The controller submits jobs and awaits their completion using the same task-based style as the rest of Objo:
Var pool As New WorkerPool(Workers.Indexer)
pool.ProcessCount = 4
pool.QueueCapacity = 16
Await pool.Start()
Var jobs As Array(Of WorkerJob) = []
For Each path As String In paths
jobs.Append(pool.Submit(path))
Next
For Each job As WorkerJob In jobs
Var result As WorkerMessage = Await job.Completion
SaveResult(result.Text)
Next
Await pool.Stop()
Workers can send ordered progress messages while a job is running. The controller can cancel queued or active work. Successful jobs return a message, while failures arrive as structured errors containing the original Objo exception type, Worker name, process ID, job ID and Worker call stack.
Debugging the Actual Architecture
Multiprocess debugging was the part we were unwilling to fake.
When a Worker reaches a breakpoint, Studio adds that process to the debugger’s target selector. If several instances of the same Worker project are running, they appear separately—for example:
Indexer · PID 4321
Indexer · PID 4327
Indexer · PID 4330
Each process retains its own call stack, locals, globals, watches and paused state. Stepping applies to the selected process. Continue resumes the complete application tree. Stop terminates the controller and all of its Workers.
Breakpoints and watchpoints also work in Shared Code. Studio qualifies source locations by project, so an App file in the controller cannot be confused with an App file in a Worker.
The Worker’s control channel remains responsive while its VM is paused. Heartbeats, cancellation, shutdown and debugger commands continue to work, so inspecting a breakpoint does not make the pool conclude that its Worker has died of boredom.
Remote debugging uses the controller’s existing authenticated connection. Worker debugger traffic is tunnelled through it, so every Worker does not demand its own network port, firewall rule and tiny security review.
Pools, Not Disposable Processes
A WorkerPool owns persistent processes. Each process handles one job at a time but can handle many jobs over its lifetime, avoiding the cost of launching a fresh process for every item.
The process count can be changed while the pool is running. Growing a pool starts and authenticates new processes before scheduling work on them. Shrinking it allows busy processes to finish their current jobs before retiring.
The queue is bounded, so a producer cannot consume unlimited memory by submitting work faster than it can be processed. A full queue produces a predictable WorkerQueueFullError, allowing the application to pause, report overload or try again later.
If a Worker crashes, only its active job fails. The controlling application continues and the pool starts a replacement process. Cancellation begins cooperatively, giving Worker code an opportunity to stop cleanly. If it ignores cancellation beyond the configured grace period, the process is terminated and replaced.
Retries are disabled by default because automatically repeating work with side effects is a splendid way to turn one problem into two. They can be enabled explicitly for jobs designed around at-least-once execution.
What Workers Are For
Workers are intended for substantial independent jobs:
- Crawling and parsing web pages
- Indexing documents
- Processing images or media
- Compressing and converting files
- Importing large datasets
- Running blocking operations without freezing the application
- Performing CPU-heavy calculations across several cores
They are not a replacement for Task. Tasks remain the lightweight choice for asynchronous coordination inside one VM. Workers are for jobs that benefit from genuine process isolation or parallel execution across processor cores.
The Feature Is the Integration
Objo Workers share source cleanly, exchange text or binary messages, report progress, support cancellation, survive crashes, participate in profiling and can be debugged as the processes they really are. Studio handles their compilation, deployment and packaging alongside the controlling application.
No debug-only impersonation. No collection of manually managed helper apps. No surprise transformation between pressing Run and pressing Publish.
Just Workers that stay Workers.
To learn more, open the Worker Pool Basics example bundled with Studio for a small introduction, then try Parallel Web Crawler to see a larger pool in action. Full documentation is also available in the Worker Processes documentation.