Semaphore: nó là gì, nguyên lý hoạt động và ứng dụng trong đồng bộ hóa luồng

Tác giả: IT Sectr Đã đăng: 2026-03-19 Thời gian đọc: 8 phút

Semaphore là một nguyên thủy đồng bộ hóa kiểm soát quyền truy cập vào tài nguyên dùng chung thông qua bộ đếm và hàng đợi các luồng đang chờ. Theo Wikipedia, 2024, semaphore được Edsger Dijkstra đề xuất vào năm 1965 để giải quyết các vấn đề tương tác đa luồng. Công cụ này cho phép giới hạn số lượng luồng làm việc đồng thời với một phần quan trọng.

Những điểm chính

  • Semaphore là một nguyên thủy đồng bộ hóa kiểm soát quyền truy cập thông qua bộ đếm quyền.
  • Semaphore nhị phân nhận giá trị 0 và 1, hoạt động như một cờ chặn.
  • Semaphore đếm cho phép truy cập đồng thời cho một số luồng xác định.
  • Khác với mutex, semaphore không bị ràng buộc với một luồng sở hữu.
  • Deadlock là một trong những nguy hiểm chính khi sử dụng semaphore không đúng cách.

Semaphore là gì?

Semaphore là một nguyên thủy đồng bộ hóa sử dụng bộ đếm để kiểm soát quyền truy cập vào tài nguyên dùng chung. Khái niệm này được Edsger Dijkstra đề xuất vào năm 1965 và trở thành nền tảng cho tất cả các cơ chế đồng bộ hóa hiện đại trong hệ điều hành.

Định nghĩa và mục đích

Semaphore là một biến số nguyên với hai thao tác nguyên tử: wait (acquire) và signal (release). Thao tác wait giảm bộ đếm, trong khi signal tăng nó. Khi bộ đếm về không, luồng gọi wait sẽ bị chặn cho đến khi một luồng khác thực thi signal.

Mục đích chính của semaphore là bảo vệ các phần quan trọng khỏi truy cập đồng thời của nhiều luồng. Không giống mutex, semaphore không yêu cầu ràng buộc với luồng sở hữu, làm cho nó phù hợp với nhiều tác vụ điều phối hơn.

Lịch sử và cơ sở lý thuyết

Khái niệm semaphore xuất hiện trong bối cảnh hệ điều hành THE, được phát triển tại Technische Hogeschool Eindhoven. Dijkstra đã chính thức hóa semaphore như một trừu tượng toán học, chứng minh tính đầy đủ của nó để thực hiện bất kỳ nguyên thủy đồng bộ hóa nào.

Semaphore hoạt động như thế nào?

Cơ chế semaphore dựa trên hai thao tác nguyên tử và một hàng đợi chờ nội bộ. Khi acquire được gọi, luồng kiểm tra giá trị bộ đếm và hoặc tiếp tục thực thi hoặc bị chặn cho đến khi tài nguyên được giải phóng.

Bộ đếm và thao tác nguyên tử

Khi tạo semaphore, một giá trị ban đầu của bộ đếm quyền được thiết lập. Mỗi lần gọi acquire giảm bộ đếm đi 1. Nếu bộ đếm trở thành âm sau đó, luồng bị chặn. Thao tác release tăng bộ đếm và đánh thức một trong các luồng đang chờ.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} đang làm việc")
    } finally {
        semaphore.release()
    }
}

Hàng đợi chờ và lập lịch

Khi một luồng gọi acquire với bộ đếm bằng không, hệ điều hành đặt nó vào hàng đợi FIFO của semaphore. Luồng chuyển sang trạng thái BLOCKED, không tiêu tốn thời gian CPU. Sau một lần gọi release, luồng đầu tiên trong hàng đợi chuyển sang trạng thái RUNNABLE và nhận được quyền truy cập vào tài nguyên.

Các loại semaphore

Trong lý thuyết đồng bộ hóa, hai loại semaphore chính được phân biệt: nhị phân và đếm. Việc chọn loại phụ thuộc vào tác vụ quản lý truy cập tài nguyên cụ thể.

Semaphore nhị phân (Binary Semaphore)

Semaphore nhị phân chỉ nhận giá trị 0 và 1. Về hành vi, nó giống mutex, nhưng không có yêu cầu sở hữu — bất kỳ luồng nào cũng có thể thực thi release. Những semaphore này thuận tiện để thực hiện cờ sẵn sàng và sự kiện giữa các luồng.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("Dữ liệu đã sẵn sàng")
}

Semaphore đếm (Counting Semaphore)

Semaphore đếm có thể nhận bất kỳ giá trị không âm nào. Nó được sử dụng để quản lý một nhóm tài nguyên tương tự có nhiều phiên bản. Ví dụ, một nhóm 5 kết nối mạng: mỗi lần acquire lấy một kết nối, release trả nó về nhóm.

