Đa luồng (Multithreading) trong Python

Python Nâng cao

5.0 (3 đánh giá)
Tạo bởi Kteam Cập nhật lần cuối 19:23 26-02-2023 28.794 lượt xem 0 bình luận
Tác giả/Dịch giả: K9
Học nhanh

Danh sách bài học

Đa luồng (Multithreading) trong Python

Bài toán đa luồng trong Python

Bản chất code python sẽ thực hiện tuần tự từ trên xuống và khi lệnh code trước đó hoàn thành mới tiếp tục lệnh tiếp theo.

Ví dụ bạn có một vòng lặp vô tận hoặc bạn gửi request đến bên thứ 3 và phải đợi bên đó phản hồi. Bạn muốn tiếp tục đợi nhưng có thể đồng thời thực hiện công việc khác.

Cách thức xử lý bài toán này từ đơn luồng thành đa luồng (Multithreading) là sử dụng Thread hoặc Process. Ở bài này chúng ta dùng Thread để thực hiện đa luồng trong Python.


Bài toán đơn luồng trong python

Ta có nhu cầu thực hiện công việc xuất ra màn hình thông tin của class ProfileInfo với ProfileInfo được định nghĩa trong file classKteam.py như sau:

class ProfileInfo:
    Name = ''
    
    def __init__(self, Name):
        self.Name = Name

và file multithreading.py có nội dung như sau:

from classKteam import *

def showInfo(profile):
    while 1==1:
        print("Profile: {}\n".format(profile.Name))      

if __name__ =="__main__":
    showInfo(ProfileInfo('Thread 1'))
    showInfo(ProfileInfo('Thread 2'))

Ở đây chúng ta có thể nhận thấy tại hàm __main__ thực hiện 2 lần hàm showInfo.

  • Lần 1 là profile với Name Thread 1
  • Lần 2 là Thread 2.

Và kết quả khi run file multithreading.py

Console liên tục in ra màn hình dòng Profile: Thread 1.

Ơ! Thế Thread 2 đâu mất rồi? Chắc chắn Thread 2 không thể được in ra vì showInfo của Thread 1 chưa thực hiện xong. Và sẽ không bao giờ xong vì đây là vòng lặp vô tận:

while 1==1:

Bài toàn đa luồng từ đơn luồng trong python

Vậy lúc này chúng ta muốn cả Thread 1 và Thread 2 đều được in ra đồng thời thì phải làm sao?

Ta sẽ thực hiện import thư viện threading ngay bên dưới dòng code import classKteam như sau

import threading

Và đoạn code để đưa một hàm vào một luồng chạy riêng biệt như sau:

p1 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 1'),))
p1.start()

Ở đây, chúng ta có thể thấy hàm cần thực hiện luồng riêng đươc đưa tên vào target và các tham số của hàm được đưa vào trong args. Các tham số cách nhau bởi dấu phẩy (",")

Toàn bộ file multithreading.py hiện trông như sau:

from classKteam import *
import threading

def showInfo(profile):
    while 1==1:
        print("Profile: {}\n".format(profile.Name))      

if __name__ =="__main__":
    p1 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 1'),))
    p1.start()

    p2 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 2'),))
    p2.start()

 Chúng ta cùng thực hiện run script nhé:

Lý do là args yêu cầu đầu vào phải là iterable. Để có thể chạy được script này ta thêm dấu phẩy (",") phía sau parameter đầu tiên của args như sau:

p1 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 1'),))

Lúc này file multithreading.py như sau:

from classKteam import *
import threading

def showInfo(profile):
    while 1==1:
        print("Profile: {}\n".format(profile.Name))      

if __name__ =="__main__":
    p1 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 1'),))
    p1.start()

    p2 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 2'),))
    p2.start()

 Khi run script:

Thread 1 và 2 được in ra liên tục đồng đều nhau. Quá tuyệt vời phải không?

Nhưng chương trình chạy lặp liên tục và quá nhanh. Vậy nếu muốn giảm tốc độ chạy lại như kiểu có một đoạn ngủ (sleep) thì phải làm sao?


Sleep trong python

Để thực hiện sleep trong python chúng ta cần import time:

import time

 Để gọi sleep dùng đoạn code như sau(sleepTime được tính bằng giây):

time.sleep(sleepTime)

Vậy nhu cầu sẽ là sleep mỗi khi in ra thông tin profile mình sẽ thêm sleep sau print như sau:

def showInfo(profile):
    while 1==1:
        print("Profile: {}\n".format(profile.Name))    
        time.sleep(1)  

sleep 1 là code của luồng đó sẽ tạm dừng tại vị trí đó 1 giây.

Vậy nếu mình muốn thời gian sleep này được truyền vào từ args thì sẽ điều chỉnh như sau:

from classKteam import *
import threading
import time

def showInfo(profile, sleepTime):
    while 1==1:
        print("Profile: {}\n".format(profile.Name))    
        time.sleep(sleepTime)  

if __name__ =="__main__":
    p1 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 1'),1))
    p1.start()

    p2 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 2'),1))
    p2.start()

Thêm sleepTime làm tham số thứ 2 cho hàm showInfo. Truyền biến sleepTime vào trong sleep và đồng thời truyền thời gian cần sleep vào trong args ở Thread(ở đây mình sẽ cho sleep 1 giây nên truyền vào là 1. Bạn có thể truyền vào số khác tùy ý ở mỗi luồng nhé!).

Khi run script bạn sẽ thấy vòng lặp vô tận được nghỉ 1 giây nên đã chạy chậm lại hiền hòa hơn.


Dừng (stop) luồng riêng biệt trong python

Vậy nếu mình có nhu cầu dừng luồng số 1 khi nhấn phím 1, dừng luồng số 2 khi nhấn phím 2… Vậy thì phải làm sao?

Chúng ta cần phải có cơ chế thấy lệnh Stop thì dừng, và cơ chế nhận được phím nào được bấm.

Vậy để có thể đặt lá cờ Stop cho từng luồng riêng biệt thì ta nên tạo thêm 1 biến IsStop vào trong ProfileInfo:

class ProfileInfo:
    Name = ''
    IsStop = False
    
    def __init__(self, Name):
        self.Name = Name
        self.IsStop = False

Với IsStop = False là luồng đó được phép chạy và ngược lại luồng đó sẽ phải dừng lại.

Vì bản chất ProfileInfo là 1 class nên nó là tham chiếu. Vậy nên khi giá trị của IsStop thay đổi thì bên trong hàm showInfo cũng sẽ nhận được giá trị thay đổi đó của IsStop. Mình sẽ cài đặt thêm cơ chế dừng cho hàm showInfo như sau:

if(profile.IsStop):
    print("{} stop.\n".format(profile.Name))
    break

Hàm showInfo lúc này trông như sau:

def showInfo(profile, sleepTime):
    # function to print cube of given num
    while 1==1:
        print("Profile: {}-{} - again after {}\n".format(profile.Name, profile.IsStop, sleepTime))
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break
        time.sleep(sleepTime)
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break

Mình kiểm tra nếu biến IsStop thành true thì sẽ break vòng lặp vô tận ngay lập tức. Mình cho lệnh này gọi ở trước và sau khi sleep để đảm bảo có thể stop hàm ngay khi có thể.

Multithreading.py lúc này như sau:

from classKteam import *
import threading
import time

def showInfo(profile, sleepTime):
    while 1==1:
        print("Profile: {}\n".format(profile.Name))    
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break
        time.sleep(sleepTime)  
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break

if __name__ =="__main__":
    p1 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 1'),1))
    p1.start()

    p2 = threading.Thread(target=showInfo, args=(ProfileInfo('Thread 2'),1))
    p2.start()

Vậy để cài đặt thêm việc nhận biết phím nào được bấm thì làm sao?

Trước đó chúng ta cần phải đưa các Thread của mình vào một danh sách để có thể quản lý

Mình sẽ tạo một biến Profiles ngoài hàm và đưa các Thread cần chạy vào trong nó như sau:

profiles = [ProfileInfo('Thread 1'), ProfileInfo('Thread 2')]

 Ở hàm main thay vì thực hiện gọi start từng thread mình sẽ đưa vào vòng lặp duyệt qua hết profile mình có:

for item in profiles:
        p = threading.Thread(target=showInfo, args=(item, 1))
        p.start()

Chúng ta cần cài thêm pynput import keyboard vào code

Đầu tiên bạn cần install pynput bằng cách gọi lệnh sau ở terminal

pip install pynput

