1. Start a thread

The Python standard library provides the threading module. To start a Thread, you pass a function to the Thread instance and call start() to run it

import threading import time def loop(): print(f'----------thread :{threading.current_thread().name} is running... ') n = 0 while n<5: n = n+1 print(f'thread: {threading.current_thread().name} n: {n}') time.sleep(1) print(f'----------thread: {threading.current_thread().name} ended') print(f'thread : {threading.current_thread().name}') t = threading.Thread(target=loop, name='LoopThread') t.start() t.join() print(f'thread : {threading.current_thread().name} ended')Copy the code

Output:

thread : MainThread
----------thread :LoopThread is running...
thread: LoopThread n: 1
thread: LoopThread n: 2
thread: LoopThread n: 3
thread: LoopThread n: 4
thread: LoopThread n: 5
----------thread: LoopThread ended
thread : MainThread ended
Copy the code

Any process starts a thread by default. We call this thread the MainThread, and the MainThread can start a new child thread, The name of the child thread can be specified at creation time, such as the “LoopThread” specified in the code. The current_thread() function in the threading module always returns an instance of the current thread

2. The concept of lock

Locking is the most basic synchronization mechanism provided by Python’s threading module. At any time, a lock object may be acquired by one thread, or not by any thread. If a thread attempts to acquire a lock object that has already been acquired by another thread, the thread attempting to acquire the lock object can only suspend execution until the lock object is released by another thread. Locks are commonly used to enable synchronous access to shared resources. Create a Lock object for each shared resource. When you need to access the resource, call acquire to acquire the Lock object (if another thread has already acquired the Lock, the current thread will have to wait for it to be released). When the resource is accessed, call Release to release the Lock

Lock = threading.lock () def run_thread(n): If lock.acquire() is executed by multiple threads at the same time, only one thread can acquire the lock successfully. The other threads continue to wait until they acquire the lock. Lock. Release (); otherwise, threads waiting for locks will wait forever and become dead.Copy the code

In Python 2.5 and later, you can use the with statement. When a lock is used, the with statement automatically acquires the lock object before entering the block and releases the lock when the block is completed:

import threading

lock = threading.Lock()

def run_thread(n):
    with lock:
        pass
Copy the code

The acquire method comes with an optional wait flag, which can be used to set whether or not the lock is blocked when another thread holds it. If you set it to False, acquire will no longer block, although sometimes it will return False if the lock is occupied.

  • Acquire (blocking=True, timeout=-1) : Access (blocking=True, timeout=-1) : access (blocking=True, timeout=-1) : access (blocking=True, timeout=-1) : access (blocking=True, timeout=-1) : access (blocking=True, timeout=-1) : access (blocking=True, timeout=-1); When one thread acquires a lock, other threads attempting to acquire the lock block until the lock is released. Timeout defaults to -1, which blocks indefinitely until the lock is acquired. If set to any other value (a float in seconds), timeout will block for up to the number of seconds specified by timeout. When blocking is False, the timeout argument is ignored, that is, no lock is acquired and no block is performed.
  • Release () : Releases a lock and changes its state to “unlocked”. Note that any thread can release the lock, not just the one that acquired it (because the lock doesn’t belong to a particular thread). The release() method can only be called when the lock is in the “locked” state; if it is in the “unlocked” state, RuntimeError is reported.

The Locked method can be used to check whether a lock object has been acquired. Note that it cannot be used to determine whether a acquire call will block, because the lock may be held by another thread between the time locked calls complete and the execution of the next statement, such as Acquire.

Import threading lock = threading.lock () if not lock.locked(): #: Other threads may acquire the lock before the next statement is executedCopy the code

3. Disadvantages of simple locks

A standard lock object does not care which thread currently holds the lock; If the lock is already held, any other thread attempting to acquire the lock will block, even if the thread is holding the lock:

lock = threading.Lock() def get_first_part(): lock.acquire() try: ... Finally: lock.release() return data def get_second_part(): lock.acquire() try:... finally: lock.release() return data def get_second_part(): Lock.acquire () try:... Get the second part of data from the shared object finally: Lock. release() return dataCopy the code

In our example, we have a shared resource and two functions that take the first and second parts of the shared resource. Both accessors use locks to ensure that no other threads modify the corresponding shared data while it is being fetched. Now, if we wanted to add a third function to get two parts of the data, we would be bogged down. An easy way to do this is to call both functions in turn and return the combined result:

def get_both_parts():
    first = get_first_part()
    seconde = get_second_part()
    return first, second
Copy the code

The problem here is that if a thread changes the shared resource between two function calls, we end up with inconsistent data. The obvious solution is to use lock in this function as well:

    def get_both_parts():
        lock.acquire()
        try:
            first = get_first_part()
            seconde = get_second_part()
        finally:
            lock.release()
        return first, second
Copy the code

However, this is not feasible. The two accessors inside will block because the outer statement already owns the lock. To solve this problem, you can make the outer statement release the lock by using tags in the access function, but this can easily get out of control and lead to errors. Fortunately, the threading module includes a more useful lock implementation: the Re-entrant lock.

4.Re-Entrant Locks (RLock)

