+4

GIL (Global Interperter Lock) là cái gì và tại sao python lại dùng nó?

1. Mở đầu

Làm việc với python một thời gian hẳn các bạn cũng từng nghe và trải nghiệm qua về GIL (Global Interpreter Lock). Vậy nó là cái gì mà bất cứ lập trình viên python nào cũng nên biết. Bằng hiểu biết và kinh nghiệm của bản thân mình xin phép làm một bài viết chia sẻ hiểu biết cơ bản về GIL và Parallelism trong Python.

2. GIL là gì?

Để cho bạn nào chưa biết thì Python Global Interpreter Lock (GIL) là cơ chế quản lý luồng (Thread) chỉ cho phép Python sử dụng một luồng duy nhất để thực thi các lệnh lập trình trong một chương trình. Điều mang ý nghĩa là tại một thời điểm một process của python chỉ có một luồng duy nhất được thực thi.

3. Tại sao Python lại có GIL?

Để tìm kiếm được câu trả lời cho vấn đề này chúng ta phải lục lại 2 khái niệm trong lập trình đó là sequential, concurrencyparallelism. Về phân biệt 2 khái niệm này thì mình thấy đã có bài phân tích khá đầy đủ bạn có thể tìm đọc ở link dưới: Phân biệt Concurrency và Parallelism trong lập trình Nếu bạn lười mở link để đọc thêm thì mình có thể giải thích cơ bản ở phía dưới 😄 Giả sử công việc trong dự án của chúng ta cần thực hiện 3 task (A, B, C) để hoàn thành nhiệm vụ.

Sequential

Thì công việc sẽ xử lý một cách tuần tự việc A -> B -> C. Như vậy có thể hiểu làm xong task A chúng ta mới được làm task B. image.png Công việc xảy ra cứ tuần tự bước sau phải đợi bước trước. Công việc này giống với việc máy tính chỉ có 1 CPU và chúng ta bắt nó chạy toàn bộ task vụ vậy ( bóc lột quá 😅).

Parallelism

Không giống việc bọc lột 1 CPU phía trên ngày này các máy tính đã sử dụng nhiều core CPU hơn. Bởi vì vậy để tận dụng nguồn tài nguyên này thì chúng ta sẽ chia các task A, B, C cho các CPU khác nhau đảm nhiệm. Việc này đảm bản thời gian thực thi các task sẽ nhanh hơn và tận dụng triển để tài nguyên. Đây cũng là một phần của khái niệm (CPU bound). image.png

Concurrency

Nghe tới việc tần dụng toàn bộ các CPU ở trên thì bạn cũng thấy là cơ chế parallelism giúp giải quyết được vấn đề tận dụng triệt để nguồn nhân lực (CPU) nhưng ta có thể xử lý tốt hơn nữa là xử dụng triệt để nguồng tài nguyên bằng các chính chia các task đó thành các sub task nhỏ (có thể xử lý độc lập không ảnh hưởng đến kết quả) rồi chia các subtask đó cho các CPU xử lý. Đó chính là tư tưởng của concurrency.Tư tưởng của xử lý đồng thời ta sẽ chia mỗi task trong 3 task đó thành những sub task nhỏ hơn, mỗi lập trình viên sẽ thực hiện một sub task (không nhất định sub task thuộc task nào miễn sao không có 2 người cùng làm 1 sub task). Khi xong 1 sub task sẽ tiếp tục làm tiếp 1 sub task tiếp theo. Nói một cách đơn giản tư tưởng của xử lý đồng thời là chia công việc thành nhiều phần nhỏ, tận dụng thời gian chết của mỗi lập trình viên để xử lý một subtask khác nhằm tận dụng được tối đa nguồn lực. image.png

Về cơ bản tư tưởng này là sẽ chia nhỏ công việc ra sau đó tận dung cơ chế parallel (song song) để thực hiện nhiều nhiệm vụ cùng lúc.

Lan man như thế đủ rồi chúng ta vào phần chính nhé 😁

Tại sao lại tồn tại GIL

Dưới đây là hình ảnh cách xử lý thread của GIL trong python:

image.png

Bạn có thể nhận thấy là các thread được lock và xử lý từng phần. Chính bởi điều này với cơ chế lập trình đa luồng (multi-thread) tồn tại CPU-bound thì GIL không thể tận dụng được hết ưu thế của máy tính đa nhân, đa luồng hiện tại.

Vậy tại sao Python vẫn sử dụng GIL nguyên nhân xuất phát từ việc concept quản lý bộ nhở của python. Mọi thứ trong Python đều là Object và kế thừa từ PyObject chứa 2 thông tin cơ bản:

  • Reference Count (ob_refcnt): chứa số lượng tham chiếu của đối tượng
  • Type Pointer (ob_type): chứa con trỏ đến loại đối tượng tương ứng.

Khi một biến được khởi báo trong Python nó sẽ không tạo object mới ngay mà sẽ tham chiếu xem biết đó có được gán với một biết nào đã tồn tại trước chưa.

Python sử dụng cơ chế reference counting để quản lý bộ nhớ để tránh việc memory leak. Một khi object được tạo, Python sẽ gán một giá trị tham chiếu Reference Count (ob_refcnt) để đểm xem số lượng tham chiếu của object gán địa chỉ đó là bao nhiêu. Khi reference count trở về giá trị 0, vùng nhớ của object đó sẽ được garbage collection xóa đi để tiết kiệm bộ nhớ cho chương trình. Ví dụ:

