You’ve got to be rather deliberate to cut out the whole PID counter part from f"asyncio-waitpid-{next(self._pid_counter)}". I even captioned that specific example with “Uniquely identify a worker:”, which is the whole point of a thread name like asyncio-waitpid-1, not because of the asyncio-waitpid part but because of the -1.
The proposed solution is no different from the numerous existing optionally configurable decorators out there including the stdlib, such as functools.lru_cache, dataclasses.dataclass, unittest.mock.patch, etc. just to name a few. It isn’t hard to explain.
Sorry, I wasn’t trying to cherry-pick. PIDs are non-deterministic; seeing no PID vs some random number is not particularly helpful when looking at a holistic thread view.
These functions aren’t designed to be used without a decorator, so I don’t think that’s a reasonable comparison.
To be blunt: If we do these weird function-returning-function semantics, I am -1 on the API. I think the behavior is too weird for a stdlib utility.
As the name implies, it’s a PID counter, not a PID, and it’s initialized with self._pid_counter = itertools.count(0) so it is a process-local deterministic counter. But even if it were an OS-assigned PID, it would be unique on the system, and would be particularly helpful in distinguishing message origins when threads output to the same logging channel.
If having the run function perform double duty as a factory feels too unconventional, we can separate it to two functions, not unlike your two-function proposal:
I agree with Peter it would make the function too weird.
You can set the name after the fact:
for kind, name in targets:
t = threading.run_daemon(self._run_kuberay_resource_events_watch, kind, name)
t.name = f"kubernetes_events_watch_{kind}_{name}"
self._threads.append(t)
Pretty sure that would run into a race condition since run/run_daemon starts the thread already when it returns–if the first thing that the thread does is to output its name it may very well output the default name rather than the name you set separately.
If that’s what you’re doing, then put the naming of the thread into the thread itself - as it starts up, it selects its own name. In the given example, the name can be entirely derived from other parameters to the thread, so it makes no difference where it’s set.
Yes, you can ALWAYS come up with some example that won’t work with the convenience API. Congratulations. You can simply not use the convenience API then. Are you really arguing with me on the basis that the easy option isn’t applicable to a sufficiently-convoluted example? Really?
Can we stop bogging down the thread with “hey, look what won’t work here”?
My point is that my proposed solution, especially the revised one, isn’t complicated whatsoever at all, so I struggle to understand your perceived downside to offering the flexibility to configure the thread properly for legitimate use cases when it costs pretty much nothing over the run/run_daemon alternative.
But it is. It’s not nearly as simple as “just call this and now you have a thread”. Maybe it works nicely if you only ever define a brand new function to use as each thread, but it won’t work for anything where you want to create more than one thread from the same code.
You are, of course, welcome to create a function like this in your own code. Nobody is stopping you from making whatever helpers you need for the situation that demands them. That’s fine, same as it has always been. But for this, we’re looking for something simple that can go into the standard library, and which doesn’t need to be overburdened with edge cases.
Um, what’s the proposal is at this point?
I wouldn’t support the variants without name, as I’d like to keep it easy to add a name to existing thread start code. Any convenience function discourages using the advanced features that it doesn’t cover.
Unless we make the name argument required (which I think would go against convenience), I’m not particularly convinced that supporting it will result in more people actually using it.
I think there is a an example like this earier that use the name Runner in place of Context. Not sure what a good name would be off the top of my head.
This reminds me of the database library API with cursors and connections, where connection.execute(...) is shorthand for connection.cursor().execute(...).
It works very well as far as I’m concerned. For complicated tasks one can use the cursor explicitly, and most of the time one can just use connection.execute.
The rationale for using start: Thread.run() is a separate method than Thread.start().
The rationale for using run: it doesn’t make sense to start a function, even if the return value of .run() is a thread (and not some function’s result)
For the mental model, having start() would make more sense. But it’s more convenient to also say “run this function in a thread, and give me back the thread”.
Alternatively, threading.eval(), threading.exec(), threading.schedule(), threading.spawn(), threading.threaded(), would all be reasonable alternatives. I like threading.threaded() because you create a thread from a function but I can see why threading.run() or threading.start() are more natural.