2020-07-09 21:50:17

I'm in a bit of a situation where I feel like, with out testing, that I may need threading to reduce strain on the processing, or at least, illiminate some issues that may arise with the amount of objects being handled at once.
I've used Threading once in my life, and that was only in theory. I've been warned to be careful of Threading by a couple people, however, I'd like to learn more about it to see if I should use it in the situation I am in.
Are there any good resources on General Threading Techniques, or, more specifically if I'm lucky, Python Threading?
Thank you.

----------
“Yes, sir. I am attempting to fill a silent moment with non-relevant conversation.”
“You don’t tell me how to behave; you’re not my mother!”
“Could you please continue the petty bickering? I find it most intriguing.” – Data (Star Trek: The Next Generation)

2020-07-09 22:47:53

In Python, threading will not help you if it's a compute-intensive task because of the global interpreter lock, which prevents parallelization.  Better to reach for Cython before threading, at which point you probably don't need threading anymore.  In general threads are not actually advantageous to games because the nature of games is that things happen before other things, and the order of events matters.

I don't have good resources because it's been too long, but in general threading is very much a here there be dragons sort of thing that's best avoided if at all possible, unless you're using some sort of abstraction library (i.e. Rust's Rayon, Intel TBB, etc).  In general I would strongly suggest that any new programmer (and you count, by my standards) stay away from it unless there is no choice.  To get some idea, google data race and deadlock.  You will discover that most of the hello world of threading is esoteric, because it's only in the world of academia that we can construct problems where you can cleanly explore threading.  You'll find yourself saying things like "But surely I can just put a lock on everything!", and then discovering, months down the road, that you couldn't because they're expensive, or that part of the engine isn't architected right for concurrency but was architected right enough that it seemed like a good idea at the time.  Most threading bugs can't be duplicated on demand, and you're often lucky if it will even happen on your machine at all.  I have literally written scripts that start the program 100+ times and do something to it, so that I could hit it and wait a couple minutes to get the program into the bugged state and figure out why it's broken, and I will almost certainly do so again.  Put simply, threads are a project killer, and learning to use them in anger will kill the first project you try to use them in (this is why camlorn_audio stopped being a thing,).

And even if you get a working implementation, it's often slower than if you hadn't, due to overhead of synchronization.  Which is part of what would have had to be fixed in Libaudioverse if I was continuing that, and why most people who used it ended up turning off threads--you could run 1000 sources with it fine, but for 10 or 20 it was 2x or more slower than just not having parallelism.

But that said, that's not a helpful answer.  So first, you have 1 of 2 kinds of problems that are making you ask.

The first kind of problem is "I want to simulate 10000 objects".  You can solve this with better algorithms (i.e. spatial hashes) for which libraries already exist, cython (which compiles your Python to C and gives you around 100x or more times the performance for this kind of thing), and increasing the time between game ticks (basically tick 10 times instead of 20, but move objects twice as far).  Python multithreading doesn't get you compute-level parallelism, if you want that you need to use the multiprocessing module (which doesn't give you shared state, i.e. no way for all the threads to see the same game) or Numba/TensorFlow/Theano/etc (which are easy to use but don't redistribute well if at all).

The second kind of problem is that you want to do networking or something and it's too slow.  You solve networking by using async networking libraries or with threads, which I'll get to in a second.  Pick up asyncio or something.  If it's file I/O, you do less file I/O during the game loop (i.e. don't try to save everything after every tick).  File I/O is a lost cause even with threads involved.  Fortunately if you do need threads for this Python threads are fine.

If you do use threads, the safest way is to think of threads like entirely separate programs.  Then you use Python's Queue module to communicate between them.  Alternatively find a parallel mapreduce implementation (and learn what mapreduce is).  Multiprocessing.dummy in Python's standard library can be used for mapreduce, because it offers the mutliprocessing API on threads.

But, to be honest, you're very far from the first person to worry about performance first, and I can't think of a single case in audiogame history where the answer was "you should use threads".  it's almost always "did you know about this faster algorithm" and that's the end of it.  Start by having the problem before you try to solve it, because what you're talking about getting into gives career programmers with 5+ years experience nightmares, and it's literally easier to go use Rust or C++ or something than to try threads.  You won't need to switch languages either.  But *if* you can't make Python fast enough for what you're contemplating and if Cython was somehow not enough, it would be easier to switch to C/C++ for your problem than to use threads (and for everyone who is going to say "but java or C#": yes, but threads are hard in Java or C# too, and if Cython isn't fast enough neither of those will be either).  Start by telling us what the actual problem is, and by writing a benchmark of what you propose to do.

In general I'm not against people knowing about threads, indeed part of my job involves them in great detail and it's the kind of thing that gets you into Google, but I would honestly say it took me multiple years to be able to use them without screwing up, significantly longer than almost anything else I've learned to do, so if your goal is to get something finished don't use them, or if you have to limit where you have to use them as much as possible so that your mistakes don't kill everything forever.

My Blog
Twitter: @ajhicks1992

2020-07-09 22:53:57

Also, one followup to that. If this is going to run on people's laptops (i.e. isn't a server), you can only count on one core for the simulation, which means even in languages better at this than Python, you don't really get any extra compute time.  This is because many people only have 2 cores (but advertised as 4 because hyperthreading), and the system is going to use some of that.  So you maybe get 1.3 or 1.5 cores on a good day but then haha oops today we've got youtube open, or something is working on downloading an update and that goes down to 1 or less, at which point you're paying for the overhead for threading but getting none of the gains, and it's a net loss.

Networking and other I/O stuff is different because the threads doing it spend most of their time in a special waiting state where they basically say "Hey OS, wake me up when some bytes arrive" or whatever, which is why stuff that runs on laptops can benefit from threading and why threads are useful in Python even with the GIL blocking parallel compute, but any project which needs it for compute on a user's machine is dead on arrival especially in blindness circles where much of the hardware is cheapest laptop on the market 5 years ago.

Anyway, here ends the walls of text. Hopefully it was at least a bit helpful.

My Blog
Twitter: @ajhicks1992

2020-07-09 23:22:06 (edited by redfox 2020-07-09 23:22:35)

Thank you. As I said, I wasn't very experienced in things of that nature, and I also don't know too much about computer performance and hardware in general. I'll look into Cython and other such things as per your responses.

----------
“Yes, sir. I am attempting to fill a silent moment with non-relevant conversation.”
“You don’t tell me how to behave; you’re not my mother!”
“Could you please continue the petty bickering? I find it most intriguing.” – Data (Star Trek: The Next Generation)

2020-07-09 23:37:07

here's a brief motivating example of why you shouldn't.  What happens if we run f in 5 threads?:

import random
import time

counter = 0

def f():
    global counter
    for i in range(1000):
        old = counter
        time.sleep(random.random())
        counter = old + 1

The answer isn't "the counter is set to 5000".  The answer (in Python) is that the counter is set to any number between 1 and 5000 depending on luck.  You can protect this with a mutex:

lock = threading.mutex()
...

def f():
    global counter, lock
    for i in range(1000):
        with lock:
            old = counter
            time.sleep(random.random())
            counter = old + 1

That may not be exactly right, I typed it off memory.  But the answer is now the counter gets to 5000 but takes as much time as not using threads to do so, or more because mutexes are expensive.

And, surely this is a contrived example?  Well, no. x += 1 is actually:

old = x
x = old + 1

in most programming languages, which effectively puts a small sleep between them (because they're separate operations).  In Python with integer variables it's not and will work "as you'd expect", but that's just an accident and as soon as you involve multiple variables you're back to the same problem.  And in C/C++/anything else you have a whole other class of problems to do with reading values that are only half written to memory, and things like that.

I'll go further but only if there's need.  Anything else gets really complicated to explain, with respect to the dangers.  But just imagine for example what happens if two threads try to update the same object's position at once--in Python a simple "move object in 2d plane by this velocity" running concurrently on the same object has at least 8 results, I think much more, and only one is the one you actually wanted.

My Blog
Twitter: @ajhicks1992