Khi cài đặt xong bạn import keyboard vào code của mình như sau:

from pynput import keyboard

Giờ chúng ta sẽ triển khai listener cho keyboard:

Thêm đoạn code này vào cuối hàm __main__

with keyboard.Listener(on_press=on_press) as listener:
        listen = listener
        listener.join()

và thêm biến listen ngoài hàm bên dưới profiles như sau:

listen = keyboard.Listener

Và bạn cần định nghĩa thêm hàm on_press như sau:

def on_press(key):
    vk = key.vk if hasattr(key, 'vk') else key.value.vk
    print('vk =', vk)
    if(vk == None):
        return
    index = vk - 48
    if(index >= 0 and index < len(profiles) and profiles[index].IsStop == False):
        print("Doing stop: {}".format(profiles[index].Name))
        profiles[index].IsStop = True

Ở hàm on_press này khi nhấn phím số thì sẽ trừ đi 48 để ra được vị trí tương ứng của ProfileInfo trong profiles.

  • Ví dụ số 0 được nhấn sẽ có mã số là 48. 48-48 = 0. Khi này index sẽ bằng 0. Profiles[index] cũng chính là profile đầu tiên.

Chúng ta có được profile cần dừng thì thực hiện chuyển giá trị IsStop cho profile đó thành True để trong hàm showInfo kiểm tra thấy IsStop == true thì break vòng lặp. Lúc này hàm showInfo sẽ được dừng lại.

Trong hàm showInfo mình điều chỉnh lệnh print lại một chút để dễ kiểm tra được thông số của ProfileInfo:

print("Profile: {}-{} - again after {}\n".format(profile.Name, profile.IsStop, sleepTime))

 file multithreading.py như sau:

import time
import threading
from classKteam import *

from pynput import keyboard

profiles = [ProfileInfo('Thread 1'), ProfileInfo('Thread 2')]
listen = keyboard.Listener

def on_press(key):
    vk = key.vk if hasattr(key, 'vk') else key.value.vk
    print('vk =', vk)
    if(vk == None):
        return
    index = vk - 48
    if(index >= 0 and index < len(profiles) and profiles[index].IsStop == False):
        print("Doing stop: {}".format(profiles[index].Name))
        profiles[index].IsStop = True

def showInfo(profile, sleepTime):
    while 1==1:
        print("Profile: {}-{} - again after {}\n".format(profile.Name, profile.IsStop, sleepTime))
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break
        time.sleep(sleepTime)
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break  

if __name__ =="__main__":

    for item in profiles:
        p = threading.Thread(target=showInfo, args=(item, 3))
        p.start()
    
    with keyboard.Listener(on_press=on_press) as listener:
        listen = listener
        listener.join()

 Khi này bạn run script sẽ thấy script chạy bình thường. Thử nhấn phím 0 thì sẽ thấy Thread 1 được dừng. Nhấn thêm phím 1 thì Thread 2 sẽ dừng. Bạn tiếp tục nhấn phím sẽ thấy terminal vẫn tiếp tục hiển thị giá trị của phím bạn bấm. Lý do là vì listener chưa dừng lại.

Để có thể hoàn toàn dừng chưng trình khi tất cả Thread đã dừng chúng ta thực hiện như sau:

Thêm đoạn code này ở cuối của hàm showInfo

totalRunningThread = any(x.IsStop == False for x in profiles)
    print("Total: {}\n".format(totalRunningThread))
    if(totalRunningThread == False):
        listen.stop()

Chúng ta tạo một biến kiểm tra tất cả Profile IsStop == False tức là kiểm tra xem có profile nào đang chạy hay không. Nếu không có profile nào đang chạy thì cho stop listen bằng lệnh listen.stop()

Khi này bạn chạy thử lại sẽ thấy khi tất cả luồng được dừng thì script của chúng ta cũng dừng lại.

Chúng ta thêm 1 dòng này ở hàm main bên trong vòng lặp để đảm bảo profile trước khi start IsStop luôn bằng false.

item.IsStop = False

Lúc này file multithread.py hoàn thiện s4 như sau:

import time
import threading
from classKteam import *

from pynput import keyboard

profiles = [ProfileInfo('Thread 1'), ProfileInfo('Thread 2')]
listen = keyboard.Listener

def on_press(key):
    vk = key.vk if hasattr(key, 'vk') else key.value.vk
    print('vk =', vk)
    if(vk == None):
        return
    index = vk - 48
    if(index >= 0 and index < len(profiles) and profiles[index].IsStop == False):
        print("Doing stop: {}".format(profiles[index].Name))
        profiles[index].IsStop = True

def showInfo(profile, sleepTime):
    while 1==1:
        print("Profile: {}-{} - again after {}\n".format(profile.Name, profile.IsStop, sleepTime))
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break
        time.sleep(sleepTime)
        if(profile.IsStop):
            print("{} stop.\n".format(profile.Name))
            break  
    totalRunningThread = any(x.IsStop == False for x in profiles)
    print("Total: {}\n".format(totalRunningThread))
    if(totalRunningThread == False):
        listen.stop()

if __name__ =="__main__":

    for item in profiles:
        item.IsStop = False
        p = threading.Thread(target=showInfo, args=(item, 3))
        p.start()
    
    with keyboard.Listener(on_press=on_press) as listener:
        listen = listener
        listener.join()

Kết luận

Vậy là chúng ta đa tìm hiểu qua và nắm được cách vận dụng đa luồng một cách hiệu quả. Chúc các bạn có thể vận dụng kiến thức vào trong thực tế công việc hằng ngày. 

© Bài viết được đăng tải tại website Howkteam.vn và video hướng dẫn được lưu trữ tại channel youtube: Kteam. Nhớ like, subscribe và nhấn chuông thông báo để không bỏ lỡ video mới nhất nhé.


Tải xuống

Tài liệu

Nhằm phục vụ mục đích học tập Offline của cộng đồng, Kteam hỗ trợ tính năng lưu trữ nội dung bài học Đa luồng (Multithreading) trong Python dưới dạng file PDF trong link bên dưới.

Ngoài ra, bạn cũng có thể tìm thấy các tài liệu được đóng góp từ cộng đồng ở mục TÀI LIỆU trên thư viện Howkteam.com

Đừng quên likeshare để ủng hộ Kteam và tác giả nhé!

Project

Nếu việc thực hành theo hướng dẫn không diễn ra suôn sẻ như mong muốn. Bạn cũng có thể tải xuống PROJECT THAM KHẢO ở link bên dưới!


Thảo luận

Nếu bạn có bất kỳ khó khăn hay thắc mắc gì về khóa học, đừng ngần ngại đặt câu hỏi trong phần bên dưới hoặc trong mục HỎI & ĐÁP trên thư viện Howkteam.com để nhận được sự hỗ trợ từ cộng đồng.

Nội dung bài viết

Tác giả/Dịch giả

K9

Nhà sáng lập Howkteam.com, KQuiz.vn & tác giả các khóa học C#, Auto, Unity3D, Python....

Với mong muốn mang đến kiến thức chất lượng, miễn phí cho mọi người, với tâm huyết phá bỏ rào cản kiến thức từ việc giáo dục thu phí. Tôi đã cùng đội ngũ Kteam đã lập nên trang website này để thế giới phẳng hơn.
Hãy cùng chúng tôi lan tỏa kiến thức đến cộng đồng! 

Khóa học

Python Nâng cao

Khác với các khóa PYTHON CƠ BẢN hay BÀI TẬP PYTHON. Với khóa Python nâng cao bạn không chỉ sẽ được làm quen với nhiều kỹ thuật ở mức độ cao hơn, khó hơn mà còn được giải thích về nguyên lý hoạt động, cách sử dụng cũng như nhiều kinh nghiệm đến từ anh Kim Long - founder và là tác giả của nhiều khóa học tại Howkteam.com

Chào mừng các bạn đã đến với khoá học Lập trình Python nâng cao của Kteam!

Đánh giá

XuanNguyen02 đã đánh giá 12:52 07-12-2023

Bài này hữu ích quá ạ. Em cảm ơn

Vo Tan Duc đã đánh giá 19:40 07-05-2023

Hay quá anh ơi! Kỹ thuật này sẽ giúp cho nhiều bạn lắm

Ntq.Huy đã đánh giá 21:25 24-02-2023

Chào mừng a trở lại, Cảm ơn a vì đã chia sẻ kiến thức

Bình luận

Để bình luận, bạn cần đăng nhập bằng tài khoản Howkteam.

Đăng nhập
Không có video.