Let’s talk about what is a GIL lock?

1. Definition of GIL

GIL (Global Interpreter Lock) is a mechanism in the CPython interpreter to ensure that only one thread can execute Python bytecode at the same time. The GIL is implemented by doing a mutex lock at the interpreter level, which means that at any given point in time, only one thread can execute Python bytecode and manipulate Python objects.

Python's GIL is a special lock. It is not a lock provided by the operating system, but a lock provided by the Python interpreter. When the Python interpreter creates a thread, it automatically creates a GIL associated with it. When multiple threads run simultaneously, only one thread can obtain the GIL and thus execute Python bytecode. Other threads must wait for the GIL to be released before they can execute. While this mechanism ensures thread safety, it also causes performance problems in Python multi-threaded programs.

It is important to note that the GIL only affects interpreter-level threads (also known as "internal threads") and not operating system-level threads (also known as "external threads"). That is to say, when using multiple operating system-level threads in a Python program, these threads can execute in parallel and are not affected by the GIL. However, internal threads created in the same interpreter are restricted by the GIL, and only one thread can run Python code.

It should be noted that although GIL is one of the performance issues of Python's multi-threaded programs, it does not mean that Python cannot use multi-threading. For I/O-intensive tasks, Python's multi-threading model can bring performance improvements. But for CPU-intensive tasks, using multi-threading does not improve performance, but may lead to performance degradation. At this time, you can consider using multi-process or asynchronous programming to improve performance.

2. Mechanism of action of GIL

GIL was introduced to solve the thread safety problem of the CPython interpreter. Since CPython's memory management is not thread-safe, if multiple threads execute Python bytecode at the same time, data races and memory errors may result. To solve this problem, the GIL was introduced and ensured that only one thread could execute Python bytecode at a time, thereby eliminating race conditions.

Specifically, the GIL prevents other threads from executing Python bytecode by acquiring and locking the global interpreter lock before executing Python bytecode. Once a thread acquires the GIL, it will monopolize the interpreter and release the GIL after executing a certain number of bytecodes or time slices, giving other threads the opportunity to acquire the GIL and execute the bytecodes. This process is repeated across multiple threads to achieve multi-threaded execution.

3. The impact of GIL on multi-threaded programming

3.1 CPU-intensive tasks will not receive true parallel acceleration

Since only one thread can execute Python bytecode at a time, for CPU-intensive tasks, multithreading cannot really achieve parallel acceleration. Even if multiple threads are used, only one thread can execute the bytecode, and the remaining threads are blocked by the GIL, which cannot fully utilize the computing power of the multi-core CPU.

import threading

def count_up():
    count = 0
    for _ in range(100000000):
        count += 1

t1 = threading.Thread(target=count_up)
t2 = threading.Thread(target=count_up)

t1.start()
t2.start()

t1.join()
t2.join()

In the above code, t1 and t2 execute the count_up function respectively, which performs 100 million self-increment operations. However, in the CPython interpreter, due to the existence of GIL, only one thread can actually perform the auto-increment operation, so multi-threading cannot speed up the execution time of this task.

3.2 I/O-intensive tasks can gain certain concurrency advantages

For I/O-intensive tasks, since the thread will release the GIL while waiting for the I/O operation to complete, multi-threading can play a certain concurrency advantage. In the process of waiting for I/O, other threads can obtain GIL and execute Python bytecode, thereby improving the execution efficiency of the overall program.

import threading
import requests

def fetch_url(url):
    response = requests.get(url)
    print(response.status_code)

urls = [
    'https://www.example1.com',
    'https://www.example2.com',
    'https://www.example3.com',
]

threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

In the above code, multiple threads initiate HTTP requests concurrently, and the GIL is released when waiting for the request to be completed. Therefore, CPU resources can be fully utilized and multiple network requests can be executed concurrently.

3.3 Data sharing between threads requires attention to synchronization

Due to the existence of GIL, multiple threads need to pay attention to the synchronization mechanism when accessing shared data at the same time to avoid data competition and inconsistency.

import threading

count = 0

def increment():
    global count
    for _ in range(100000):
        count += 1

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

t1.start()
t2.start()

t1.join()
t2.join()

print("Final count:", count)

In the above code, multiple threads perform the self-increment operation concurrently, which may cause a race condition due to the shared variable count. Due to the existence of GIL, only one thread can actually perform the increment operation, which may cause the final counting result to be incorrect.

To avoid this race condition, a thread lock (Lock) can be used for synchronization:

import threading

count = 0
lock = threading.Lock()

def increment():
    global count
    for _ in range(100000):
        lock.acquire()
        count += 1
        lock.release()

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

t1.start()
t2.start()

t1.join()
t2.join()

print("Final count:", count)

By introducing thread locks, it is ensured that only one thread can access and modify the shared variable count at a time, thereby avoiding race conditions and ultimately obtaining the correct counting result.

4. GIL guidelines

1. The current execution thread must hold GIL
2. When the thread encounters IO, the time slice expires, or encounters a blockage, the GIL will be released (Python 3.x uses a timer--after the execution time reaches the threshold, The current thread releases the GIL, or in Python 2.x, the tickets count reaches 100.)

5. Advantages and disadvantages of GIL

advantage:

Threads are not independent, so threads in the same process share data. When each thread accesses data resources, a "competition" state will occur, that is, the data may be occupied by multiple threads at the same time, causing data confusion. This is thread insecurity. Therefore, a mutex is introduced to ensure that a certain piece of key code and shared data can only be executed completely by one thread from beginning to end.
shortcoming:

In a single process, opening multiple threads cannot achieve parallelism, but can only achieve concurrency, sacrificing execution efficiency. Due to the limitations of GIL locks, multi-threading is not suitable for computing-intensive tasks and is more suitable for IO-intensive tasks. Common IO-intensive tasks: network IO (grabbing web page data), disk operations (reading and writing files), keyboard input

6. To avoid the impact of Python's GIL lock, you can consider the following methods:

  1. Use multiple processes. Python's multi-process model can avoid GIL restrictions, and multiple processes can execute Python code in parallel. However, communication and data sharing among multiple processes need to be realized through some additional means, such as pipes, shared memory, sockets, etc.
  2. Use third-party extension modules. Some third-party extension modules, such as NumPy, Pandas, etc., will use the underlying libraries written in C language when performing computationally intensive tasks, and these libraries will not be restricted by the GIL. Therefore, using these extension modules can improve the performance of Python programs.
  3. Use asynchronous programming. Asynchronous programming is a non-blocking programming model that can perform multiple tasks in a single thread, thereby avoiding the limitations of the GIL. There are many asynchronous programming frameworks for Python, such as asyncio, Tornado, Twisted, etc.
  4. Use multi-threading + process pool. Multithreading can be used to handle I/O-intensive tasks, and process pools can be used to handle computing-intensive tasks. Assigning multiple threads to different process pools can improve the processing speed of both I/O-intensive and compute-intensive tasks.

It should be noted that when using the above methods, the appropriate method should be selected according to the specific situation. For example, when handling a large number of I/O operations, using multiple processes may cause performance degradation because switching between processes is expensive. At this point, using asynchronous programming may be a better choice. When dealing with computationally intensive tasks, using multiple processes may be a better choice because the computationally intensive tasks can be performed in parallel between processes.

Guess you like

Origin blog.csdn.net/weixin_53909748/article/details/132203641