The RLock class is another version of the simple lock. It blocks only when the same lock object is acquired by another thread. Simple locks can only be held once in the same thread. If the current thread already owns an RLock object, the current thread can acquire it again.

import threading lock = threading.RLock() lock.acquire() lock.acquire() #: Print (222222222) lock = threading.lock () lock.acquire() lock.acquire() #: print(1111111111)Copy the code

Output:

222222222
Copy the code

The main purpose of RLock is to solve the problem of nested access to shared resources, as described in the previous example. To solve the problem in the previous example, we just need to change Lock to an RLock object, and the nested calls will work.

lock = threading.RLock()


def get_first_part():
    ... see above


def get_second_part():
    ... see above


def get_both_parts():
    ... see above
Copy the code

This allows both parts of the data to be accessed separately or at once without being blocked by a lock or obtaining inconsistent data.

Note that RLock tracks recursion levels, so remember to release after Acquire.

5. Semaphores concept /Semaphores

Semaphores are a more advanced locking mechanism. A semaphore has a counter inside it, unlike a lock object that has a lock identifier inside it, and a thread blocks only if the number of threads occupying the semaphore exceeds the number. This allows multiple threads to access the same code area simultaneously.

Semaphore = threading. BoundedSemaphore () semaphore. Acquire (#) : reduce the counter... Semaphore.release () #: counter increasesCopy the code

When the semaphore is captured, the counter decreases; When the semaphore is released, the counter increases. When a semaphore is fetched, the process will block if the counter value is 0. When a semaphore is released and the value of counter increases to 1, one of the blocked threads (if any) will continue to run. Semaphores are often used to restrict access to resources of limited capacity, such as a network connection or a database server. In such scenarios, just initialize the counter to the maximum value and the semaphore implementation will do the rest for you.

max_connections = 10
semaphore = threading.BoundedSemaphore(max_connections)
Copy the code

If you pass no initialization parameters, the value of the counter will be initialized to 1. Python’s threading module provides two semaphore implementations. The Semaphore class provides an unlimited amount of Semaphore, and you can call Release any number of times to increase the counter. To avoid errors, it’s best to use BoundedSemaphore, which will alert you when you call Release more times than acquire.

6

An event is a simple synchronization object represented by an internal flag that a thread waits for to be set by another thread, or sets and clears itself.

Event.wait () #: Set or clear flag event.set() event.clear()Copy the code

Once the flag is set, the wait method does nothing (does not block), and when the flag is cleared, wait is blocked until it is reset. Any number of threads may wait for the same event.

7. Conditions /Conditions

The condition is an advanced version of the event object. A condition is a state change in a program, and a thread can wait for a given condition or a signal that the condition has occurred. Here is a simple producer/consumer example. First you need to create a condition object:

Condition = threading.condition ()Copy the code

The producer thread needs to obtain conditions before notifying the consumer thread of a newly generated resource:

#: Producer thread... Condition. Acquire ()... Condition.notify () #: signal that a resource is available condition.release()Copy the code

The consumer must get the condition (and the associated lock) and then try to get the resource item from the resource:

Condition. Acquire () while True:... If item: break condition.wait() #: hibernate until there is a new resource condition.release()... Processing resourcesCopy the code

Wait releases the lock, and then blocks the current thread until another thread calls notify or notifyAll on the same condition, and then retrieves the lock again. If multiple threads are waiting, notify wakes up only one thread, while notifyAll wakes up all threads. To avoid blocking in the wait method, you can pass in a timeout argument, a floating point number in seconds. If timeout is set, wait returns at the specified time, even if notify is not called. Once a timeout is used, you must examine the resource to determine what happened. Note that the condition object is associated with a lock that you must obtain before accessing the condition; Again, you must release the lock when you have finished accessing the condition. In production code, you should use try-finally or with. Conditions can be associated with an existing lock by taking the lock object as an argument to the condition constructor. This allows multiple conditions to share a single resource:

lock = threading.RLock()
condition_1 = threading.Condition(lock)
condition_2 = threading.Condition(lock)
Copy the code

8. The concept of ThreadLocal

In multi-threaded environments, each thread can use the global variable of its own process. If one thread makes a change to a global variable, it affects all other threads. To avoid multiple threads modifying variables at the same time, thread synchronization is introduced to control access to global variables through mutex, conditional variables, or read/write locks. Global variables alone are not sufficient for multithreaded environments, and many times threads need to have their own private data, which is not visible to other threads. Therefore, local variables can also be used in a thread. Local variables can only be accessed by the thread itself, but not by other threads in the same process. Sometimes using local variables is inconvenient, so Python also provides a ThreadLocal variable, which is itself a global variable, but can be used by each thread to hold its own private data, which is also invisible to other threads, truly isolating data between threads.

import threading

global_data = threading.local()

def thread_call():
    global_data.num = 0
    for _ in range(1000):
        global_data.num +=1
    print(f'thread : {threading.current_thread().name}, {global_data.num}')
    
threads = []
...
Copy the code

Each thread can obtain its own data from global_data.num, and each thread can read different Global_data, which is truly isolated from each other