Semaphore đếm không thể thiếu để giới hạn tốc độ truy cập vào các dịch vụ bên ngoài và thực hiện nhóm luồng. Chúng cho phép kiểm soát chính xác mức độ song song mà không cần quản lý luồng thủ công.

Tham sốSemaphore nhị phânSemaphore đếm
Phạm vi0 hoặc 1từ 0 đến N
Luồng đồng thời1lên đến N
Ứng dụngtín hiệu, cờnhóm tài nguyên, giới hạn tốc độ

Semaphore vs Mutex

Các nhà phát triển thường nhầm lẫn semaphore và mutex, mặc dù có sự khác biệt cơ bản giữa chúng. Hiểu những khác biệt này rất quan trọng để chọn cơ chế đồng bộ hóa phù hợp trong một dự án.

Nguyên tắc sở hữu

Sự khác biệt chính là khái niệm sở hữu. Mutex luôn biết luồng nào đã chiếm giữ nó và chỉ luồng đó mới có thể giải phóng nó. Semaphore không có chủ sở hữu: bất kỳ luồng nào cũng có thể gọi release mà không cần gọi acquire. Điều này làm cho mutex an toàn hơn để bảo vệ dữ liệu và semaphore linh hoạt hơn để điều phối.

Hiệu suất và trường hợp sử dụng

Trong thực tế, mutex nhanh hơn cho loại trừ lẫn nhau đơn giản nhờ tối ưu hóa cho các kịch bản điển hình. Semaphore yêu cầu chi phí bổ sung để duy trì bộ đếm. Tuy nhiên, để giới hạn song song hoặc thực hiện mô hình nhà sản xuất-người tiêu dùng, semaphore là không thể thiếu.

Đặc điểmSemaphoreMutex
Sở hữukhông có chủ sở hữucó chủ sở hữu
Giải phóngbất kỳ luồng nàochỉ luồng sở hữu
Bộ đếm0 đến Nnhị phân
Trường hợp sử dụnggiới hạn song song và tín hiệubảo vệ phần quan trọng
Đệ quykhôngcó (có thể vào lại)

Semaphore trong phát triển di động

Trong phát triển ứng dụng di động, Semaphore được sử dụng để quản lý quyền truy cập vào các tài nguyên hạn chế: kết nối mạng, tệp tin, cơ sở dữ liệu và thành phần phần cứng. Các nền tảng hiện đại cung cấp các triển khai tích hợp thuận tiện.

Nhóm kết nối máy chủ

Một trường hợp sử dụng điển hình là nhóm kết nối HTTP. Ứng dụng có thể gửi không quá 4 yêu cầu đồng thời đến máy chủ vì API của nhà cung cấp giới hạn song song. Một semaphore với giá trị ban đầu 4 đảm bảo rằng dưới bất kỳ tải nào, số lượng yêu cầu đồng thời không vượt quá giới hạn, trong khi các luồng khác chờ trong hàng đợi.

Không có semaphore, sự gia tăng đột ngột trong hoạt động người dùng có thể gây quá tải bất ngờ cho cơ sở hạ tầng máy chủ, dẫn đến hết thời gian chờ và lỗi 429 Too Many Requests. Semaphore hoạt động như một cầu chì, cho phép một số lượng cuộc gọi đồng thời được chỉ định chặt chẽ bất kể số lượng luồng hoạt động.

Semaphore trong Kotlin cho Android

Android cung cấp lớp Semaphore từ gói java.util.concurrent. Hãy xem ví dụ về giới hạn yêu cầu mạng đồng thời xuống hai luồng để ngăn quá tải máy chủ.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

DispatchSemaphore trong Swift cho iOS

Trong iOS, DispatchSemaphore từ GCD giải quyết cùng một tác vụ. Các nhà phát triển sử dụng nó để đồng bộ hóa quyền truy cập vào tài nguyên trong mã bất đồng bộ mà không chặn luồng chính.

swift
let semaphore = DispatchSemaphore(value: 3)

func processBatch(_ items: [UIImage]) {
    for img in items {
        semaphore.wait()
        DispatchQueue.global().async {
            applyFilter(to: img)
            semaphore.signal()
        }
    }
}

Lỗi điển hình khi làm việc với semaphore

Lỗi phổ biến nhất là quên release khi xảy ra ngoại lệ. Nếu một luồng kết thúc với lỗi trước khi gọi release, semaphore sẽ bị chặn vĩnh viễn cho các luồng khác. Sử dụng try/finally hoặc defer để đảm bảo giải phóng. Vấn đề thứ hai là deadlock khi chiếm giữ nhiều semaphore theo thứ tự khác nhau bởi các luồng khác nhau.

Mẫu sử dụng semaphore

Semaphore không chỉ được sử dụng để bảo vệ dữ liệu mà còn để điều phối luồng trong các kịch bản đa luồng phức tạp. Biết các mẫu phổ biến giúp tăng tốc phát triển và giảm khả năng xảy ra lỗi đồng bộ hóa.

