TThreadSafeHashSet<T> is a generic hash set protected by one TCriticalSection per set instance.
The current implementation lives in src/ThreadSafeCollections.HashSet.pas.
- Free Pascal 3.2.2 or later
SyncObjsHashFunctionsTypInfoThreadSafeCollections.InterfacesThreadSafeCollections.ErrorMessages
generic THashSetEqualityComparer<T> = function(const A, B: T): Boolean;
generic TEqualityComparer<T> = function(const A, B: T): Boolean;
generic THashFunction<T> = function(const Value: T): Cardinal;THashSetEqualityComparer<T> is the constructor's comparer type. The original
TEqualityComparer<T> name remains assignment-compatible for existing source code;
the distinct internal name avoids a Free Pascal late-specialization collision with
Generics.Defaults.TEqualityComparer<T>.
Specialized classes:
TThreadSafeHashSetInteger;
TThreadSafeHashSetString;
TThreadSafeHashSetBoolean;
TThreadSafeHashSetReal;Primitive comparer/hash helpers:
function IntegerEquals(const A, B: Integer): Boolean;
function StringEquals(const A, B: string): Boolean;
function BooleanEquals(const A, B: Boolean): Boolean;
function RealEquals(const A, B: Real): Boolean;
function IntegerHash(const Value: Integer): Cardinal;
function StringHash(const Value: string): Cardinal;
function BooleanHash(const Value: Boolean): Cardinal;
function RealHash(const Value: Real): Cardinal;The set uses separate chaining:
FBuckets: array of PEntry;
TEntry = record
Value: T;
Hash: Cardinal;
Next: PEntry;
end;Entries are allocated by a slab allocator:
ENTRY_BLOCK_SIZE = 256;Freed entries are recycled through a freelist. The allocator has no lock of its own; callers must hold the set lock.
Generic constructor:
constructor Create(
AEqualityComparer: specialize THashSetEqualityComparer<T>;
AHashFunction: specialize THashFunction<T>;
AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);Specialized constructors:
constructor TThreadSafeHashSetInteger.Create(AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);
constructor TThreadSafeHashSetString.Create(AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);
constructor TThreadSafeHashSetString.Create(
AHashFunction: specialize THashFunction<string>;
AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);
constructor TThreadSafeHashSetBoolean.Create(AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);
constructor TThreadSafeHashSetReal.Create(AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);Use the specialized classes for common primitive types. Use the generic class with explicit equality and hash functions for custom types.
type
TPoint = record
X: Integer;
Y: Integer;
end;
function PointEquals(const A, B: TPoint): Boolean;
begin
Result := (A.X = B.X) and (A.Y = B.Y);
end;
function PointHash(const Value: TPoint): Cardinal;
begin
Result := MultiplicativeHash(Cardinal(Value.X xor Value.Y));
end;
var
Points: specialize TThreadSafeHashSet<TPoint>;
P: TPoint;
begin
Points := specialize TThreadSafeHashSet<TPoint>.Create(@PointEquals, @PointHash);
try
P.X := 1;
P.Y := 1;
Points.Add(P);
finally
Points.Free;
end;
end;Core operations:
function Add(const Item: T): Boolean;
function Remove(const Item: T): Boolean;
function Contains(const Item: T): Boolean;
procedure Clear;
property Count: Integer read GetCount;
function IsEmpty: Boolean;Array and bulk operations:
function ToArray: specialize TArray<T>;
procedure AddRange(const Items: array of T);
procedure AddRange(const Collection: specialize IThreadSafeHashSet<T>);
function RemoveRange(const Items: array of T): Integer;
function RemoveRange(const Collection: specialize IThreadSafeHashSet<T>): Integer;Set operations:
function TryGetValue(const Item: T; out Value: T): Boolean;
procedure IntersectWith(const Collection: specialize IThreadSafeHashSet<T>);
procedure UnionWith(const Collection: specialize IThreadSafeHashSet<T>);
procedure ExceptWith(const Collection: specialize IThreadSafeHashSet<T>);
function Overlaps(const Collection: specialize IThreadSafeHashSet<T>): Boolean;
function SetEquals(const Collection: specialize IThreadSafeHashSet<T>): Boolean;Iteration and locking:
function GetEnumerator: TEnumerator;
function Lock: ILockToken;Most public operations acquire FLock directly and release it in finally.
Private helpers such as InternalAdd and InternalRemove assume the caller already holds FLock.
Manual Lock() is advanced usage. Do not hold a token and then call public methods on the same set, because those methods try to acquire the same critical section again.
AddRange(Collection) and RemoveRange(Collection) call Collection.ToArray directly. The source lock is held only while creating the snapshot and is released before this set is mutated, avoiding nested source-lock acquisition on non-reentrant POSIX critical sections.
The hash set enumerator holds an ILockToken for the full enumeration:
constructor TThreadSafeHashSet.TEnumerator.Create(ASet: TThreadSafeHashSet);
begin
inherited Create;
FSet := ASet;
FLockToken := FSet.Lock;
FCurrentBucket := -1;
FCurrentEntry := nil;
end;During a for..in loop, other public operations on the same set wait until enumeration ends.
IntersectWith snapshots the other collection with ToArray before acquiring this set's lock. That avoids the ABBA lock-order deadlock that would happen if two threads called A.IntersectWith(B) and B.IntersectWith(A). It builds an array-backed hash index over that snapshot using this set's hash and equality functions, then performs average O(n+m) membership filtering without calling the source collection while the destination lock is held.
AddRange, RemoveRange, ExceptWith, Overlaps, and SetEquals also use ToArray snapshots of the other collection.
Specialized hash functions:
Integer: multiplicative hashstring:XXHash32Boolean: direct value hashReal: fixed-point conversion hash
The generic TThreadSafeHashSet<T> does not auto-select a hash function for arbitrary T; callers provide one.
Constants:
INITIAL_BUCKET_COUNT = 16;
LOAD_FACTOR = 0.75;
MIN_BUCKET_COUNT = 4;
ENTRY_BLOCK_SIZE = 256;CheckLoadFactor resizes when FCount / Length(FBuckets) exceeds LOAD_FACTOR.
AddRange(const Items: array of T) pre-calculates the required bucket count from the current count plus input length, then resizes once if needed.
The set does not shrink automatically after removals.
| Operation | Complexity |
|---|---|
Add, Remove, Contains, TryGetValue |
O(1) average, O(n) worst under collisions |
Clear |
O(n) |
ToArray, iteration |
O(n) |
AddRange(array) |
O(m) average, plus resize costs |
RemoveRange(array) |
O(m) average |
IntersectWith |
O(n + m) average after source snapshot; O(n * m) worst case under full hash collision |
UnionWith |
Delegates to AddRange(Collection) |
ExceptWith |
O(m) average after source snapshot |
Overlaps |
O(m) average after source snapshot |
SetEquals |
O(m) average after source snapshot |
var
Set: TThreadSafeHashSetInteger;
begin
Set := TThreadSafeHashSetInteger.Create;
try
Set.Add(42);
Set.Add(17);
if Set.Contains(42) then
WriteLn('Found 42');
Set.Remove(17);
finally
Set.Free;
end;
end;var
SetA, SetB: TThreadSafeHashSetInteger;
begin
SetA := TThreadSafeHashSetInteger.Create;
SetB := TThreadSafeHashSetInteger.Create;
try
SetA.AddRange([1, 2, 3]);
SetB.AddRange([2, 3, 4]);
SetA.IntersectWith(SetB); // SetA contains 2 and 3.
finally
SetA.Free;
SetB.Free;
end;
end;- One exclusive lock protects the whole set; there are no reader/writer locks.
- Iteration blocks other public operations until the enumerator is destroyed.
- Ordering is bucket/chaining order and is not stable API.
- Heavy collisions degrade lookup operations to linear chain scans.
- There is no built-in serialization support.
- There is no
DEBUG_LOGGINGconstant or runtime debug logging switch in this unit.