Punchclock 4.0.2

dotnet add package Punchclock --version 4.0.2
                    
NuGet\Install-Package Punchclock -Version 4.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Punchclock" Version="4.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Punchclock" Version="4.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Punchclock" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Punchclock --version 4.0.2
                    
#r "nuget: Punchclock, 4.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Punchclock@4.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Punchclock&version=4.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Punchclock&version=4.0.2
                    
Install as a Cake Tool

NuGet Stats Build Code Coverage #yourfirstpr <br>

<br /> <a href="https://github.com/reactiveui/punchclock"> <img width="120" heigth="120" src="https://raw.githubusercontent.com/reactiveui/styleguide/master/logo_punchclock/main.png"> </a>

Punchclock: A library for managing concurrent operations

Punchclock is the low-level scheduling and prioritization library used by Fusillade to orchestrate pending concurrent operations.

What even does that mean?

Ok, so you've got a shiny mobile phone app and you've got async/await. Awesome! It's so easy to issue network requests, why not do it all the time? After your users one-:star2: you for your app being slow, you discover that you're issuing way too many requests at the same time.

Then, you try to manage issuing less requests by hand, and it becomes a spaghetti mess as different parts of your app reach into each other to try to figure out who's doing what. Let's figure out a better way.

Key features

  • Bounded concurrency with a priority-aware semaphore
  • Priority scheduling (higher number runs first)
  • Key-based serialization (only one operation per key runs at a time)
  • Pause/resume with reference counting
  • Cancellation via CancellationToken or IObservable
  • Task and IObservable friendly API

Install

  • NuGet: dotnet add package Punchclock

Quick start

using Punchclock;
using System.Net.Http;

var queue = new OperationQueue(maximumConcurrent: 2);
var http = new HttpClient();

// Fire a bunch of downloads – only two will run at a time
var t1 = queue.Enqueue(1, () => http.GetStringAsync("https://example.com/a"));
var t2 = queue.Enqueue(1, () => http.GetStringAsync("https://example.com/b"));
var t3 = queue.Enqueue(1, () => http.GetStringAsync("https://example.com/c"));
await Task.WhenAll(t1, t2, t3);

Priorities

  • Higher numbers win. A priority 10 operation will preempt priority 1 when a slot opens.
await queue.Enqueue(10, () => http.GetStringAsync("https://example.com/urgent"));
  • Use a key to ensure only one operation for that key runs at a time.
  • Useful to avoid thundering herds against the same resource.
// These will run one-after-another because they share the same key
var k1 = queue.Enqueue(1, key: "user:42", () => LoadUserAsync(42));
var k2 = queue.Enqueue(1, key: "user:42", () => LoadUserPostsAsync(42));
await Task.WhenAll(k1, k2);

Cancellation

  • Via CancellationToken:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await queue.Enqueue(1, key: "img:1", () => DownloadImageAsync("/1"), cts.Token);
  • Via IObservable cancellation signal:
var cancel = new Subject<Unit>();
var obs = queue.EnqueueObservableOperation(1, "slow", cancel, () => Expensive().ToObservable());
cancel.OnNext(Unit.Default); // cancels if not yet running or in-flight

Pause and resume

var gate = queue.PauseQueue();
// enqueue work while paused; nothing executes yet
// ...
gate.Dispose(); // resumes and drains respecting priority/keys

Adjust concurrency at runtime

queue.SetMaximumConcurrent(8); // increases throughput

Shutting down

await queue.ShutdownQueue(); // completes when outstanding work finishes

API overview

  • OperationQueue

    • ctor(int maximumConcurrent = 4)
    • IObservable<T> EnqueueObservableOperation<T>(int priority, Func<IObservable<T>>)
    • IObservable<T> EnqueueObservableOperation<T>(int priority, string key, Func<IObservable<T>>)
    • IObservable<T> EnqueueObservableOperation<T, TDontCare>(int priority, string key, IObservable<TDontCare> cancel, Func<IObservable<T>>)
    • IDisposable PauseQueue()
    • void SetMaximumConcurrent(int maximumConcurrent)
    • IObservable<Unit> ShutdownQueue()
  • OperationQueueExtensions

    • Task Enqueue(int priority, Func<Task>)
    • Task<T> Enqueue<T>(int priority, Func<Task<T>>)
    • Task Enqueue(int priority, string key, Func<Task>)
    • Task<T> Enqueue<T>(int priority, string key, Func<Task<T>>)
    • Overloads with CancellationToken for all of the above

Best practices

  • Prefer Task-based Enqueue APIs in application code; use observable APIs when composing with Rx.
  • Use descriptive keys for shared resources (e.g., "user:{id}", "file:{path}").
  • Keep operations idempotent and short; long operations block concurrency slots.
  • Use higher priorities sparingly; they jump the queue when a slot opens.
  • PauseQueue is ref-counted; always dispose the returned handle exactly once.
  • For cancellation via token, reuse CTS per user action to cancel pending work quickly.

Advanced notes

  • Unkeyed work is prioritized ahead of keyed work internally to keep the pipeline flowing; keys are serialized per group.
  • The semaphore releases when an operation completes, errors, or is canceled.
  • Cancellation before evaluation prevents invoking the supplied function.

Full examples

  • Image downloader with keys and priorities
var queue = new OperationQueue(3);

Task Download(string url, string dest, int pri, string key) =>
    queue.Enqueue(pri, key, async () =>
    {
        using var http = new HttpClient();
        var bytes = await http.GetByteArrayAsync(url);
        await File.WriteAllBytesAsync(dest, bytes);
    });

var tasks = new[]
{
    Download("https://example.com/a.jpg", "a.jpg", 1, "img"),
    Download("https://example.com/b.jpg", "b.jpg", 1, "img"),
    queue.Enqueue(5, () => Task.Delay(100)), // higher priority misc work
};
await Task.WhenAll(tasks);

Troubleshooting

  • Nothing runs? Ensure you didn't leave the queue paused. Dispose the token from PauseQueue.
  • Starvation? Check if you assigned very high priorities to long-running tasks.
  • Deadlock-like behavior with keys? Remember keyed operations are strictly serialized; avoid long critical sections.

Contribute

Punchclock is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. Because of our Open Collective model for funding and transparency, we are able to funnel support and funds through to our contributors and community. We ❤ the people who are involved in this project, and we’d love to have you on board, especially if you are just getting started or have never contributed to open-source before.

So here's to you, lovely person who wants to join us — this is how you can support us:

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Punchclock:

Package Downloads
fusillade

Package Description

akavache.http

An HttpClient-based cache for HTTP requests based on Akavache

GitHub repositories (5)

Showing the top 5 popular GitHub repositories that depend on Punchclock:

Repository Stars
HMBSbige/BilibiliLiveRecordDownLoader
Bilibili 直播录制
reactiveui/Fusillade
An opinionated HTTP library for Mobile Development
flagbug/Espera
Espera is a media player that plays your music, YouTube videos, SoundCloud songs and has a special "party mode".
Clancey/gMusic
This is a multi platform music player.
HTBox/crisischeckin
Crisischeckin Humanitarian Toolbox repository
Version Downloads Last Updated
4.0.2 234 9/3/2025
4.0.1 142 9/3/2025
3.4.143 19,439 5/2/2024
3.4.95-g63ed44e8a1 253 11/24/2022
3.4.3 129,787 2/1/2021
3.4.1 821 1/22/2021
3.3.2 39,410 10/23/2020
3.3.1-ge8d6934868 446 10/23/2020
3.2.7 4,148 7/28/2020
3.2.1 43,952 4/28/2020
3.1.1 103,726 3/21/2019
3.0.17 7,959 2/5/2019
2.1.0 56,576 9/3/2017
2.0.0 19,794 11/10/2016
1.2.0 100,751 10/10/2014
1.1.1 96,696 5/1/2014
1.1.0 2,339 12/5/2013
1.0.1.1 1,729 11/12/2013
1.0.0 1,598 10/9/2013