Multi-threading gone wrong on Kivy...

247 views
Skip to first unread message

David Murphy

unread,
Jul 4, 2020, 6:59:36 AM7/4/20
to Kivy users support
I've dug myself into a bit of a hole here and I present you with a tale of woe.  I had a pretty much completed Kivy app and the core screen displays text but in doing so it inserts a lot of mark up which involves looking up and appending html dictionaries.  Worked fine but it was very CPU intensive and slower than I'd have liked.  

So Mr Genius here decides to multi-process. Rather than do a basic implementation to test how this works is Kivy like a normal person he completely redesigns his programme's flow to divvy the heavy lifting between the available cores.Then he confidently jumps to runtime and finds to his horror that  each core has spawned a new window... Imagine the sinking feeling when this muppet realises that import itself spawns a window! What followed is a horrendous PEP8 defacing hack where Mr Gone-Beyond-The-Point-Of-No-Return takes the called function completely out of scope like this:

def func():
    return stuff

if __name__ == '__main__':
    multiprocessing.freeze_support()

    import Kivy

Yup, the whole app is now indented. I'm embarrassed to share this but this is the mess I'm left with. Ok, but at least it returns the values and doesn't spawn multiple windows. Except the damn thing is completely out of scope.I want to look up dictionaries, call my objects but with:

with concurrent.futures.ProcessPoolExecutor() as exe:
    next_batch = self.final_contents[layout.lines:layout.lines + cores]
    lines = exe.map(func, next_batch)

All I can do is pass an iterator.  I've been sat here thinking how I can maybe bundle my beautiful soup dictionary object and all the other values into a list and pass it with the iterable and then I can return the values and pick them up within the Kivy app... Or hell, I guess I could use global variables. My poor PEP8 complaint programme is turning into an ugly mess...

Should I just cut my losses and give up on multiprocessing in Kivy?  Or does anyone have a suggestion?

David Murphy

unread,
Jul 4, 2020, 8:24:27 AM7/4/20
to Kivy users support
You know what, why don't I just move over to Linux and carry on the development there? I'll have to do that to make an apk anyway and iOS and Android are the focus, not Windows. A pity to lose a platform but hacking apart elegant code like this is damaging my tranquillity 

Grr, just noticed I said "multi-threading" in the title. I'm officially an idiot.

Elliot Garbus

unread,
Jul 4, 2020, 8:31:19 AM7/4/20
to kivy-...@googlegroups.com

It sounds like you have already figured most of this out the hard way, here is a small multiprocessing app I wrote.  You approach is correct and that crazy indenting is required.  https://github.com/ElliotGarbus/MidiClockGenerator

 

Can you say more about what computation is going on in each of the processes?  Is this a candidate for threading – that would remove the data sharing challenges.  Perhaps there are some other algorithm choices?

 

Python 3.8 introduces a package for shared memory multiprocessing… I have not used it, and then you would have the complexity of building your own kivy distribution.

 

Global variables will not solve your problem.  Each process has it’s own address space. 

You could use the filesystem to pass data.

--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/d305c2fe-7261-4c0b-8238-75550b257653n%40googlegroups.com.

 

Elliot Garbus

unread,
Jul 4, 2020, 8:37:22 AM7/4/20
to kivy-...@googlegroups.com

I don’t think Jumping over to Linux will not solve your problem.  You still need to isolate your kivy process from your compute processes.  This requires the uncomfortable indenting.

 

Have you explored queues or pipes to share data?

 

From: David Murphy
Sent: Saturday, July 4, 2020 5:24 AM
To: Kivy users support

--

You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.

David Murphy

unread,
Jul 4, 2020, 11:34:43 AM7/4/20
to Kivy users support
Thanks for the response and sharing the code, I'll study it properly over the weekend.

Simple stuff: pure CPU looping messing with string objs, inserting words from dict beautiful soup obj, adding mark up depending on branching so clock speed is the limiting factor with no downtime. I designed it for that to be the case, lots of branching with .join() concatenation. So an ideal multiprocessing candidate as everything was long pre-loaded from file and the save to file happens after completion when there's no rush. I minimised the size of the dict_obj too prior to run. There are also text insertions from and to this xml dict obj and these insertions are decided in the loop that I'm wanting to multiprocess which is separate from the text output. The multiprocessing is done on batches of lines of text and they're returned in order with map method, appended to a list and published.  

There's also a secondary process where I track the coordinates of a markup anchor to manage the total page contents. I don't want to multiprocess that because there'd be conflicts and it's not where the holdup is. So if you flip the tablet the page fills correctly, building on or taking away from the existing content to minimise CPU load.

I implemented threading already on the image loading, no issues at all but that's an ideal I/O threading candidate as well as on the screenmanager screen transition so we're not waiting around. Worked a treat, got a good result.

What I need to focus on is that the CPU work must be done regardless. If I add glacial I/O loading and saving into each iteration then I'd definitively be better off with one core. So queues/pipes with a single pass to the new environment might be the answer. Excellent suggestion. I'll need to think about this and do some documentation reading.

Your 3.8 suggestion is an interesting one, I should look into that too, I'd prefer not to make my own distribution if I'm honest.

Thanks again, super helpful suggestions.

To unsubscribe from this group and stop receiving emails from it, send an email to kivy-...@googlegroups.com.

Robert Flatt

unread,
Jul 4, 2020, 8:28:40 PM7/4/20
to Kivy users support
Python threads (but not processes) are not the same on a desktop and Android (iOS I don't know)

On the desktop Python threads are tasks all executed on one core, but the OS is multi tasking, so there is programming concurrency but limited performance improvement. In most cases there is probably not a cost to using join(), and this simplfies the programming. The reason is the Python Global Interpreter Lock.

On Android threads are assigned to a core up to the number of available cores. So there can be significant performance improvement, which can be removed by using join(). The results from any thread must be handled in a thread safe way as thread execution is asynchronous. So sync a worker theread result to the Kivy UI thread using Clock.schedule_once(). The reason is, underneath is the JVM.

Network I/O should always be on a different thread else the UI may hang on an OS that is not multitasking (process is OK too, but there will be a lot of overhead for something that just sits and waits). For the same reason join() some thread to the UI thread may freeze the UI on a 'not a desktop', multitasking will hide this and lull you into a false sense of security ;)

Android already uses Python 3.8  and kivy==2.0.0rc3 installs and runs on Python 3.8

Elliot Garbus

unread,
Jul 4, 2020, 8:43:50 PM7/4/20
to kivy-...@googlegroups.com

Robert,

Just out of curiosity, can you explain how Python threads work on Android?  What is the underlying mechanism? How do they get around the GIL?  

To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/9b3f5537-8cbf-4ff9-a4b5-577e77cea160o%40googlegroups.com.

 

Robert Flatt

unread,
Jul 4, 2020, 10:44:59 PM7/4/20
to Kivy users support
On Android Python uses Java Virtual Machine not the Python Interpreter. As I understand it (with not too much detail) the JVM explicitly handles threads (Java has real threads) in part because there is no global lock. Again, as I understand it, there was a historical reason for using the JVM, because of linker limitations on a native applications on Android; I think these limitations no longer apply. Sorry thats all I (think) I know.

Personally I think that threads thing work 'right' are a great thing. Your average dweeb hardly notices, as requests are implictly threaded (but must be used asynchronously). 

Elliot Garbus

unread,
Jul 4, 2020, 11:13:44 PM7/4/20
to kivy-...@googlegroups.com

Thanks for the response.

It is a little mind bending to think that an environment that does not have true multi-threading support, gets real multi-threading by changing an underlying runtime component …and the code still runs correctly.  That is really impressive.

To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/8ee239e9-4c95-40ee-bcdf-1719590df8fco%40googlegroups.com.

 

Robert Flatt

unread,
Jul 4, 2020, 11:20:28 PM7/4/20
to Kivy users support
To complete the story, a few words on processes:

As an alternative to threads one could uses processes. A thread is a lightweight process. In this case a process has its own copy of Python, a thread shares the original Python instance. Which is why all memory is not shared between processes, but memory is shared between threads.

On the desktop and on Android a process is executed on any available core. The cost of executing a process is in memory usage - a new copy of Python for each process.

On the desktop with lots of physical memory and more importantly virtual memory, this is not too much of a deal. But it is good practice not concurrently execute a lot more processes than cores becase the app will page its self into stagnation. 

On Android there is no virtual memory, start lots of processes concurrently and app memory usage gets significant. At this point Android will terminate other running apps, which is why no app is guaranteed to return from on_pause(). Eventually your app hits the hard physical memory limit and dies.

These machines have finite resources, write a challenging enough app and you will need to understand how to finess these limitations.


On Saturday, July 4, 2020 at 2:43:50 PM UTC-10, Elliot Garbus wrote:

Robert Flatt

unread,
Jul 4, 2020, 11:34:03 PM7/4/20
to Kivy users support
It is a little mind bending...

Yes indeed. No need for 'better living through chemistry', computing has a similar effect. :)

But just as porting C code between OSes will find issues that were there but didn't manifest, porting code between different thread models will find issues that were there but didn't manifest. Practice thread safe computing.  ;)  

David Murphy

unread,
Jul 6, 2020, 8:21:47 AM7/6/20
to Kivy users support
This little exchange was fascinating and enlightening, much to bear in mind. Also, I discovered the problem with my code: I'd be staring at it too long without a break. Liveware problem.
Reply all
Reply to author
Forward
0 new messages