Search code examples
pythonlockingpython-multithreading

Console program checks if number is prime. Why does `threading.Lock()` cause it to fail for products of non-tiny primes?


I am reading the chapter about parallel programming in a Python book.
The following code is based on an example about the module threading.
The point of this particular example is the use of threading.Lock().
The code can also be found here (on GitHub). The code without the lock is here.

import threading


class PrimeThread(threading.Thread):

    lock = threading.Lock()

    def __init__(self, n):
        super().__init__()
        self.n = n

    def run(self):
        if self.n < 2:
            with PrimeThread.lock:
                print('▪ That is not an integer greater 1.')
            return
        i = 2
        while i ** 2 <= self.n:
            remainder = self.n % i
            quotient = self.n // i
            if remainder == 0:  # if n is divisible by i
                with PrimeThread.lock:
                    print(
                        f'▪ {self.n} is not prime, '
                        f'because it is {i} * {quotient}.'
                    )
                return
            i += 1
        with PrimeThread.lock:
            print(f'▪ {self.n} is prime.')


my_threads = []

user_input = input('▶ ')

while user_input != 'q':
    try:
        n = int(user_input)  # raises ValueError if string is not integer
        thread = PrimeThread(n)
        my_threads.append(thread)
        thread.start()
    except ValueError:
        with PrimeThread.lock:
            print('▪ That is not an integer.')

    with PrimeThread.lock:
        user_input = input('▶ ')

for t in my_threads:
    t.join()

The problem with the initial version was, that the output could interfere with the new input:

▶ 126821609265383
▶ 874496478251477▪ 126821609265383 is prime.

▶ ▪ 874496478251477 is not prime, because it is 23760017 * 36805381.

The lock is supposed to achieve, that it looks like this:

▶ 126821609265383
▶ 874496478251477
▪ 126821609265383 is prime.
▪ 874496478251477 is not prime, because it is 23760017 * 36805381.

Maybe it does achieve that. (Hard to test with inputs that allow a fast answer.)
But now the program does not return anything for many inputs with 7 or more digits.
4374553 (prime) sometimes works. But 76580839 (prime) and 67898329 (2953 · 22993) will fail.

How can I use the lock to prevent the mixing of input and output lines, and still get the result printed, when the calculation is finished?

More generally, I would like to ask, if the console is necessary to illustrate the point of threads and locks. Could those also be illustrated by just looping over a list of numbers?
(I should probably bring that up on CodeReview.)


Edit: I just realized, that the missing output can be triggered, by entering some easy input.
In this case the program was stuck after entering 76580839.
But entering 123 caused both answers to appear:

trick first attempt

But of course the missing answer can only be released with this trick, when it already exists. In this case the calculation for 126821609265383 was not yet finished, when 123 was entered, but a bit later it was released by entering 345. Surprisingly now the output for 345 is stuck:

trick second attempt

My question remains the same:
What needs to be changed, to make the program behave normally for non-easy inputs?
(The way it already works for easy inputs.)

As mentioned above: This is an example problem about threading.
The prime check is just a placeholder for a calculation that takes time and resources.


Solution

  • It's impossible to do what you want. When the lock is acquired in the main thread before the input() is called, a worker thread can't print a result because it can't acquire the lock. For a small integer, a worker thread can print a result because it acquires the lock before the main thread tries to acquire the lock. The current implementation of CPython gives a short time(5ms) to a newly created thread before a context switch occurs.

    You can experiment this feature by adding the following code at the beginning of your code.

    import sys
    sys.setswitchinterval(20) # In seconds