import sys
a = "hello"
print(sys.getrefcount(a))
b = a
c = a
print(sys.getrefcount(a))
c = "world"
print(sys.getrefcount(a))
del b
print(sys.getrefcount(a))

Kết quả in ra màn hình:

4
6
5
4

Giải thích 1 chút thì việc khởi tạo biến thì ob_refcnt được khởi tạo sau khi biến b và biến c được tham chiếm vào thì ob_refcnt sẽ được +2 thành 6 và sau khi c được gán bằng object mới thì referrent count của object trước đó sẽ bị -1 và khi del biến b thì referrent count tiếp tục -1 khi referrent count về 0 thì vùng nhớ của object đó sẽ bị xóa . Khá hay phải không 😁.

Vậy điều này liên quan gì tới GIL??

Chính bởi concept như vậy thì GIL mới là cơ chế thiết yếu trong Python. Thử tưởng tượng nếu như trong một thời điểm cả 2 luồng (thread) cùng tham chiếu tới 1 object và cùng tăng hoặc giảm referrent count của object đó thì chuyện gì sẽ xảy ra 😁😁. Trường hợp khả quan nhất thì chương trình chúng ta sẽ xuất hiện memory leak không thì tệ hơn object đó bị xóa khỏi bộ nhớ trong khi vẫn còn 1 thread khác đang tham chiếu tới nó. Do đó việc khóa (lock) cho phép 1 luồng chạy trên 1 procces giải quyết được an toàn vấn đề này.

Tới đây bạn các bạn có thắc mắc về vấn đề việc sử dụng GIL để lock 1 thread sao Python không chỉ cho 1 thread trên 1 process (Sequential) thời gian có vẻ giống nhau thậm chí GIL còn khi còn chậm hơn khi tốn thời gian overhead time (chuyển giữa các thread)

Mình cũng rất thắc mắc điều này nhưng sau thời gian tìm hiểu và test thử thì Boom!!. Thì ra các thread của GIL không lock cứng các thread như chúng ta nghĩ.

import threading
import time

def task():
    # Tính toán đơn giản: đếm số lần lặp
    count = 0
    for _ in range(100000000):
        count += 1

# Sử dụng multithreading với GIL
def multithreading_test():
    start_time = time.time()
    threads = []
    for _ in range(10):  # Tạo 10 luồng
        thread = threading.Thread(target=task)
        thread.start()
        threads.append(thread)
    for thread in threads:
        thread.join()
    end_time = time.time()
    print("Multithreading with GIL time:", end_time - start_time)

# Chỉ sử dụng một luồng
def single_thread_test():
    start_time = time.time()
    for _ in range(10):  # Thực hiện task 10 lần liên tiếp với một luồng
        task()
    end_time = time.time()
    print("Single thread time:", end_time - start_time)

multithreading_test()
single_thread_test()

Kết quả là:

Multithreading with GIL time: 28.802220582962036
Single thread time: 30.20151162147522

Phần chương trình trên không thể hiện được sự vượt trội khi xử lý bằng GIL. Thực chất GIL chỉ ảnh hưởng tới các tác vụ của ứng dụng CPU-bound (tốc độ xử lý phụ thuộc vào thời gian tính toán của CPU) bởi các task vụ này liên quan tới thời gian tính toán và xử lý giá trị biến, ngược lại với các ứng dụng I/O-bound (thời gian thực thi chủ yếu phụ thuộc vào thời gian chờ đợi I/O, chẳng hạn như đọc/ghi dữ liệu từ/đến ổ đĩa, mạng hoặc giao tiếp với các thiết bị ngoại vi) thì GIL lại không gây vấn đề gì lớn, thậm chí việc sử dụng multithreading có thể được áp dụng một cách hiệu quả để tận dụng thời gian chờ đợi I/O.

Ngoài cách sử dụng GIL, các lập trình viên còn có thể tạo ra 1 layer trong quá trình compiler - JIT (Just in time compiler) để giải quyết vấn đề trên. Jpython, IronPython là 2 ví dụ điển hình của interpreter Python mà không sử dụng GIL. Tuy nhiên nhược điểm của JIT là thời gian khởi động lại siêu chậm nên không được nhiều lập trình viên Python sử dụng.

Kết luận

Python Global Interpreter Lock (GIL) là cơ chế cốt lõi của Python core. Nó giúp chương trình Python có thế đảm bảo tính toàn vẹn và quản lý hiệu quả bộ nhớ. Bù lại cơ chế này làm giảm hiệu xuất với các ứng dụng xử lý tính toán nhiều (CPU-bound).

Hiện nay Python 3 đã có cải tiến đáng kể để thay đổi nguyên tác hoạt đọng của global lock. Với phiên bản python trở đi, một số cải tiến đã được thực hiện để giảm tác động của GIL, chẳng hạn như việc cho phép các tác vụ I/O (như đọc/ghi tệp, mạng) chạy đồng thời với luồng Python thông qua các thư viện bất đồng bộ như asynciothreading. Một số extension như NumPy hoặc Cython không gắn kết với GIL, cho phép chúng chạy các phần lớn của mã Python mà không bị chặn bởi GIL. Chúng ta có thể sử dụng extension trên để tăng hiệu suất cho ứng dụng Python.

Mong là với các chia sẻ phía trên của mình giúp các bạn có cái nhìn sâu sắc hơn về Python 😄.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí