Khóa học
Nếu bạn vừa bắt đầu với Python và muốn học thêm, hãy tham gia khóa học Giới thiệu Khoa học Dữ liệu với Python của DataCamp.

Giới thiệu
Threading cho phép các phần khác nhau của tiến trình của bạn chạy đồng thời (Nguồn: RealPython). Những phần khác nhau này thường độc lập và có đơn vị thực thi riêng thuộc cùng một tiến trình. Tiến trình chỉ đơn giản là một chương trình đang chạy có các đơn vị riêng có thể chạy song song. Ví dụ, một trình duyệt web có thể là một tiến trình, một ứng dụng chạy đồng thời nhiều camera có thể là một tiến trình; trò chơi điện tử là một ví dụ khác về tiến trình.
Bên trong một tiến trình xuất hiện khái niệm đa luồng (multi-threading), nơi nhiều luồng phối hợp để đạt một mục tiêu chung. Lợi ích quan trọng nhất của việc dùng luồng là cho phép bạn chạy chương trình song song.
Hãy hiểu khái niệm threading qua một ví dụ. Hãy tưởng tượng bạn có một ứng dụng đếm số xe ra vào bãi đỗ xe của trung tâm thương mại. Thiết bị của bạn có nhiều camera giám sát lối vào và lối ra, kết nối đến một thiết bị trung tâm. Mỗi camera sẽ có một thuật toán để theo dõi lưu lượng xe, thuật toán này thuộc cùng một tiến trình hay chương trình. Tuy nhiên, mỗi camera cùng với thuật toán đang chạy trên đó có thể thuộc về một luồng riêng. Không chỉ vậy, ngay cả phần đọc khung hình từ camera và thuật toán dự đoán trên các khung hình cũng có thể là hai luồng riêng biệt.
Một ví dụ khác là trò chơi điện tử, trong đó tiến trình phải chạy các tác vụ song song như đồ họa, tương tác người dùng và mạng (khi chơi nhiều người) vì trò chơi phải luôn phản hồi. Để đạt được điều này, nó cần sử dụng khái niệm đa luồng, nơi mỗi luồng chịu trách nhiệm chạy từng tác vụ độc lập.
Một luồng có luồng thực thi riêng, nghĩa là trong tiến trình sẽ có nhiều việc xảy ra cùng lúc.
Lưu ý rằng mỗi tiến trình có ít nhất một luồng, gọi là main thread (luồng chính). Nếu chương trình của bạn không có luồng nào được định nghĩa, thì nó sẽ có ít nhất luồng chính, tức là bản thân tiến trình.
Nếu bạn nhìn vào sơ đồ ở trên, có một tiến trình gồm luồng chính chứa các biến toàn cục và ba luồng khác nhau t1, t2 và t3. Mỗi luồng có các biến cục bộ và luồng thực thi mã riêng. Các luồng thường chia sẻ tài nguyên của tiến trình, như tài nguyên sẵn có cho luồng chính cũng sẽ được chia sẻ giữa ba luồng t1, t2 và t3. Tất cả các luồng sẽ có quyền truy cập vào biến toàn cục đồng thời có các biến cục bộ của riêng mình.
Có các loại luồng khác nhau:
- Luồng kernel (kernel thread)
- Luồng người dùng (user thread)
- Kết hợp luồng kernel và luồng người dùng
Giờ hãy xem một số ưu điểm khi dùng threading trong chương trình của bạn:
-
Đa luồng cho phép chương trình tăng tốc thực thi với điều kiện có nhiều CPU.
-
Nó cũng cho phép bạn thực hiện các tác vụ khác trong khi các thao tác I/O đang diễn ra với sự trợ giúp của nhiều luồng hoặc thậm chí luồng chính cùng với một luồng. Ví dụ, tốc độ đọc khung hình từ camera và tốc độ suy luận của thuật toán sẽ được xử lý bởi các luồng khác nhau. Do đó, thuật toán sẽ không phải chờ khung hình được đưa vào, và phần đọc khung hình cũng không phải đợi thuật toán chạy xong mới đọc khung tiếp theo.
- Các luồng trong cùng một tiến trình có thể chia sẻ bộ nhớ và tài nguyên của luồng chính.
Thách thức của Threading
-
Hãy nhớ rằng Python dựa trên triển khai CPython, vốn giới hạn chỉ một luồng được chạy tại một thời điểm, do đó threading có thể không tăng tốc mọi tác vụ. Lý do cốt lõi là Global Interpreter Lock (GIL).
Nếu bạn muốn tìm hiểu về GIL, hãy xem hướng dẫn này.
-
Nếu bạn muốn tăng tốc các tác vụ nặng CPU, thì threading có thể không phải giải pháp tốt nhất. Trong những trường hợp như vậy, multi-processing thường hữu ích.
-
Để thực thi một chương trình gồm nhiều luồng, cần chuyển đổi nhanh giữa các luồng; do đó, việc lập lịch cần được chú ý trong các chương trình có nhiều luồng.
-
Chia sẻ tài nguyên cũng có thể là vấn đề vì tất cả các luồng cùng chia sẻ tài nguyên và bộ nhớ của biến toàn cục. Do đó, thao tác thực hiện ở một luồng có thể gây lỗi bộ nhớ cho luồng khác, hoặc luồng kia có thể không có đủ bộ nhớ để thực hiện tác vụ.
Threading trong Python
-
Trong Python, mô-đun threading là mô-đun tích hợp sẵn có tên
threadingvà có thể import trực tiếp. -
Vì gần như mọi thứ trong Python đều được biểu diễn dưới dạng đối tượng, threading cũng là một đối tượng trong Python. Một luồng có khả năng
- Lưu trữ dữ liệu,
- Được lưu trong các cấu trúc dữ liệu như dictionary, list, set, v.v.
- Có thể được truyền làm tham số cho một hàm.
-
Một luồng cũng có thể được thực thi như một tiến trình.
-
Một luồng trong Python có thể ở nhiều trạng thái như:
- Chờ (Wait),
- Khóa (Locked).
Giờ hãy học cách bạn có thể triển khai threading trong Python.
Mô-đun Thread trong Python3
Lưu ý Python3 tương thích ngược với mô-đun thread tồn tại trong Python2.7. Ở Python3, có thể import nó dưới dạng mô-đun _thread. Vậy hãy lấy một ví dụ để hiểu mô-đun _thread.
import _thread #thread module imported
import time #time module
Hãy định nghĩa một hàm tên thread_delay, nhận hai tham số đầu vào, tức là tên luồng và độ trễ. Bên trong hàm này, bạn sẽ:
- Đặt một biến đếm bằng 0,
- Sau đó lặp bằng vòng lặp while chạy ba lần,
- Bên trong vòng while, dùng
time.sleep()để thêm độ trễ; độ trễ này hữu ích để hiểu việc thực thi của luồng. Độ trễ tính bằng giây, - Tăng biến đếm thêm 1,
- Để xem việc thực thi luồng có diễn ra trơn tru hay không, bạn sẽ in tên luồng và thời điểm luồng được thực thi.
def thread_delay(thread_name, delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(thread_name, '-------->', time.time())
Giờ để thêm chức năng luồng cho hàm trên hoặc chạy hàm trên trong luồng, bạn sẽ dùng phương thức start_new_thread trong mô-đun _thread.
Hãy xem docstring của phương thức start_new_thread.
?_thread.start_new_thread

Hãy truyền vào hàm thread_delay không có ngoặc và hai đối số, tức là tên luồng và độ trễ (để trực quan hóa việc thực thi luồng vì nó rất nhanh).
_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 3))
10916
t1 --------> 1588077856.4221454
t1 --------> 1588077857.4418247
t2 --------> 1588077858.422453
t1 --------> 1588077858.4431
t2 --------> 1588077861.4371448
t2 --------> 1588077864.438915
Từ kết quả trên, bạn có thể thấy luồng t1 bắt đầu thực thi trước. Trong khi đó, luồng t2 chờ vì có độ trễ 3 giây, và ngay khi độ trễ kết thúc, luồng t2 được thực thi; đồng thời, độ trễ của luồng t1 chỉ 1 giây.
Giờ hãy đổi độ trễ của t2 thành 5 giây, điều này sẽ khởi chạy t2 sau khi t1 đã thực thi xong vì t1 chỉ mất 3 giây để hoàn tất.
_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 5))
14632
t1 --------> 1588078001.4087512
t1 --------> 1588078002.4272866
t1 --------> 1588078003.432218
t2 --------> 1588078005.4082909
t2 --------> 1588078010.4268317
t2 --------> 1588078015.4283602
Triển khai threading bằng mô-đun Threading
Hãy dùng lại ví dụ như trên, nhưng lần này bạn sẽ dùng mô-đun threading thay vì mô-đun _thread.
import threading
import time
def thread_delay(thread_name, delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(thread_name, '-------->', time.time())
Bên trong mô-đun threading có lớp Thread, có tinh thần tương tự hàm start_new_thread của mô-đun _thread.
Hãy xem docstring của lớp Thread, lớp này nhận nhiều tham số như group, target (như một hàm), args, v.v.
?threading.Thread

Trong hàm khởi tạo của lớp Thread, bạn sẽ truyền vào hàm đích thread_delay và các đối số của hàm đó.
t1 = threading.Thread(target=thread_delay, args=('t1', 1))
t2 = threading.Thread(target=thread_delay, args=('t2', 3))
Trong mô-đun threading, để chạy hay thực thi luồng, bạn sử dụng phương thức start(), phương thức này đơn giản là chịu trách nhiệm chạy luồng.
?t1.start

t1.start()
t2.start()
t1 --------> 1588081121.7790291
t1 --------> 1588081122.7962046
t2 --------> 1588081123.7789369
t1 --------> 1588081123.7980876
t2 --------> 1588081126.7961557
t2 --------> 1588081129.7995002
Bạn cũng sẽ dùng phương thức join, nghĩa là chờ cho đến khi tất cả luồng thực thi xong. Vậy nên bất kỳ mã nào bạn viết sau phương thức join sẽ chỉ chạy khi các luồng này đã kết thúc.
?t1.join

t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
t1 --------> 1588081798.7118263
t1 --------> 1588081799.7231226
t2 --------> 1588081800.7129223
t1 --------> 1588081800.730732
t2 --------> 1588081803.7298765
t2 --------> 1588081806.7309563
Thread execution is complete!
Giờ hãy tính thể tích hình lập phương và hình vuông bằng khái niệm threading, khá đơn giản.
def volume_cube(a):
print ("Volume of Cube:", a*a*a)
def volume_square(a):
print ("Volume of Square:", a*a)
t1 = threading.Thread(target=volume_cube, args=(2))
t2 = threading.Thread(target=volume_square, args=(3))
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Thread execution is complete!
Exception in thread Thread-9:
Traceback (most recent call last):
File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
TypeError: volume_square() argument after * must be an iterable, not int
Exception in thread Thread-8:
Traceback (most recent call last):
File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
TypeError: volume_cube() argument after * must be an iterable, not int
Khoan đã? Ngay khi bạn gọi phương thức start, nó dẫn đến lỗi. Đó là vì tham số args cần một tuple, và vì volume_cube và volume_square chỉ nhận một tham số, nên bạn cần đặt dấu phẩy trong tham số args sau khi chỉ định đối số đầu tiên của các hàm.
t1 = threading.Thread(target=volume_cube, args=(2,))
t2 = threading.Thread(target=volume_square, args=(3,))
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Volume of Cube: 8
Volume of Square: 9
Thread execution is complete!
Giờ hãy học cách dùng threading như một lớp con.
Threading như một lớp con
Trong phần này, bạn sẽ học cách tạo lớp con từ lớp Thread có trong mô-đun threading, rồi tạo các thể hiện luồng t1 và t2 từ lớp đó.
Bạn sẽ dùng lại hàm thread_delay ở đây.
class DataCampThread(threading.Thread):
def __init__(self, name, delay):
threading.Thread.__init__(self)
self.name = name
self.delay = delay
def run(self):
print('Starting Thread:', self.name)
thread_delay(self.name,self.delay)
print('Execution of Thread:', self.name, 'is complete!')
t1 = DataCampThread('t1', 1)
t2 = DataCampThread('t2', 3)
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Starting Thread: t1
Starting Thread: t2
t1 --------> 1588089083.4879806
t1 --------> 1588089084.489222
t1 --------> 1588089085.4909666
Execution of Thread: t1 is complete!
t2 --------> 1588089085.4979634
t2 --------> 1588089088.499365
t2 --------> 1588089091.5012596
Execution of Thread: t2 is complete!
Thread execution is complete!
Giờ hãy hiểu đoạn mã trên từng bước:
-
Bạn định nghĩa một lớp
DataCampThread, và ở tham số lớp, bạn truyền vào lớpthreading.Thread. Lý do là bạn muốn tạo một lớp con từ mô-đun threading. Làm vậy cho phép bạn dùng tất cả các phương thức có sẵn trong lớpthreading.Threadtương tự khái niệm kế thừa. -
Tiếp theo, bạn định nghĩa phương thức init của lớp DataCampThread và truyền vào hàm cùng với độ trễ. Init là hàm khởi tạo tương tự như các tham số bạn đã truyền cho lớp
threading.Thread; ở đây bạn sẽ truyền cho lớpDataCampThread. -
Sau đó bạn khởi tạo phương thức init của lớp chính threading.Thread rồi gán name và delay.
-
Tiếp theo, bạn định nghĩa phương thức
run; nhớ lại ở đây bạn đã định nghĩa phương thứcstart, vốn thực chất sẽ gọi phương thứcrunở tầng dưới. Vì bạn đã kế thừa lớp chính, bạn được phép thay đổi phương thứcrun. Do đó, khi bạn gọi phương thứcstart, mọi chỉnh sửa bạn thực hiện trong phương thứcrunsẽ được phản ánh.
Kết luận
Chúc mừng bạn đã hoàn thành hướng dẫn.
Hướng dẫn này là phần giới thiệu cơ bản về threading trong Python. Tuy nhiên, threading là một chủ đề rất rộng và quan trọng, và có thể một số khái niệm chưa được đề cập. Hãy thoải mái tìm hiểu thêm các phương thức threading như active count, current thread, v.v.
Nếu bạn muốn học thêm, hãy xem Xử lý song song dựa trên Thread của Python.
Nếu bạn mới bắt đầu với Python và muốn học thêm, hãy tham gia khóa học Giới thiệu Khoa học Dữ liệu với Python của DataCamp.