Worker threads

Didn’t want to hijack Idlework’s thread…

It is for sure complicated and error prone to make a new thread. And, yes, you should not share any std container between two treads. Well, you can, but it’s tricky.

For your application I would just make a flag both threads can look at, like std::atomic inintDone;

The UI could just not look at the container until initDone is true. The worker thread can set initDone true when it’s done rounding up all the data.

My SFZ Player does something similar. When you load a new SFZ it stops playing, and stops showing the normal UI. The UI shows loading status (I think), and the module::process does nothing. Until the worker thread is done loading the SFZ, parsing it, then loading the (possibly thousands of) samples.

1 Like

So the UI could request a new container by clearing initDone after it had built a menu structure. That would work for 1 reader, 1 writer, with the worker looping in a test yield after notification of changes to be collected and sorted.

Multiple readers can use a proxied singular read only, perhaps cloned for transit over updates.

Multiple writers could maintain their own structures to write in, and use a pointer to volatile list atomic to add themselves as head, and an intermediate could merge the list of structures into a single proxied copy. Of course the writers could have to check the read proxy. Or check included, remove and check removed from the “is included” list.

EDIT: Of course you could do a python style GIL, but the benefits of writer count scaling and merging of presorted part collections into a fully sorted collection in time complexity would be lost. So the trade off is the latency in scheduling the merge and proxy provision thread.

EDIT2: I guess the proxy provider could recycle the read only collection when its get request in progress counter reached zero after the collection had been replaced with an update. Making for kind of a triple buffer.

EDIT3: Rather than reference counting the read only clone, an “accessor” list with head check added while logic, and remove self check removed and on empty recycle. :smiley:

EDIT4: Heavy readers could benefit from a supplyOwnedClone() method, But to prevent deep copy generics a releaseOwnership(clone) for delete to best work by the proxy supplier owning all the elements for delete.

Yeah, ok.you teaching a class here in thread contention and scalability?

I’m not sure. Is that a job offer? I doubt that I’d pass the interview. I haven’t checked the std:: library yet for some suitable base classes.

// atomic forward parallel list
// item made atomic to prevent non-NULL save before sheduled after
// any field in item used for sequencing must be atomic (i.e. bool done)
// to prevent list addition of OoO scheduled writes
// N.B. there is no LIFO order garauntee
// this structure is both the element and the pointer to list
template<typename kind>
struct plist {
	std::atomic<plist<kind>*> next;
	std::atomic<kind*> item;
	void insertOne(plist<kind>* what);
	plist<kind>* removeAfter(kind* what);
	plist<kind>* removeOneAfter();
	bool containedAfter(kind* what);
	plist<kind>* supply(kind* what);
	kind* resolve(plist<kind>* what);
};

and

// atomic parallel list primitives
template<typename kind>
void plist<kind>::insertOne(plist<kind>* what) {
	while(!containedAfter(what)) {
		plist<kind>* here = this.next.load();
		what->next.store(here);
		this.next.store(what);
	}
}

template<typename kind>
plist<kind>* plist<kind>::removeAfter(kind* what) {
	plist<kind>* here = NULL;
	while(containedAfter(what)) {
		here = &this;
		plist<kind>* last = NULL;
		while(last = here, here = here->next.load()) {
			if(here->item.load() == what) {
				last->next.store(here->next.load());
				break;
			}
		}
	}
	return here;
}

template<typename kind>
plist<kind>* plist<kind>::removeOneAfter() {
	plist<kind>* here = &this;
	here = here->next.load();
	if(here) return removeAfter(here->item.load());
	return NULL;
}

template<typename kind>
bool plist<kind>::containedAfter(kind* what) {
	plist<kind>* here = &this;
	while(here = here->next.load()) {
		if(here->item.load() == what) return true; 
	}
	return false;
}

template<typename kind>
plist<kind>* plist<kind>::supply(kind* what) {
	plist<kind>* here = new plist<kind>;
	here->next.store(NULL);
	here->item.store(what); 
	return here;
}