Có một số mẫu đã được chứng minh cho việc sử dụng semaphore trong các dự án thực tế. Biết chúng giúp tránh các lỗi điển hình và xây dựng các hệ thống đa luồng đáng tin cậy.

Bộ giới hạn tốc độ (Rate Limiter)

Một semaphore với giá trị ban đầu N và release định kỳ qua bộ đếm thời gian thực hiện giới hạn tốc độ của các yêu cầu API. Ví dụ, dịch vụ cho phép 10 yêu cầu mỗi giây: semaphore bắt đầu ở 10, mỗi yêu cầu giảm bộ đếm và một TimerTask riêng biệt đưa bộ đếm về giá trị ban đầu mỗi giây. Điều này bảo vệ cả ứng dụng và máy chủ khỏi quá tải.

Nhà sản xuất-Người tiêu dùng qua semaphore

Trong bài toán nhà sản xuất-người tiêu dùng cổ điển, hai semaphore quản lý một bộ đệm: empty (quyền ghi) và full (quyền đọc). Nhà sản xuất gọi acquire trên empty và release trên full, trong khi người tiêu dùng làm ngược lại. Sơ đồ này đảm bảo người tiêu dùng không bao giờ đọc bộ đệm rỗng và nhà sản xuất không bao giờ làm tràn nó.

Sơ đồ tương tự này là nền tảng cho bộ đệm có giới hạn trong hệ điều hành — một bộ đệm vòng có kích thước cố định. Trong các ứng dụng di động, mẫu này được sử dụng để xử lý hàng đợi hình ảnh, tệp video và sự kiện phân tích.

Tiết lưu yêu cầu mạng

Semaphore được sử dụng thành công để tiết lưu cuộc gọi mạng trong các dịch vụ nền. Ví dụ, một ứng dụng phân tích gửi các gói sự kiện đến máy chủ. Nếu không giới hạn các luồng đồng thời trong thời gian tải đỉnh (khởi động ứng dụng, đồng bộ sau khi ngoại tuyến), số lượng yêu cầu đồng thời có thể vượt quá giới hạn máy chủ. Một semaphore với giá trị ban đầu 3 đảm bảo gửi suôn sẻ và ngăn chặn chặn phía máy chủ.

Câu hỏi thường gặp

Sự khác biệt giữa Semaphore và bộ đếm thông thường là gì?

Semaphore không chỉ là một bộ đếm, mà là một nguyên thủy đồng bộ hóa với các thao tác nguyên tử và hàng đợi chờ. Bộ đếm thông thường không chặn luồng và không đảm bảo tính nguyên tử của việc tăng khi nhiều luồng truy cập đồng thời.

Semaphore có thể gây ra deadlock không?

Có, deadlock có thể xảy ra khi chiếm giữ nhiều semaphore theo thứ tự khác nhau bởi các luồng khác nhau. Ví dụ, luồng A chiếm giữ S1, sau đó S2, trong khi luồng B chiếm giữ S2, sau đó S1. Hãy cố định một thứ tự chiếm giữ duy nhất cho tất cả các semaphore trong dự án.

Điều gì xảy ra khi gọi acquire với bộ đếm bằng không?

Luồng bị chặn và chuyển sang trạng thái chờ. Nó không tiêu tốn thời gian CPU cho đến khi một luồng khác gọi release. Trong Java, đây là trạng thái BLOCKED; trong Swift, luồng bị tạm dừng bởi GCD.

Binary Semaphore khác Mutex về cơ bản như thế nào?

Sự khác biệt chính là quyền sở hữu. Mutex chỉ có thể được giải phóng bởi luồng sở hữu. Binary Semaphore có thể được giải phóng bởi bất kỳ luồng nào, thuận tiện cho việc truyền tín hiệu giữa các luồng nhưng kém an toàn hơn để bảo vệ tính toàn vẹn dữ liệu.

Nên chọn giá trị ban đầu nào cho bộ đếm?

Giá trị ban đầu phụ thuộc vào kịch bản. Để bảo vệ một tài nguyên duy nhất — 1. Cho một nhóm N kết nối — N. Để truyền tín hiệu giữa các luồng, sử dụng 0 để luồng người tiêu dùng chờ tín hiệu từ nhà sản xuất.

Tổng kết

  • Semaphore là một nguyên thủy đồng bộ hóa dựa trên bộ đếm được Dijkstra đề xuất vào năm 1965.
  • Semaphore nhị phân nhận giá trị 0 và 1; semaphore đếm nhận bất kỳ giá trị không âm nào.
  • Các thao tác acquire và release là nguyên tử và an toàn cho luồng.
  • Khác với mutex, semaphore không bị ràng buộc với luồng sở hữu.
  • Semaphore đếm được sử dụng để quản lý nhóm tài nguyên và giới hạn song song.
  • Quên release là lỗi phổ biến nhất dẫn đến treo luồng.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm