New FAMILUG

The PyMiers

Monday, 26 June 2023

CPU chạy thread hay process?

Hay những câu hỏi liên quan:

  • Thread và process khác gì nhau?
  • 1 process chạy multithreading trên mấy CPU?
  • Vì sao Java, Rust không nhắc tới multiprocessing?
  • Khi chạy CPU bound, dùng multi-process hay multi-threaded?

bài này sẽ làm cho ra nhẽ.

Việc khó khăn nhất khi trả lời các câu hỏi này là tìm được các tài liệu có tính "chuẩn mực"/căn cứ, không phải mấy trang tutorial, wikipedia hay hỏi đáp trên mạng.

Process là gì, thread là gì

Người dùng máy tính thường biết đến khái niệm process trước, nó hiển thị mặc định trên các chương trình process manager như top, hay ps, hay Monitor trên Ubuntu, hay cả Task manager trên Windows. Mỗi process được gán cho 1 số ProcessID (PID), dùng lệnh kill -9 PID để tắt chương trình bị "treo". Khi chạy 1 chương trình, hệ điều hành sẽ tạo ra 1 (hay vài) process.

Cho tới khi học lập trình Python, thấy mỗi chương trình chỉ chạy code tuần tự từ trên xuống, nhận ra rằng mỗi 1 process chỉ chạy 1 "luồng" (thread), học thêm thư viện threading, biết tạo 2 3 4 thread chạy "cùng lúc" trong 1 process.

Ở đây dùng khái niệm process và thread của hệ điều hành (OS), một số ngôn ngữ lập trình có khái niệm process của riêng mình, VD: Erlang process không giống như OS process.

Trên hệ điều hành dùng Linux như Ubuntu, gõ man 7 pthreads, pthread hay POSIX thread, là "OS thread" trên Linux:

A single process can contain multiple threads, all of which are executing the same program. These threads share the same global memory (data and heap segments), but each thread has its own stack (automatic variables).

Các thread trong 1 process dùng chung dữ liệu (share data) và file description, nhưng có stack riêng.

Theo man 3 pthreads trên OpenBSD:

A thread is a flow of control within a process. Each thread represents a minimal amount of state: normally just the CPU state and a signal mask. All other process state (such as memory, file descriptors) is shared among all of the threads in the process.

In OpenBSD, threads use a 1-to-1 implementation, where every thread is independently scheduled by the kernel.

Theo man 3 pthread trên FreeBSD

POSIX threads are a set of functions that support applications with re- quirements for multiple flows of control, called threads, within a process. Multithreading is used to improve the performance of a program.

Hai BSD OS đều định nghĩa thread là một flow of control trong 1 process.

Theo Microsoft, nhà sản xuất hệ điều hành nhiều người dùng nhất trên thế giới định nghĩa: 1 process đơn giản là 1 chương trình đang chạy. Hay chi tiết hơn: một process cung cấp các tài nguyên để chạy 1 chương trình (code, file description, memory, ... và ít nhất 1 thread), một process bắt đầu với 1 thread, thường được gọi là primary/main thread.

Còn thread là đơn vị mà được hệ điều hành cung cấp cho thời gian dùng CPU.

So sánh process và thread

Khái niệm process có trước, mãi sau này mới có khái niệm (nhiều) thread. Mặc dù khi có 1 process thì nó luôn luôn chạy 1 thread. Trong 1 process có thể có nhiều thread, trong 1 thread không thể có nhiều process.

Nhưng thực ra phần lớn người ta muốn hỏi:

So sánh multi thread và multi process

Multi thread giống như multi process, ngoại trừ 1 việc: các thread share chung memory còn process thì không.

Multitasking - đa nhiệm

Máy tính ngày nay CPU 4 lõi, 8 lõi (core)... luôn chạy nhiều chương trình cùng lúc. Máy tính ngày xưa khi chỉ có 1 CPU 1 core cũng vậy, chạy được nhiều chương trình "cùng lúc" nhờ CPU chuyển liên tục chạy các chương trình khác nhau, việc chuyển đổi rất nhanh này khiến người dùng có cảm giác là chạy cùng lúc. Ví dụ chạy 4 process A B C D:

A B C D A B D C B A C D...

chuyện này không thay đổi kể cả với máy tính nhiều core do số chương trình chạy luôn lớn hơn số core nhiều lần. Ví dụ:

$ grep -c processor /proc/cpuinfo
4
$ ps -ef | wc -l
287

PS: bạn đọc sau khi đọc xong bài và tham khảo xem thread bằng top sẽ chạy ps -eLf

CPU Scheduler

Việc sắp xếp các chương trình chạy thế nào (dùng CPU thế nào) do một bộ phận của kernel có tên "scheduler" thực hiện. Đọc thêm về Linux CPU scheduler tại https://opensource.com/article/19/2/fair-scheduling-linux.

CPU chạy thread hay process?

Tham khảo tại man 7 sched

$ whatis sched
sched (7)            - overview of CPU scheduling

Trong tài liệu viết:

Scheduling policies
   The scheduler is the kernel component that decides which runnable thread
   will be executed by the CPU next.  Each thread has an associated
   scheduling policy and a  static scheduling  priority, sched_priority.  The
   scheduler makes  its  decisions  based  on knowledge  of  the  scheduling
   policy and static priority of all threads on the sys‐ tem.
...
API summary
   Linux provides the following system  calls for  controlling the CPU
   scheduling behavior, policy,  and  priority  of  processes (or, more
   precisely, threads).

Linux kernel scheduler sắp xếp lịch chạy trên CPU cho các thread (hay gọi là task). Trong man 1 taskset viết:

-a, --all-tasks
  Set or retrieve the CPU affinity of all the tasks (threads) for a given PID.

Một process chạy multithreading trên mấy CPU core?

Với 10 process, mỗi process chỉ có 1 thread, sẽ là 10 thread cần chạy, kernel sẽ sched (xếp lịch) việc chạy 10 task này cho N CPU. Tương tự 1 process, chạy 10 thread, kernel cũng sẽ sched việc chạy 10 task này cho N CPU (N > 0).

What?!

Python multi-threaded vs multi-process

Dòng thứ 2 trong tài liệu thư viện threading của Python viết

CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.

Python là một trong số ít ngôn ngữ mà nhiều thread không chạy được trên nhiều CPU core cùng lúc do giới hạn của Global Interpreter Lock - GIL trong CPython/PyPy. Giới hạn này KHÔNG tồn tại trong các bản Python khác như Jython (trên JVM) và IronPython (trên .NET). Vì GIL, CPython chỉ có thể chạy trên CPU 1 thread 1 lúc, nên muốn chạy nhiều thread/process trên nhiều CPU core cùng lúc, Python có thư viện multiprocessing.

multiprocessing is a package that supports spawning processes using an API similar to the threading module. The multiprocessing package offers both local and remote concurrency, effectively side-stepping the Global Interpreter Lock by using subprocesses instead of threads. Due to this, the multiprocessing module allows the programmer to fully leverage multiple processors on a given machine.

Tránh nhầm lẫn rằng python threading thực sự chạy các thread cùng lúc trên nhiều CPU core, việc các thread có vẻ chạy cùng lúc trong Python chỉ là multitasking, chạy chuyển đổi giữa các thread.

Java multithreading

Java hỗ trợ multithreading với các thread chạy cùng lúc như mong đợi, và nhiều thread này hoàn toàn có thể được chạy trên nhiều CPU core.

Vì multithreading chạy rất ngon lành, nên ít có lý do gì để sinh ra khái niệm "multiprocessing" như Python. Ngoài ra, bật 1 Java process là chạy 1 máy ảo JVM nặng nề, khởi động chậm (so với bật 1 process CPython interpreter 0.1s 8MB RAM) nên việc này rất ít thấy trong thực tế.

PS: Python threading API dựa trên API của Java

Rust multithreading

Tương tự Java, không tồn tại thư viện "multiprocessing" trong Rust.

In most current operating systems, an executed program’s code is run in a process, and the operating system will manage multiple processes at once. Within a program, you can also have independent parts that run simultaneously. The features that run these independent parts are called threads. For example, a web server could have multiple threads so that it could respond to more than one request at the same time.

https://doc.rust-lang.org/book/ch16-01-threads.html

Go multithreading, multiprocessing

Go không dùng khái niệm process hay thread của hệ điều hành mà dùng khái niệm Goroutine, tương tự thread, nhưng do Go runtime quản lý thay vì OS kernel.

A goroutine is a lightweight thread managed by the Go runtime. Goroutines run in the same address space, so access to shared memory must be synchronized.

https://go.dev/tour/concurrency/1

Các goroutine cũng có thể được nhiều CPU core chạy cùng lúc

GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting.

Khi chạy CPU bound, dùng multi process hay multi thread?

  • CPU bound là chương trình dành phần lớn thời gian dùng CPU xử lý, khác với
  • IO bound là chương trình dành phần lớn thời gian đọc ghi file/network.

Câu hỏi này có thể là trap, cần hỏi lại dùng ngôn ngữ gì, trừ khi hỏi cụ thể tới Python thì trả lời dùng multiprocessing. Trong các ngôn ngữ khác như Rust/Java, multithreading là câu trả lời, vì không có thư viện multi-process mà chạy. Hay Go chỉ có goroutine chứ không có lựa chọn khác.

Khi nói chung chung, multi-process có ưu điểm là sự tách biệt giữa các process, một process bị crash sẽ không ảnh hưởng tới process khác, nhược điểm là việc giao tiếp giữa các process để chia sẻ data sẽ phức tạp. Nhiều chương trình dùng mô hình này như:

  • postgresql
  • nginx master-workers

Multi-threaded giúp dễ dàng truy cập bộ nhớ chung, nhưng có thể gặp trường hợp 1 thread crash khiến cả chương trình tắt ngóm, nhược điểm là dễ xảy ra race-condition: N thread tranh nhau truy cập cùng 1 tài nguyên.

Không có câu trả lời dễ dàng, vì đây là trường hợp của PostgreSQL, sau vài chục năm chạy multi-process, nay đang khám phá option multi-threaded.

Let's make PostgreSQL multi-threaded

Tham khảo

Kết luận

Thread là đơn vị task được kernel sched chạy trên nhiều CPU, process là 1 chương trình đang chạy.

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺

Monday, 19 June 2023

ps và top hiển thị số thread

Người dùng Linux hẳn đều biết dùng lệnh ps -ef hay top để xem các "chương trình" đang chạy. Chương trình trên Linux thường ám chỉ 1 (vài) process đang chạy, mỗi process được gắn số ProcessID (PID).

Một process có thể chạy nhiều OS thread, để xem số lượng thread (NLWP) hay thread ID (LWP), thêm option -L

Xem thread với ps

$ ps -efL | grep python3
# Dòng này được thêm vào cho dễ hiểu
UID          PID    PPID     LWP  C NLWP STIME TTY          TIME CMD
--------------------------------------------------------------------------------
hvn         3818    1585    3818  0    2 21:28 pts/0    00:00:00 python3 main.py
hvn         3818    1585    3819  0    2 21:28 pts/0    00:00:00 python3 main.py

PID là 3818, 2 thread ID lần lượt là 3818 và 3819.

Code Python3

import time
from threading import Thread

class MyThread(Thread):
    def run(self):
        while True:
            time.sleep(2)
            print("running")

t = MyThread()
t.start()

t.join()

Xem thread với top

Trong top, bấm chữ H (hoa) để hiển thị thread, cột PID lúc này sẽ hiển thị threadID.

$ top -Hbn1 | grep python3
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   3979 hvn       20   0   87912  10232   4900 S   0.0   0.1   0:00.05 python3
   3980 hvn       20   0   87912  10232   4900 S   0.0   0.1   0:00.00 python3
$ ps -efL | grep python3
UID          PID    PPID     LWP  C NLWP STIME TTY          TIME CMD
hvn         3979    1585    3979  0    2 21:35 pts/0    00:00:00 python3 main.py
hvn         3979    1585    3980  0    2 21:35 pts/0    00:00:00 python3 main.py