template<typename kind>
kind* plist<kind>::resolve(plist<kind>* what) {
	kind* here = what->item.load();
	delete what;
	return here;
}

May or may not work, may or may not be lock free and … but roughly at least.

Right now I just have a queue with a lock everyone uses. There’s N writers, 1 reader. The reader pulls items out of the queue and indexes them. It’s not the fastest thing possible, but it’s the complexity I can manage right now.

That’s OK, N writers for fixed N is likely best done by an array of atomic bool. The main reason for trying for lock free is task switching or waiting on locks. I see from the C++17 docs somewhere that volatile atomics are deprecated as atomic implies volatile. Mutexes and spinlocks are also fine. The Linux kernel uses a lot of them. The task switch on wait as locked is not too critical in the Rack world for most purposes. It does let N be quite big in the O(N) list length though if the list is lock free (the C++ compiler/architecture decides this).

// atomic parallel list primitives
template<typename kind>
void plist<kind>::insertOne(plist<kind>* what) {
	while(!containedAfter(what)) {
		plist<kind>* here = this.next.load();
		what->next.store(here);
		this.next.store(what);
	}
}

template<typename kind>
plist<kind>* plist<kind>::removeAfter(plist<kind>* what) {
	plist<kind>* here = NULL;
	while(containedAfter(what)) {
		here = &this;
		plist<kind>* last = NULL;
		while(last = here, here = here->next.load()) {
			if(here == what) {
				last->next.store(here->next.load());
				break;
			}
		}
	}
	if(here) here->next.store(NULL);// maybe null to remove
	return here;
}

template<typename kind>
bool plist<kind>::containedAfter(plist<kind>* what) {
	plist<kind>* here = &this;
	while(here = here->next.load()) {
		if(here == what) return true; 
	}
	return false;
}

template<typename kind>
plist<kind>* plist<kind>::supply(kind* what) {
	plist<kind>* here = new plist<kind>;
	here->next.store(NULL);
	here->item.store(what); 
	return here;
}

template<typename kind>
kind* plist<kind>::resolve(plist<kind>* what) {
	kind* here = what->item.load();
	delete what;
	return here;
}

Might work better

// atomic forward parallel list
// item made atomic to prevent non-NULL save before sheduled after
// any field in item used for sequencing must be atomic (i.e. bool done)
// to prevent list addition of OoO scheduled writes
// N.B. there is no LIFO order garauntee
// this structure is both the element and the pointer to list

// to be safe the reader shouldn't remove or add things but can traverse
// the list and mark feedback atomics in kind for the writer to remove and update
// in this way list containment represents read freedom until an atomic
// within kind is set to allow updates.

// a reader may clone or merge various kind structures and then use an atomic
// in kind to get updates, as this would be more efficient than the writer
// checking containment (although it could do this).

// allowing deletes of things (a type of write) needs some signalling.
// a read proxy likely needs a plist to track when reclimation can happen.
template<typename kind>
struct plist {
	std::atomic<plist<kind>*> next;
	std::atomic<kind*> item;
	void insertOne(plist<kind>* what);
	plist<kind>* removeAfter(plist<kind>* what);
	//plist<kind>* removeOneAfter();//BAD, needs unique reference plist<kind>*
	bool containedAfter(plist<kind>* what);
	// malloc from new and delete can lock on heap compaction
	// use the following to obtain and release unique reference list elements
	plist<kind>* supply(kind* what);
	kind* resolve(plist<kind>* what);
};

With as a header.

Ok so it’s an unreferenced thing, but a minor cache line on the I-cache already there?

unsubscribe

I’ve been looking into having a thread for computation outside of the audio thread, so this is helpful.

As a side note, I searched and couldn’t find a better place to post this, but this is in the general topic of increasing performance. I wanted to thank you @Squinky for the documentation on making efficient plugins. I’m using your lower-sample-rate cycle strategy like you demonstrated here, and it’s working wonderfully! Just wanted to make a shout-out for your awesomeness. Let me know if it should go somewhere else. :slight_smile: