Page 1 of 1

Is Micropython inherently multithreaded?

Posted: Fri Mar 26, 2021 3:42 am
by hwiguna
After much hair pulling, I was able to get my Mandelbrot calculation run on two threads (core0 as usual, plus core1 via _thread.start_new_thread().

However, it does not run to be twice as fast or anywhere near that.
Is MicroPython inherently already multithreaded and I'm just inefficiently trying to do what it already does?
Thx!

Re: Is Micropython inherently multithreaded?

Posted: Fri Mar 26, 2021 7:21 am
by stijn
No MicroPython is singlethreaded by default. As in, the interpreter won't automatically distribute calculations over multiple threads (assuming that is what you are asking).
it does not run to be twice as fast or anywhere near that
That is not surprising. Expecting x threads to lead to execution time being divided by x is incorrect. There are so many other things which need to be considered, and writing multithreaded code correctly is also pretty hard, that most 'naive' implementation will not speed things up.

Re: Is Micropython inherently multithreaded?

Posted: Fri Mar 26, 2021 8:19 am
by pythoncoder
My understanding is that this is generally correct but is incorrect on the Pico.

This platform supports a single thread which runs on the other core, with its own exclusive interpreter. However running a heavy duty calculation on the other core won't achieve anything if the main program just spins its wheels waiting for a result. To speed things up you need to do half the calculation on one core and the other half on the other. Shouldn't be too hard with Mandelbrot.

Re: Is Micropython inherently multithreaded?

Posted: Fri Mar 26, 2021 12:34 pm
by hwiguna
Thank you for the replies everyone!
If you're interested, here is the dual core version:
https://github.com/hwiguna/HariFun_202_ ... ualCore.py

It works, but it is only marginally faster than the single core version:
https://github.com/hwiguna/HariFun_202_ ... rotQuik.py

I tried to distribute the mandelbrot calculation equally between the two cores by going though the X axis in steps of 2.
All Y for X is calculated by the thread, and all Y for X+1 is calculated by main thread.

Functions of interest are:
Loop() where I call DrawMandelbrotX() and then wait for user input.
DrawMandelbrotX() where I spin a thread to compute all Y pixels for column X, and also compute all Y pixels for column X+1 then loop to X+2
mandelbrotThreadX() is basically the same calculation as in DrawMandelbrotX() but stores the pixel results in an array of booleans.
When done I set a global variable resultsReady to True.

Back in DrawMandelbrotX() I wait for resultsReady to become true, then plot the results pixel array on the actual screen.

Do you see how we could improve this code for better multi core performance? Thanks!!!

Code: Select all

def mandelbrotThreadX(x):
    global MAX_ITER
    global WIDTH, HEIGHT, realStart, realEnd, imStart, imEnd
    global results, resultsReady
    #print("Thread Begin x=",x)
    xx = realStart + (x / WIDTH) * (realEnd - realStart)
#     for y in range(HEIGHT):
#         results[y]=False
    for y in range(HEIGHT):
        yy = imStart + (y / HEIGHT) * (imEnd - imStart)
        c = complex(xx, yy) # Convert pixel coordinate to complex number
        m = mandelbrot(c)   # Compute the number of iterations
        color = 1 - int(m/MAX_ITER)
        results[y] = color>0
    resultsReady = True
    #print("Thread Done x", x)
    _thread.exit() # when done, commit suicide so we could be re-incarnated for next X.

def DrawMandelbrotX():
    global oled, brotFB, cursorFB, isHiRez, nextRefresh, MAX_ITER
    global results, resultsReady
    print("DRAWING:", realStart, realEnd, imStart, imEnd)
    stopWatch = time.ticks_ms()
    RE_START = realStart
    RE_END = realEnd
    IM_START = imStart
    IM_END = imEnd

    brotFB.fill(0)
    
    for x in range(0, WIDTH,2): # We're drawing two columns at a time. One by the thread, the other by main.
        resultsReady=False # Will be set by thread to True when it's done computing column.
        _thread.start_new_thread(mandelbrotThreadX,(x,))
        
        x1 = x+1
        #print("Main begin x1=",x1)
        xx = RE_START + (x1 / WIDTH) * (RE_END - RE_START)
        for y in range(0, HEIGHT, 1):
            yy = IM_START + (y / HEIGHT) * (IM_END - IM_START)
            c = complex(xx, yy) # Convert pixel coordinate to complex number
            m = mandelbrot(c)   # Compute the number of iterations
            color = 1 - int(m/MAX_ITER)
            brotFB.pixel(x1,y, 1 if color>0 else 0) # Plot the point
        #print("Main End x1=",x1)
                   
        #stopwatchStart = time.ticks_ms()
        while not resultsReady:
            pass
        #print("waited ", time.ticks_ms()-stopwatchStart, "ms")
        
        # Plot the X column computed by the thread
        for y in range(HEIGHT):
            brotFB.pixel(x,y, 1 if results[y] else 0)

        if x % 6 == 0: # No need to refresh everytime we go through X loop.
            oled.blit(brotFB,0,0)
            oled.show()

    oled.blit(brotFB,0,0)
    oled.show()

Re: Is Micropython inherently multithreaded?

Posted: Sat Mar 27, 2021 10:09 am
by pythoncoder
Optimisation is an involved process. A first step is to read this doc and this one to learn about MicroPython specific optimisations. There are a lot of ways to improve performance.

After fixing design issues a key part of the process is determining where the time is being used up, and concentrating your efforts on optimising that part of the code. Timings should be measured with

Code: Select all

from time import ticks_us, ticks_diff  # Not by subtraction
# code omitted
start = ticks_us()
# Section of code to be measured
print(ticks_diff(ticks_us(), start))
I haven't used threading on the Pico, but it is possible that the overhead in starting and stopping a thread is significant. It may be more efficient to run the thread for the entire duration of computing half of the set, rather than repeatedly starting and stopping it. But that's a guess: you'll need to measure it.

Re: Is Micropython inherently multithreaded?

Posted: Sat Mar 27, 2021 12:51 pm
by hwiguna
pythoncoder wrote:
Sat Mar 27, 2021 10:09 am
Optimisation is an involved process. A first step is to read this doc and this one to learn about MicroPython specific optimisations. There are a lot of ways to improve performance.
Peter, I've just scanned those two resources. They're excellent!
I will digest them in detail, apply, and time their performance.
Thank you!!!

Re: Is Micropython inherently multithreaded?

Posted: Sun Mar 28, 2021 9:20 am
by pythoncoder
A point to consider is that the Pico does not support hardware floating point. It will therefore be substantially slower than platforms such as the Pyboards. I believe there is an integer algorithm for Mandelbrot. There is, or used to be, a PC program called fractint.

Don't ask me how this is done. To this "bear of little brain" the whole concept seems rooted in the notion of complex numbers of widely ranging magnitudes.

Re: Is Micropython inherently multithreaded?

Posted: Mon Mar 29, 2021 2:55 am
by jbeale
pythoncoder wrote:
Sun Mar 28, 2021 9:20 am
A point to consider is that the Pico does not support hardware floating point. It will therefore be substantially slower than platforms such as the Pyboards. I believe there is an integer algorithm for Mandelbrot. There is, or used to be, a PC program called fractint.
It's cool how some things from long ago turn out to still be around:
News: 28 November 2020 - Released DOSBox/Fractint package to allow running on Windows
https://www.fractint.org/