<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Threading
2018-01-07
<br>
Basics
A thread is the smallest unit that computer can schedule to run.
One process can contain multiple threads.
These threads share the memory and the state of the process.
    i.e. variables and commands are shared.
Threads are run by scheduling or time slicing in a single processor machine.
Why threading?
A computer with multiple CPUs can run programs faster 
Given a process, threads share the memory of global variables; change in variable reflects for all threads. Threads can have a local variable.
<img src="https
Step1:  Example 1
Step2:  Example 2
Step3:  Example 3
Step4:  Example 4
Step5:  Example 5
Step6:  Example 6
Step7:  Example 7
<ASSISTANT_TASK:>
Python Code:
import threading
from _thread import start_new_thread, allocate_lock
import logging
import time
import numpy as np
Explanation: Threading
2018-01-07
<br>
Basics
A thread is the smallest unit that computer can schedule to run.
One process can contain multiple threads.
These threads share the memory and the state of the process.
    i.e. variables and commands are shared.
Threads are run by scheduling or time slicing in a single processor machine.
Why threading?
A computer with multiple CPUs can run programs faster 
Given a process, threads share the memory of global variables; change in variable reflects for all threads. Threads can have a local variable.
<img src="https://www.python-course.eu/images/threads_400.png">    
Notes
For Python 3, use threading. _thread is also compatible.
threading treats a thread as an object (OOP).
_thread treats a thread as a function.
ref:
https://www.python-course.eu/threads.php
https://pymotw.com/2/threading/
End of explanation
def worker1():
    print(threading.currentThread().getName(), '--begin')
    time.sleep(3)
    print(threading.currentThread().getName(), '--end')
    
def worker2():
    print(threading.currentThread().getName(), '--BEGIN')
    time.sleep(2)
    print(threading.currentThread().getName(), '--END')
    
w1 = threading.Thread(name='firstWorker', target=worker1)
w2 = threading.Thread(name='secondWorker', target=worker2)
w3 = threading.Thread(name='thirdWorker', target=worker1)
w4 = threading.Thread(target=worker2)
w1.start()
w2.start()
w3.start()
w4.start()
# threads run asynchronously and look messy in the results.
# So, logging module can help tracking threads.
Explanation: Example 1: run multiple threads
End of explanation
logging.basicConfig(level=logging.DEBUG,
                   format='[%(levelname)s] (%(threadName)-10s) %(message)s')
def worker1():
    logging.debug('--begin') # <-- use logging instead of print function!
    time.sleep(3)
    logging.debug('--end')
    
def worker2():
    logging.debug('--BEGIN')
    time.sleep(2)
    logging.debug('--END')
    
w1 = threading.Thread(name='firstWorker', target=worker1)
w2 = threading.Thread(name='secondWorker', target=worker2)
w3 = threading.Thread(name='thirdWorker', target=worker1)
w4 = threading.Thread(target=worker2)
w1.start()
w2.start()
w3.start()
w4.start()
Explanation: Example 2: use logging for tracking threads
End of explanation
num_threads = 0
def elephant(a):
    global num_threads
    num_threads += 1
    time.sleep(0.1)
    num_threads -= 1
    print(num_threads)
start_new_thread(elephant,(99,))
start_new_thread(elephant,(999,))
start_new_thread(elephant,(1733,))
start_new_thread(elephant,(17334,))
if num_threads == 0:
    print('num_threads is 0')
else:
    print('num_threads is not 0')
# The output is strange and it also changes every time you run.
# That's because the global variable num_threads are accessed and changed simultaneously!
# Using locking method can help salvage this situation.
Explanation: Example 3: threads without locking
End of explanation
num_threads = 0
lock = allocate_lock() # make a lock object
def elephant(a):
    global num_threads
    lock.acquire() # start locking
    num_threads += 1 
    time.sleep(0.1)
    num_threads -= 1
    lock.release() # releasing locking
    print(num_threads)
start_new_thread(elephant,(99,))
start_new_thread(elephant,(999,))
start_new_thread(elephant,(1733,))
start_new_thread(elephant,(17334,))
if num_threads == 0:
    print('num_threads is 0')
else:
    print('num_threads is not 0')
Explanation: Example 4: threads with locking
End of explanation
# Daemon threads die when the main program dies.
# Non-daemon threads do not die until the work is done.
# NB. Default is non-daemon mode.
logging.basicConfig(level=logging.DEBUG,
                    format='(%(threadName)-10s) %(message)s')
def daemon():
    logging.debug('Start')
    time.sleep(3)
    logging.debug('Done')
    
d = threading.Thread(name='Daemon', target=daemon)
d.setDaemon(True)
def non_daemon():
    logging.debug('Start')
    logging.debug('Done')
t = threading.Thread(name='Non-daemon', target=non_daemon)
d.start()
t.start()
# It's supposed not to show the last line "[DEBUG] (Daemon    ) Done"
# But, Jupyter Notebook seemingly runs each cell as a program.
# If you run in the command line as 
#   $ python test.py
# it will throw logs:
#    [DEBUG] (Daemon    ) Start
#    [DEBUG] (Non-daemon) Start
#    [DEBUG] (Non-daemon) Done
#
# This program exits before the daemon thread finishes.
#
# Adding below lines will make the program waits until daemon threads finish.
#
# d.join()
# t.join()
Explanation: Example 5: daemon vs. non-daemon threads
End of explanation
logging.basicConfig(level=logging.DEBUG,
                    format='(%(threadName)-10s) %(message)s')
class SimpleThread(threading.Thread):
    def run(self):
        logging.debug('Running now...')
        return
    
for i in range(4):
    t = SimpleThread()
    t.start()    
Explanation: Example 6: subclassing thread (simple)
End of explanation
# If you want to customize threads, 
# make instance attributes so that they can be seen.
logging.basicConfig(level=logging.DEBUG,
                    format='(%(threadName)-10s) %(message)s')
class AwesomeThreads(threading.Thread):
    def __init__(self, group=None, target=None, name=None,
                args=(), kwargs=None, daemon=None):
        # This redefining let you control threads and save values
        threading.Thread.__init__(self, group=group, target=target, 
                                  name=name, daemon=daemon)
        self.args = args
        self.kwargs = kwargs
        return
        
    def run(self):
        logging.debug('My args={}, My kwargs={}'.format(self.args, self.kwargs))
        return
for i in range(6):
    t = AwesomeThreads(args=(i,), kwargs={'하나':1, '둘':2})
    t.start()
Explanation: Example 7: subclassing thread (complex)
End of explanation
<END_TASK>