信號量是GCD中最多見的操做,一般用於保證資源的多線程安全性。其本質其實是基於mach內核的信號量接口來實現的,本文將從源碼角度對其進行解析。html
@interface MyObject : NSObject
@property (nonatomic, strong) dispatch_semaphore_t sema;
@end
@implementation MyObject
@end
複製代碼
初始化信號量,而後能夠看到這樣一個結構:ios
myObj.sema = dispatch_semaphore_create(0);
// (lldb) po myObj.sema
// <OS_dispatch_semaphore: semaphore[0x6000007f14f0] = { xref = 1, ref = 1, port = 0x0, value = 0, orig = 0 }>
複製代碼
xref和ref是引用相關的。value和orig則是信號量執行任務的關鍵。執行一次dispatch_semaphore_wait操做後,value值會發生一次減操做。macos
dispatch_semaphore_wait(myObj.sema, DISPATCH_TIME_FOREVER);
// (lldb) po myObj.sema
// <OS_dispatch_semaphore: semaphore[0x60000133b890] = { xref = 2, ref = 1, port = 0x4007, value = -1, orig = 0 }>
複製代碼
那這些成員變量都是什麼意思呢?緩存
信號量的基本數據結構以下:安全
struct dispatch_semaphore_s {
DISPATCH_OBJECT_HEADER(semaphore);
long volatile dsema_value;
long dsema_orig;
_dispatch_sema4_t dsema_sema;
};
複製代碼
_DISPATCH_OBJECT_HEADER是一個宏定義:數據結構
#define DISPATCH_OBJECT_HEADER(x) \ struct dispatch_object_s _as_do[0]; \ _DISPATCH_OBJECT_HEADER(x)
複製代碼
_DISPATCH_OBJECT_HEADER以下:多線程
#define _DISPATCH_OBJECT_HEADER(x) \ struct _os_object_s _as_os_obj[0]; \ OS_OBJECT_STRUCT_HEADER(dispatch_##x); \ struct dispatch_##x##_s *volatile do_next; \ struct dispatch_queue_s *do_targetq; \ void *do_ctxt; \ void *do_finalizer
複製代碼
其中有兩個成員比較關鍵:app
struct dispatch_##x##_s *volatile do_next; \
struct dispatch_queue_s *do_targetq; \
複製代碼
這兩個在後邊會講到。less
dispatch_object_s對象也使用了_DISPATCH_OBJECT_HEADER:函數
struct dispatch_object_s {
_DISPATCH_OBJECT_HEADER(object);
};
/* * Dispatch objects are NOT C++ objects. Nevertheless, we can at least keep C++ * aware of type compatibility. */
typedef struct dispatch_object_s {
private:
dispatch_object_s();
~dispatch_object_s();
dispatch_object_s(const dispatch_object_s &);
void operator=(const dispatch_object_s &);
} *dispatch_object_t;
typedef union {
struct _os_object_s *_os_obj;
struct dispatch_object_s *_do;
struct dispatch_queue_s *_dq;
struct dispatch_queue_attr_s *_dqa;
struct dispatch_group_s *_dg;
struct dispatch_source_s *_ds;
struct dispatch_mach_s *_dm;
struct dispatch_mach_msg_s *_dmsg;
struct dispatch_semaphore_s *_dsema;
struct dispatch_data_s *_ddata;
struct dispatch_io_s *_dchannel;
} dispatch_object_t DISPATCH_TRANSPARENT_UNION;
複製代碼
dispatch_object_t是一個聯合體,libdispatch中的全部對象都須要使用到。
/*! * @function dispatch_semaphore_create * * @abstract * Creates new counting semaphore with an initial value. * * @discussion * Passing zero for the value is useful for when two threads need to reconcile * the completion of a particular event. Passing a value greater than zero is * useful for managing a finite pool of resources, where the pool size is equal * to the value. * * @param value * The starting value for the semaphore. Passing a value less than zero will * cause NULL to be returned. * * @result * The newly created semaphore, or NULL on failure. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT DISPATCH_NOTHROW dispatch_semaphore_t dispatch_semaphore_create(long value);
複製代碼
參數value若是小於0,則無效。其實現源碼以下:
dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
dispatch_semaphore_t dsema;
// If the internal value is negative, then the absolute of the value is
// equal to the number of waiting threads. Therefore it is bogus to
// initialize the semaphore with a negative value.
if (value < 0) {
return DISPATCH_BAD_INPUT;
}
dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
sizeof(struct dispatch_semaphore_s));
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
// 目標隊列
dsema->do_targetq = _dispatch_get_default_queue(false);
// 當前值
dsema->dsema_value = value;
_dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
// 初始值
dsema->dsema_orig = value;
return dsema;
}
複製代碼
_dispatch_object_alloc的第一個參數DISPATCH_VTABLE(semaphore),設置了dispatch_semaphore_t的相關回調函數,如銷燬函數_dispatch_semaphore_dispose。
void *
_dispatch_object_alloc(const void *vtable, size_t size)
{
#if OS_OBJECT_HAVE_OBJC1
const struct dispatch_object_vtable_s *_vtable = vtable;
dispatch_object_t dou;
dou._os_obj = _os_object_alloc_realized(_vtable->_os_obj_objc_isa, size);
dou._do->do_vtable = vtable;
return dou._do;
#else
return _os_object_alloc_realized(vtable, size);
#endif
}
複製代碼
_os_object_alloc_realized函數以下,其中會調用_os_objc_alloc函數。
_os_object_t
_os_object_alloc_realized(const void *cls, size_t size)
{
dispatch_assert(size >= sizeof(struct _os_object_s));
return _os_objc_alloc(cls, size);
}
複製代碼
static inline id
_os_objc_alloc(Class cls, size_t size)
{
id obj;
size -= sizeof(((struct _os_object_s *)NULL)->os_obj_isa);
while (unlikely(!(obj = class_createInstance(cls, size)))) {
_dispatch_temporary_resource_shortage();
}
return obj;
}
複製代碼
DISPATCH_VTABLE的定義以下:
#define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)
// vtable symbols
#define OS_OBJECT_VTABLE(name) (&OS_OBJECT_CLASS_SYMBOL(name))
#define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))
#define DISPATCH_CLASS_SYMBOL(name) OS_dispatch_##name##_class
複製代碼
實際上, DISPATCH_VTABLE(semaphore) 即爲 &OS_dispatch_semaphore_class。
另外還有一個宏DISPATCH_VTABLE_INSTANCE,
DISPATCH_VTABLE_INSTANCE(semaphore,
.do_type = DISPATCH_SEMAPHORE_TYPE,
.do_dispose = _dispatch_semaphore_dispose,
.do_debug = _dispatch_semaphore_debug,
.do_invoke = _dispatch_object_no_invoke,
);
#define DISPATCH_VTABLE_INSTANCE(name, ...) \ DISPATCH_VTABLE_SUBCLASS_INSTANCE(name, name, __VA_ARGS__)
#define DISPATCH_VTABLE_SUBCLASS_INSTANCE(name, ctype, ...) \ OS_OBJECT_VTABLE_SUBCLASS_INSTANCE(dispatch_##name, dispatch_##ctype, \ _dispatch_xref_dispose, _dispatch_dispose, __VA_ARGS__)
// vtables for proper classes
#define OS_OBJECT_VTABLE_INSTANCE(name, xdispose, dispose, ...) \ OS_OBJECT_VTABLE_SUBCLASS_INSTANCE(name, name, \ xdispose, dispose, __VA_ARGS__)
#define OS_OBJECT_VTABLE_SUBCLASS_INSTANCE(name, ctype, xdispose, dispose, ...) \ __attribute__((section("__DATA,__objc_data"), used)) \ const struct ctype##_extra_vtable_s \ OS_OBJECT_EXTRA_VTABLE_SYMBOL(name) = { __VA_ARGS__ }
#define OS_OBJECT_EXTRA_VTABLE_SYMBOL(name) _OS_##name##_vtable
複製代碼
這一堆宏定義,繞來繞去,意圖就是創建一個vtable。vtable是虛函數表,能夠經過索引方式來快速獲取方法。相比於OC的方法查找,vtable的方式會有極大的性能提高。Swift中就大量使用了相似的vtable機制,如。
// 查處方法
let method = MyClass.vtable[methodIndex]
// 調用方法
method()
複製代碼
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_sema4_create(_dispatch_sema4_t *sema, int policy)
{
if (!_dispatch_sema4_is_created(sema)) {
_dispatch_sema4_create_slow(sema, policy);
}
}
複製代碼
_dispatch_sema4_create_slow的源碼以下:
#define _dispatch_sema4_is_created(sema) (*(sema) != MACH_PORT_NULL)
void
_dispatch_sema4_create_slow(_dispatch_sema4_t *s4, int policy)
{
semaphore_t tmp = MACH_PORT_NULL;
_dispatch_fork_becomes_unsafe();
// lazily allocate the semaphore port
// Someday:
// 1) Switch to a doubly-linked FIFO in user-space.
// 2) User-space timers for the timeout.
#if DISPATCH_USE_OS_SEMAPHORE_CACHE
if (policy == _DSEMA4_POLICY_FIFO) {
tmp = (_dispatch_sema4_t)os_get_cached_semaphore();
if (!os_atomic_cmpxchg(s4, MACH_PORT_NULL, tmp, relaxed)) {
os_put_cached_semaphore((os_semaphore_t)tmp);
}
return;
}
#endif
kern_return_t kr = semaphore_create(mach_task_self(), &tmp, policy, 0);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
if (!os_atomic_cmpxchg(s4, MACH_PORT_NULL, tmp, relaxed)) {
kr = semaphore_destroy(mach_task_self(), tmp);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
}
複製代碼
若是用到了信號量緩存(DISPATCH_USE_OS_SEMAPHORE_CACHE),且是FIFO,則會直接從cache中取出信號量來使用。
不然,就使用semaphore_create來建立一個新的信號量。
wait操做,會將信號量的值減一,若減操做後的結果值爲負數,則函數會一直等待信號量的釋放。
/*! * @function dispatch_semaphore_wait * * @abstract * Wait (decrement) for a semaphore. * * @discussion * Decrement the counting semaphore. If the resulting value is less than zero, * this function waits for a signal to occur before returning. * * @param dsema * The semaphore. The result of passing NULL in this parameter is undefined. * * @param timeout * When to timeout (see dispatch_time). As a convenience, there are the * DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants. * * @result * Returns zero on success, or non-zero if the timeout occurred. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
複製代碼
源碼實現以下:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) {
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
複製代碼
dispatch_semaphore_wait一開始調用了系統的原子操做os_atomic_dec2o,將信號量的值減一。該操做以後,若信號量不是負數,則依然有信號量資源可用。若爲負數,則執行_dispatch_semaphore_wait_slow進入等待。
DISPATCH_NOINLINE
static long
_dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema,
dispatch_time_t timeout)
{
long orig;
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
switch (timeout) {
default:
if (!_dispatch_sema4_timedwait(&dsema->dsema_sema, timeout)) {
break;
}
// Fall through and try to undo what the fast path did to
// dsema->dsema_value
case DISPATCH_TIME_NOW:
orig = dsema->dsema_value;
while (orig < 0) {
if (os_atomic_cmpxchgvw2o(dsema, dsema_value, orig, orig + 1,
&orig, relaxed)) {
return _DSEMA4_TIMEOUT();
}
}
// Another thread called semaphore_signal().
// Fall through and drain the wakeup.
case DISPATCH_TIME_FOREVER:
_dispatch_sema4_wait(&dsema->dsema_sema);
break;
}
return 0;
}
複製代碼
_dispatch_semaphore_wait_slow函數根據timeout來決定等待行爲,
_dispatch_sema4_wait和_dispatch_sema4_timedwait的源碼以下,
void
_dispatch_sema4_wait(_dispatch_sema4_t *sema)
{
kern_return_t kr;
do {
kr = semaphore_wait(*sema);
} while (kr == KERN_ABORTED);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
bool
_dispatch_sema4_timedwait(_dispatch_sema4_t *sema, dispatch_time_t timeout)
{
mach_timespec_t _timeout;
kern_return_t kr;
do {
uint64_t nsec = _dispatch_timeout(timeout);
_timeout.tv_sec = (typeof(_timeout.tv_sec))(nsec / NSEC_PER_SEC);
_timeout.tv_nsec = (typeof(_timeout.tv_nsec))(nsec % NSEC_PER_SEC);
kr = semaphore_timedwait(*sema, _timeout);
} while (unlikely(kr == KERN_ABORTED));
if (kr == KERN_OPERATION_TIMED_OUT) {
return true;
}
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
return false;
}
複製代碼
其中調用了mach內核的信號量接口semaphore_wait和semaphore_timedwait進行wait操做。因此,GCD的信號量其實是基於mach內核的信號量接口來實現。semaphore_timedwait函數便可以指定超時時間。
dispatch_semaphore_signal負責釋放信號量。
/*! * @function dispatch_semaphore_signal * * @abstract * Signal (increment) a semaphore. * * @discussion * Increment the counting semaphore. If the previous value was less than zero, * this function wakes a waiting thread before returning. * * @param dsema The counting semaphore. * The result of passing NULL in this parameter is undefined. * * @result * This function returns non-zero if a thread is woken. Otherwise, zero is * returned. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
複製代碼
dispatch_semaphore_signal的源碼以下:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema) {
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
複製代碼
dispatch_semaphore_signal操做,則是執行了原子操做os_atomic_inc2o,將信號量的值加一。若過分釋放,致使信號量的值爲LONG_MIN,則會觸發crash,信息爲 ***Unbalanced call to dispatch_semaphore_signal()***。因此,跟GCD group的enter/leave相似,過分調用dispatch_semaphore_signal,理論上來講會致使崩潰。但並未實際復現出來,很奇怪。
而_dispatch_semaphore_signal_slow實際上會調用mach內核的semaphore_signal函數。
DISPATCH_NOINLINE
long
_dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema)
{
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
_dispatch_sema4_signal(&dsema->dsema_sema, 1);
return 1;
}
void
_dispatch_sema4_signal(_dispatch_sema4_t *sema, long count)
{
do {
kern_return_t kr = semaphore_signal(*sema);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
} while (--count);
}
複製代碼
semaphore_signal可以喚醒一個在semaphore_wait中等待的線程。若是有多個等待線程,則根據線程優先級來喚醒。
信號量的銷燬函數以下:
void
_dispatch_semaphore_dispose(dispatch_object_t dou,
DISPATCH_UNUSED bool *allow_free)
{
dispatch_semaphore_t dsema = dou._dsema;
if (dsema->dsema_value < dsema->dsema_orig) {
DISPATCH_CLIENT_CRASH(dsema->dsema_orig - dsema->dsema_value,
"Semaphore object deallocated while in use");
}
_dispatch_sema4_dispose(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
}
複製代碼
這裏有一個判斷,若 dsema->dsema_value < dsema->dsema_orig,則致使崩潰,並提示 Semaphore object deallocated while in use。這也是容易遇到的問題之一,後邊會講到。
_dispatch_sema4_dispose代碼以下:
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_sema4_dispose(_dispatch_sema4_t *sema, int policy)
{
if (_dispatch_sema4_is_created(sema)) {
_dispatch_sema4_dispose_slow(sema, policy);
}
}
複製代碼
進一步往下,會調用_dispatch_sema4_dispose_slow函數,
void
_dispatch_sema4_dispose_slow(_dispatch_sema4_t *sema, int policy)
{
semaphore_t sema_port = *sema;
*sema = MACH_PORT_DEAD;
#if DISPATCH_USE_OS_SEMAPHORE_CACHE
if (policy == _DSEMA4_POLICY_FIFO) {
return os_put_cached_semaphore((os_semaphore_t)sema_port);
}
#endif
kern_return_t kr = semaphore_destroy(mach_task_self(), sema_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
複製代碼
若是使用了信號量緩存,且FIFO,則將待回收的信號量對象放入緩存便可。不然,調用mach內核的semaphore_destroy函數進行信號量的銷燬。
跟GCD group的enter/leave相似,這一類接口要保證操做的平衡。不然可能致使嚴重的問題。蘋果的文檔已經說得很清楚了:
Calls to dispatch_semaphore_signal must be balanced with calls to dispatch_semaphore_wait. Attempting to dispose of a semaphore with a count lower than value causes an EXC_BAD_INSTRUCTION exception.
這一點暫時未能復現。不過看原理,應該就是過分調用dispatch_semaphore_signal致使的。
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001a3b77ff4
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [249]
Triggered by Thread: 15
Application Specific Information:
BUG IN CLIENT OF LIBDISPATCH: Semaphore object deallocated while in use
Abort Cause 1
Thread 15 name: Dispatch queue: com.xxxx.xxxx.xxxxQueue (QOS: UNSPECIFIED)
Thread 15 Crashed:
0 libdispatch.dylib 0x00000001a3b77ff4 0x1a3b75000 + 12276
1 libdispatch.dylib 0x00000001a3b77014 0x1a3b75000 + 8212
2 libobjc.A.dylib 0x00000001a336e7cc 0x1a336a000 + 18380
3 libobjc.A.dylib 0x00000001a337e6b8 0x1a336a000 + 83640
4 libobjc.A.dylib 0x00000001a337e720 0x1a336a000 + 83744
5 XXXX 0x000000010535056c -[MyXXXXObject dealloc] + 70960492 (MyXXXXObject.mm:xx)
6 XXXX 0x000000010535171c __destroy_helper_block_ea8_32s40s48s56s64r + 70965020 (MyXXXXObject.mm:xxx)
7 libsystem_blocks.dylib 0x00000001a3c30a44 0x1a3c30000 + 2628
8 Foundation 0x00000001a4b09410 0x1a4aea000 + 128016
9 Foundation 0x00000001a4b97330 0x1a4aea000 + 709424
10 libsystem_blocks.dylib 0x00000001a3c30a44 0x1a3c30000 + 2628
11 libdispatch.dylib 0x00000001a3bd57d4 0x1a3b75000 + 395220
12 libdispatch.dylib 0x00000001a3b7a01c 0x1a3b75000 + 20508
13 libdispatch.dylib 0x00000001a3b796e0 0x1a3b75000 + 18144
14 libdispatch.dylib 0x00000001a3b86030 0x1a3b75000 + 69680
15 libdispatch.dylib 0x00000001a3b868d4 0x1a3b75000 + 71892
16 libsystem_pthread.dylib 0x00000001a3db61b4 0x1a3daa000 + 49588
17 libsystem_pthread.dylib 0x00000001a3db8cd4 0x1a3daa000 + 60628
複製代碼
這類crash的緣由在於對象釋放的時候,其內部持有的信號量對象依然在使用(即dsema->dsema_value < dsema->dsema_orig,信號量的值未經過signal操做恢復到其原始值)。常見於:其餘線程執行了wait操做,而在沒有對應signal的前提下,即將該信號量釋放了(持有信號量的對象釋放致使的)。
能夠經過下邊代碼來複現,以查看調用堆棧。
dispatch_semaphore_t sema = dispatch_semaphore_create(1);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
sema = dispatch_semaphore_create(1);
複製代碼
在信號量依然在使用的時候,從新給sema賦值就會致使崩潰,即使是 sema = nil; 也會。堆棧以下:
_dispatch_semaphore_dispose.cold.1
_disaptch_semaphore_dispose
_dispatch_dispose
sema = dispatch_semaphore_create(1);
複製代碼
固然,_disaptch_semaphore_dispose中會調用_dispatch_sema4_dispose_slow,這些咱們在以前代碼就已經分析過了。
解決辦法:若是對於已知場景的信號量,好比信號量初始值爲1的狀況,能夠在對象dealloc的時候手動執行一次signal操做,以避免出現 dsema->dsema_value < dsema->dsema_orig 的狀況。畢竟多執行了signal操做,目前是不會有問題的。
然而,實際的使用場景仍是要儘可能保證信號量的wait和signal保持平衡,這樣代碼邏輯纔不會有問題。