Tham khảo

  • man top
      -H  :Threads-mode operation Instructs top to display individual threads.
      Without this command-line option a summation of all threads  in  each
      process  is shown.  Later this can be changed with the `H' interactive
      command.
  • man ps
       -f     Do full-format listing.  This option can be combined with many
       other UNIX-style options to add additional columns.  It also causes the
       command arguments to be printed.  When used with -L, the NLWP (number of
               threads) and LWP (thread ID) columns will be added.  See the c
       option, the format keyword args, and the format keyword comm.

Kết luận

Có thể nhìn tận mắt thread đang chạy với ps và top.

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺

Friday, 9 June 2023

Thảm họa PyYAML

PyYAML, thư viện parse YAML phổ biến bậc nhất của Python, được dùng trong các phần mềm dùng YAML như Ansible, SaltStack, ...

> YAML: YAML Ain't Markup Language™

Ví dụ Salt state:

install_network_packages:
  pkg.installed:
    - pkgs:
      - rsync
      - lftp
      - curl

Mặc định nguy hiểm

YAML không phải một ngôn ngữ/format đơn giản như JSON, nó có hàng tá tính năng mà có thể bạn không biết tới. Dùng YAML giống như dùng pickle hơn là json (có thể chứa object tùy ý).

def double(x):
    return x+x
import sys
yaml.dump(double, sys.stdout)
# !!python/name:__main__.double ''

Function mặc định (và siêu phổ biến) để đọc file YAML: yaml.load, có thể chạy code Python, và là tác giả của hàng loạt lỗ hổng bảo mật được gắn CVE:

  • CVE-2017-18342 In PyYAML before 5.1, the yaml.load() API could execute arbitrary code if used with untrusted data. The load() function has been deprecated in version 5.1 and the 'UnsafeLoader' has been introduced for backward compatibility with the function.
  • Rất nhiều nữa https://www.opencve.io/cve?vendor=pyyaml...

Đến sau 2017, người dùng mới bắt đầu làm quen với function mới yaml.safe_load, an toàn hơn.

Cài pip install 'pyyaml<5.1' để trải nghiệm CVE này:

python3 -c 'import yaml; yaml.load("!!python/object/new:os.system [echo EXPLOIT!]")'                              [0]
EXPLOIT!

Theo https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation

Phức tạp, nhiều phiên bản

JSON có... 1 phiên bản duy nhất?!!

YAML có 1.0 1.1 1.2 và còn nữa. Phiên bản 1.2 có từ 2009, mà PyYAML chỉ hỗ trợ YAML 1.1

>   - YAML 1.2:
    - Revision 1.2.2      # Oct 1, 2021 *New*
    - Revision 1.2.1      # Oct 1, 2009
    - Revision 1.2.0      # Jul 21, 2009
  - YAML 1.1

Muốn dùng YAML 1.2, phải dùng ruamel.yaml

  - PyYAML        # YAML 1.1, pure python and libyaml binding
  - ruamel.yaml   # YAML 1.2, update of PyYAML; comments round-trip

PyYAML 6.0+ không tương thích phiên bản cũ

Phiên bản mới nhất 6.0, ra đời năm 2021, không tương thích với code các bản cũ.

6.0

import yaml
yaml.load("key: value", Loader=yaml.CLoader)

CHỮ L ở Loader= VIẾT HOA

5

import yaml
yaml.load("key: value")

bản 6 bắt buộc argument Loader phải được set. Tới tháng 6 2023, tài liệu tutorial trang chủ vẫn không update https://pyyaml.org/wiki/PyYAMLDocumentation:

>>> yaml.load("""
... - Hesperiidae
... - Papilionidae
... - Apatelodidae
... - Epiplemidae
... """)

Issue GitHub: https://github.com/yaml/pyyaml/issues/576

May thay nếu dùng yaml.safe_load thì không đổi, nó sẽ gọi Loader=yaml.SafeLoader)

def safe_load(stream):
    """
    Parse the first YAML document in a stream
    and produce the corresponding Python object.

    Resolve only basic YAML tags. This is known
    to be safe for untrusted input.
    """
    return load(stream, SafeLoader)

Tham khảo

Kết luận

YAML không đơn giản như JSON, nếu không nhất thiết, chớ động vào.

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