c++ 跨平台线程同步对象那些事儿——基于 ace

2022-08-31 10:17:10 浏览数 (2)

前言

ACE (Adaptive Communication Environment) 是早年间很火的一个 c 开源通讯框架,当时 c 的库比较少,以至于谈 c 网络通讯就绕不开 ACE,随着后来 boost::asio / libevent / libev … 等专门解决通讯框架的库像雨后春笋一样冒出来,ACE 就渐渐式微了。特别是它虽然号称是通讯框架,实则把各个平台的基础设施都封装了一个遍,导致想用其中一个部分,也牵一发而动全身的引入了一堆其它的不相关的部分,虽然用起来很爽,但是耦合度太强,学习曲线过于陡峭,以至于坊间流传一种说法:ACE 适合学习,不适合快速上手做项目。所以后来也就慢慢淡出了人们的视线,不过对于一个真的把它拿来学习的人来说,它的一些设计思想还是不错的,今天就以线程同步对象为例,说一下“史上最全”的 ACE 是怎么封装的,感兴趣的同学可以和标准库、boost 或任意什么跨平台库做个对比,看看它是否当得起这个称呼。

互斥量

互斥量主要就是指各种 mutex 了,依据 mutex 的各种特性,又细分为以下几类:

ACE_Thread_Mutex

这个主要是做进程内多线程同步的,底层类型为 ACE_thread_mutex_t,这个类型在不同平台上依赖的设施也不尽相同,可以列表如下:

平台/接口/设施

windows

unix like (pthread)

Solaris

VxWorks

unsupport

ACE_thread_mutex_t

CRITICAL_SECTION

pthread_mutex_t

mutex_t

SEM_ID

int

init

InitializeCriticalSection

pthread_mutex_init

mutex_init

semMCreate

n/a

acquire

EnterCriticalSection

pthread_mutex_lock

mutex_lock

semTake (..WAIT_FOREVER..)

n/a

acquire (..time..)

n/a

pthread_mutex_timedlock

n/a

semTake (..time..)

n/a

tryacquire

TryEnterCriticalSection

pthread_mutex_trylock

mutex_trylock

semTake (..NOWAIT..)

n/a

release

LeaveCriticalSection

pthread_mutex_unlock

mutex_unlock

semGive

n/a

remove

DeleteCriticalSection

pthread_mutex_destroy

mutex_destroy

semDelete

n/a

对于上面的表做个简单说明:

  • windows 上就是使用临界区来做线程级别的互斥量;
  • unix like 一般都支持 pthread,例如 AIX / HPUX / IRIX / LYNXOS / MACOSX / UNIXWARE / OPENBSD / FREEBSD ……,如果不支持 pthread,则不在此列;
  • Solaris 有自己的线程库,不使用 pthread;
  • VxWorks 实时操作系统只有一个进程,可以有多个线程 (任务),所以这里使用的是进程级别的同步对象来模拟,具体就是信号灯 (SEM_ID);
  • 对于没有 mutex 支持的系统,使用 int 来定义类别,函数体留空来避免编译报错 (相当于不起作用)。

另外由于线程同步对象没有对读写做分离,所以 acquire_read / acquire_write / tryacquire_read / tryacquire_write 均使用默认的 acquire / tryacquire 来实现。带超时参数的 acquire 重载,在有些平台并不被支持,例如 windows 和 Solaris。

ACE_Recursive_Thread_Mutex

与 ACE_Thread_Mutex 相比,增加了已锁定线程再次加锁的能力 (递归进入不死锁)。底层类型为 ACE_recursive_thread_mutex_t,与它相关的一些设施列表如下:

平台/接口/设施

windows

unix like (pthread)

Solaris

VxWorks

unsupport

ACE_recursive_thread_mutex_t

CRITICAL_SECTION

pthread_mutex_t

自定义类型模拟

自定义类型模拟

int

init

InitializeCriticalSection

pthread_mutex_init (..PTHREAD_MUTEX_RECURSIVE..)

参考自定义类型

参考自定义类型

n/a

acquire

EnterCriticalSection

pthread_mutex_lock

参考自定义类型

参考自定义类型

n/a

acquire (..time..)

n/a

pthread_mutex_timedlock

参考自定义类型

参考自定义类型

n/a

tryacquire

TryEnterCriticalSection

pthread_mutex_trylock

参考自定义类型

参考自定义类型

n/a

release

LeaveCriticalSection

pthread_mutex_unlock

参考自定义类型

参考自定义类型

n/a

remove

DeleteCriticalSection

pthread_mutex_destroy

参考自定义类型

参考自定义类型

n/a

get_thread_id

n/a

n/a

.owner_id_

.owner_id_

n/a

get_nesting_level

.RecursionCount / .LockCount 1

n/a

.nesting_level_

.nesting_level_

n/a

对于上面的表做个简单说明:

  • windows 的临界区默认就是递归的,所以直接拿来用没有一点儿问题;
  • 支持 pthread 的 unix like 系统,可以为 pthread_mutex_init 设置 PTHREAD_MUTEX_RECURSIVE 参数来指定互斥量是递归的 (当然了,创建 pthread mutex 还有一些其它选项,例如 PTHREAD_MUTEX_ERRORCHECK 可以做一些错误检测并返回错误,而不是直接死锁);
  • Solaris 系统的原生互斥量不支持递归加锁,这里使用自定义类型来模拟,其实只要是不支持递归互斥量的系统,都由这个自定义类型搞定,例如 VxWorks 等;
  • 相对于 ACE_Thread_Mutex,递归版本的增加了两个接口,分别是 get_thread_id 和 get_nesting_level,分别用来获取当前锁的拥有者线程 ID 和嵌套层次,不过貌似只有自定义类型全部支持。windows 的 CRITICAL_SECTION 可以支持后者,不过对于 32 位系统与 64 位系统有略微差别,前者使用 CRITICAL_SECTION 的 RecursionCount 字段,后者使用 LockCount 字段;
  • 对于没有 mutex 支持的系统,使用 int 来定义类别,函数体留空来避免编译报错 (相当于不起作用)。

带超时参数的 acquire 重载,在有些平台并不被支持,例如 windows。自定义类型的通用定义如下:

代码语言:javascript复制
 1 class ACE_recursive_thread_mutex_t
 2 {
 3 public:
 4   /// Guards the state of the nesting level and thread id.
 5   ACE_thread_mutex_t nesting_mutex_;
 6 
 7   /// This condition variable suspends other waiting threads until the
 8   /// mutex is available.
 9   ACE_cond_t lock_available_;
10 
11   /// Current nesting level of the recursion.
12   int nesting_level_;
13 
14   /// Current owner of the lock.
15   ACE_thread_t owner_id_;
16 };

而其中具体使用的平台设施,又随 ACE_thread_mutex_t / ACE_cond_t / ACE_thread_t 的定义而不同。关于如何基于非递归 mutex 与 condition variable 来实现递归 mutex,这个留在后面详细说明。

ACE_RW_Thread_Mutex

与  ACE_Thread_Mutex 相比,ACE_RW_Thread_Mutex 允许对读和写分别加锁,以提高读的并行程度 (读-写、写-写之间还是互斥的,读-读可以同时进入)。底层类型为 ACE_rwlock_t,与它相关的一些设施列表如下:

平台/接口/设施

windows

unix like (pthread)

Solaris

VxWorks

unsupport

ACE_rwlock_t

自定义类型模拟

pthread_rwlock_t

rwlock_t

自定义类型模拟

int

init

参考自定义类型

pthread_rwlock_init

rwlock_init

参考自定义类型

n/a

acquire_read

参考自定义类型

pthread_rwlock_rdlock

rw_rdlock

参考自定义类型

n/a

tryacquire_read

参考自定义类型

pthread_rwlock_tryrdlock

rw_tryrdlock

参考自定义类型

n/a

acquire_write

参考自定义类型

pthread_rwlock_wrlock

rw_wrlock

参考自定义类型

n/a

tryacquire_write

参考自定义类型

pthread_rwlock_trywrlock

rw_trywrlock

参考自定义类型

n/a

tryacquire_write_upgrade

参考自定义类型

n/a

n/a

参考自定义类型

n/a

release

参考自定义类型

pthread_rwlock_unlock

rw_unlock

参考自定义类型

n/a

remove

参考自定义类型

pthread_rwlock_destroy

rwlock_destroy

参考自定义类型

n/a

对于上面的表做个简单说明:

  • 支持 pthread 的 unix like 系统,可以直接基于 pthread_rwlock_t 原生类型进行封装;
  • Solaris 系统的原生读写锁 rwlock_t 本身就可以支持上述接口;
  • windows 没有读写锁原生支持,这里使用自定义类型来模拟,其实只要是不支持读写锁的系统,都由这个自定义类型搞定,例如 VxWorks ;
  • 读写锁的 acquire 分为 acquire_read / acquire_write 分别表示获取读锁与写锁;同理,tryacquire 也分为 tryacquire_read / tryacquire_write;而通用的 acquire 其实就是 acquire_write,tryacquire 就是 tryacquire_write;没有列出带超时参数的 acquire 重载,因为底层都不支持;另外读写锁还增加了一个 tryacquire_write_upgrade 接口,用来给已经获取读锁的线程升级为写锁,不过目前仅有模拟的自定义类型支持该接口;
  • 对于没有 mutex 支持的系统,使用 int 来定义类别,函数体留空来避免编译报错 (相当于不起作用)

该自定义类型的通用定义如下:

代码语言:javascript复制
 1 struct ACE_Export ACE_rwlock_t
 2 {
 3 public:
 4 //protected:
 5 
 6   ACE_mutex_t lock_;
 7   // Serialize access to internal state.
 8 
 9   ACE_cond_t waiting_readers_;
10   // Reader threads waiting to acquire the lock.
11 
12   int num_waiting_readers_;
13   // Number of waiting readers.
14 
15   ACE_cond_t waiting_writers_;
16   // Writer threads waiting to acquire the lock.
17 
18   int num_waiting_writers_;
19   // Number of waiting writers.
20 
21   int ref_count_;
22   // Value is -1 if writer has the lock, else this keeps track of the
23   // number of readers holding the lock.
24 
25   int important_writer_;
26   // indicate that a reader is trying to upgrade
27 
28   ACE_cond_t waiting_important_writer_;
29   // condition for the upgrading reader
30 };

而其中具体使用的平台设施,又随 ACE_mutex_t / ACE_cond_t 的定义而不同。关于如何使用 mutex 与 condition variable 来实现读写锁,这个留在后面详细说明。

ACE_Process_Mutex

这个主要是做进程间线程同步的,底层类型为 ACE_Mutex 或 ACE_SV_Semaphore_Complex,前者是通用的进程间互斥量,后者依赖 System V IPC 机制,默认使用前者,如果所在平台支持,可以通过定义宏 ACE_USES_MUTEX_FOR_PROCESS_MUTEX 来切换到后者,但是我看系统预定义的各平台头文件,都没有定义这个宏,所以还是重点看一下前者的实现。ACE_Mutex 底层类型为 ACE_mutex_t,这个类型在不同平台上依赖的设施也不尽相同,可以列表如下:

平台/接口/设施

windows

unix like (pthread)

Solaris

VxWorks

unsupport

ACE_mutex_t

HANDLE

pthread_mutex_t

mutex_t

SEM_ID

int

init

CreateMutex

pthread_mutex_init (..PTHREAD_PROCESS_SHARED..)

mutex_init

semMCreate

n/a

acquire

WaitForSingleObject (..INFINITE..)

pthread_mutex_lock

mutex_lock

semTake (..WAIT_FOREVER..)

n/a

acquire (..time..)

WaitForSingleObject (..time..)

pthread_mutex_timedlock

n/a

semTake (..time..)

n/a

tryacquire

WaitForSingleObject (..0..)

pthread_mutex_trylock

mutex_trylock

semTake(..NOWAIT..)

n/a

release

ReleaseMutex

pthread_mutex_unlock

mutex_unlock

semGive

n/a

remove

CloseHandle

pthread_mutex_destroy

mutex_destroy

semDelete

n/a

 对于上面的表做个简单说明:

  • windows 上就是使用互斥量来做进程级别的互斥;
  • 支持 pthread 的 unix like 系统,可以直接基于 pthread_mutex_t 原生类型进行封装,不过相比进程内互斥量,需要多做两个工作:
    • 创建或打开一块共享内存,在该内存上创建互斥量,需要使用该互斥量的进程,都打开这块共享内存进行操作;
    • 创建互斥量时指定 PTHREAD_PROCESS_SHARED 属性。
  • Solaris 自己的 mutex_t 就可以支持进程间的互斥,在 type 中指定 USYNC_PROCESS 标志位即可 (进程内的指定 USYNC_THREAD);
  • VxWorks 实时操作系统只有一个进程,所以无所谓进程间互斥量了,因此还是使用信号灯 SEM_ID 来模拟;
  • 对于没有 mutex 支持的系统,使用 int 来定义类别,函数体留空来避免编译报错 (相当于不起作用)。

另外由于线程同步对象没有对读写做分离,所以 acquire_read / acquire_write / tryacquire_read / tryacquire_write 均使用默认的 acquire / tryacquire 来实现。ACE_mutex_t 和 ACE_thread_mutex_t 的一个最大不同是,前者可以根据传入的 type 自动决定是使用进程内还是进程间的互斥量,例如在 windows 上,它的类型其实是一个 union:

代码语言:javascript复制
 1 typedef struct
 2 {
 3   /// Either USYNC_THREAD or USYNC_PROCESS
 4   int type_;
 5   union
 6   {
 7     HANDLE proc_mutex_;
 8     CRITICAL_SECTION thr_mutex_;
 9   };
10 } ACE_mutex_t;

使用 HANDLE 还是 CRITICAL_SECTION,完全由 type 决定,当然,在 ACE_Process_Mutex 中,是明确指定了 type 为 USYNC_PROCESS 的。

ACE_RW_Process_Mutex

与  ACE_RW_Thread_Mutex 相比,它提供了进程间多线程读写锁的能力。基于 ACE_File_Lock,而它的底层类型是 ace_flock_t,其实也是一个自定义类型,主要封装了不同平台上的文件锁,因此 ACE_RW_Thread_Mutex 其实只能做进程间的读写锁,而不能做进程内线程间的读写锁,这是因为一般的文件锁的粒度是到进程而不是线程的 (进程内多个线程去获取锁,都会得到锁已获取的结果,完全没有锁的效果)。与 ace_flock_t 相关的一些设施列表如下:

平台/接口/设施

windows

unix like (pthread)

Solaris

VxWorks

unsupport

ace_flock_t

HADNLE/OVERLAPPED

int/struct flock

int/struct flock

int/struct flock

int

init

CreateFile

open

open

n/a

n/a

acquire_read

LockFile[Ex]

fcntl (..F_RDLCK..F_SETLKW..)

fcntl (..F_RDLCK..F_SETLKW..)

n/a

n/a

tryacquire_read

LockFileEx (..LOCKFILE_FAIL_IMMEDIATELY..)

fcntl (..F_RDLCK..F_SETLK..)

fcntl (..F_RDLCK..F_SETLK..)

n/a

n/a

acquire_write

LockFileEx (..LOCKFILE_EXCLUSIVE_LOCK..)

fcntl (..F_WRLCK..F_SETLKW..)

fcntl (..F_WRLCK..F_SETLKW..)

n/a

n/a

tryacquire_write

LockFileEx (..LOCKFILE_FAIL_IMMEDIATELY | LOCKFILE_EXCLUSIVE_LOCK..)

fcntl (..F_WRLCK..F_SETLK..)

fcntl (..F_WRLCK..F_SETLK..)

n/a

n/a

tryacquire_write_upgrade

LockFileEx (..LOCKFILE_FAIL_IMMEDIATELY | LOCKFILE_EXCLUSIVE_LOCK..)

fcntl (..F_WRLCK..F_SETLK..)

fcntl (..F_WRLCK..F_SETLK..)

n/a

n/a

release

UnlockFile

fcntl (..F_UNLCK..F_SETLK..)

fcntl (..F_UNLCK..F_SETLK..)

n/a

n/a

remove

CloseHandle/DeleteFile

close/unlink

close/unlink

n/a

n/a

对于上面的表做个简单说明:

  • windows 系统基于原生的文件锁进行封装;
  • unix like 系统 (包含 Solaris) 可以直接基于 struct flock 原生类型进行封装;
  • 除了上面列出的接口,还有通用的 acquire 和 tryacquire,它们其实就是通过 acquire_write 和 tryacquire_write 来实现的;带超时参数的 acquire 重载没有列出,因为底层都不支持;另外 tryacquire_write_upgrade 接口底层是通过 tryacquire_write 来实现的,底层文件锁具备直接将读锁转化为写锁的接口;
  • 所有接口基本上都使用 start / whence / len 来指定锁定或解锁的文件范围,这个与其它锁参数还是有很大不同的,好在如果只是锁定文件第一个字节,ace 提供的默认值就够了,所以还能有一定通用性的 (可以在某些模板中通过不带参数的方式来调用);
  • 对于没有文件锁支持的系统,使用 int 来定义类别 (VxWorks 虽然定义了 flock 但是没有相应的机制来实现文件锁功能),函数体留空来避免编译报错 (相当于不起作用)。

下面是 ace_flock_t 的具体定义:

代码语言:javascript复制
 1   /**
 2    * @class ace_flock_t
 3    *
 4    * @brief OS file locking structure.
 5    */
 6   class ACE_Export ace_flock_t
 7   {
 8   public:
 9   /// Dump state of the object.
10     void dump (void) const;
11 
12 # if defined (ACE_WIN32)
13     ACE_OVERLAPPED overlapped_;
14 # else
15     struct flock lock_;
16 # endif /* ACE_WIN32 */
17 
18     /// Name of this filelock.
19     const ACE_TCHAR *lockname_;
20 
21     /// Handle to the underlying file.
22     ACE_HANDLE handle_;
23   };

可以看到就是主要支持两类:windows 的重叠 IO 和支持文件锁的 unix like 系统。

ACE_RW_Mutex

通用的读写锁类型,ACE_RW_Thread_Mutex 基类,与后者不同的是,它提供了 type 类型来指定共享的范围是进程内 (USYNC_THREAD) 还是进程间 (USYNC_PROCESS),ACE_RW_Thread_Mutex 就是通过传递 USYNC_THREAD 来实现的。底层类型同为 ACE_rwlock_t,这里重点考察一下它在 posix 与 solaris 上底层设施的差别:

平台/接口/设施

unix like (pthread)

Solaris

ACE_rwlock_t

pthread_rwlock_t

rwlock_t

init

pthread_rwlock_init

rwlock_init

acquire_read

pthread_rwlock_rdlock

rw_rdlock

tryacquire_read

pthread_rwlock_tryrdlock

rw_tryrdlock

acquire_write

pthread_rwlock_wrlock

rw_wrlock

tryacquire_write

pthread_rwlock_trywrlock

rw_trywrlock

tryacquire_write_upgrade

n/a

n/a

release

pthread_rwlock_unlock

rw_unlock

remove

pthread_rwlock_destroy

rwlock_destroy

其中 rwlock_init 接收一个 type 参数用于表示进程内、进程间共享 (USYNC_THREAD | USYNC_PROCESS);pthread_rwlock_init 也是如此,不过具体类型定义与 Solaris 上有所不同 (THREAD_PROCESS_SHARED | THREAD_PROCESS_PRIVATE),ACE 内部会做适当转换。这两组接口都不支持原生 name (虽然接口出于一致性提供了,但是内部都没有使用),是通过将读写锁放在共享内存中实现跨进程访问的,这一点需要特别注意。

条件变量

条件变量主要源自于 pthread 中的 condition variable,依据条件变量配合使用的 mutex 的不同,又细分为以下几类:

ACE_Condition_Thread_Mutex

这个主要是做进程内多线程等待与通知的,底层类型为 ACE_cond_t 与 ACE_Thread_Mutex,后者上面已经说明过了,下面重点说一下前者,它在不同平台上依赖的设施也不尽相同,可以列表如下:

平台/接口/设施

windows

unix like (pthread)

Solaris

VxWorks

unsupport

ACE_cond_t

自定义类型模拟

pthread_cond_t

cond_t

自定义类型模拟

int

init

参考自定义类型

pthread_cond_init

cond_init

参考自定义类型

n/a

wait

参考自定义类型

pthread_cond_wait

cond_wait

参考自定义类型

n/a

wait(..timeout..)

参考自定义类型

pthread_cond_timedwait

cond_timedwait

参考自定义类型

n/a

signal

参考自定义类型

pthread_cond_signal

cond_signal

参考自定义类型

n/a

broadcast

参考自定义类型

pthread_cond_broadcast

cond_broadcast

参考自定义类型

n/a

remove

参考自定义类型

pthread_cond_destroy

cond_destroy

参考自定义类型

n/a

对于上面的表做个简单说明:

  • 支持 pthread 的 unix like 系统,可以直接基于 pthread_cond_t 原生类型进行封装;
  • Solaris 系统的原生条件变量 cond_t 本身就可以支持上述接口;
  • windows 没有原生条件变量支持,这里使用自定义类型来模拟,其实只要是不支持条件变量的系统,都由这个自定义类型搞定,例如 VxWorks  等;
  • 条件变量的 wait 有两个重载,第二个可以带超时参数,此时对应的底层设施和第一个接口是不一样的;signal 用于唤醒一个线程;broadcast 用于唤醒所有等待在这个条件变量上的线程,不过最终仍只有一个线程可获取锁从而进入条件变量中;
  • 对于没有 thread mutex 和信号灯或事件支持的系统 (模拟类型所依赖的基础设施),使用 int 来定义 ACE_cond_t 类型、函数体留空,来避免编译报错 (相当于不起作用)。

该自定义类型的通用定义如下:

代码语言:javascript复制
 1 class ACE_Export ACE_cond_t
 2 {
 3 public:
 4 
 5   /// Returns the number of waiters.
 6   long waiters (void) const;
 7 
 8 //protected:
 9   /// Number of waiting threads.
10   long waiters_;
11 
12   /// Serialize access to the waiters count.
13   ACE_thread_mutex_t waiters_lock_;
14 
15   /// Queue up threads waiting for the condition to become signaled.
16   ACE_sema_t sema_;
17 
18 #     if defined (VXWORKS)19   /**
20    * A semaphore used by the broadcast/signal thread to wait for all
21    * the waiting thread(s) to wake up and be released from the
22    * semaphore.
23    */
24   ACE_sema_t waiters_done_;
25 #     elif defined (ACE_WIN32)
26   /**
27    * An auto reset event used by the broadcast/signal thread to wait
28    * for the waiting thread(s) to wake up and get a chance at the
29    * semaphore.
30    */
31   HANDLE waiters_done_;
32 #     else
33 #       error "Please implement this feature or check your config.h file!"
34 #     endif /* VXWORKS || ACE_PSOS */
35 
36   /// Keeps track of whether we were broadcasting or just signaling.
37   size_t was_broadcast_;
38 };

而其中具体使用的平台设施,又随 ACE_thread_mutex_t / ACE_sema_t / event 的定义而不同 (waiters_done_ 成员还特别区分了 VxWorks 与 Win32 平台,前者基于信号灯,后者基于事件)。关于如何使用 mutex 与 semaphore 或 event 来实现条件变量,这个留在后面详细说明。

ACE_Condition <MUTEX>

通用类型的条件变量,底层的互斥量可通过模板参数传递。与 ACE_Thread_Mutex_Condition 唯一的不同之处是提供了 type 类型来指定共享的范围是进程内 (USYNC_THREAD) 还是进程间 (USYNC_PROCESS),底层类型同为 ACE_cond_t,这里重点考察一下它在 posix 与 solaris 上的底层设施:

平台/接口/设施

unix like (pthread)

Solaris

ACE_cond_t

pthread_cond_t

cond_t

init

pthread_cond_init

cond_init

wait

pthread_cond_wait

cond_wait

wait(..timeout..)

pthread_cond_timedwait

cond_timedwait

signal

pthread_cond_signal

cond_signal

broadcast

pthread_cond_broadcast

cond_broadcast

remove

pthread_cond_destroy

cond_destroy

其中 cond_init 接收一个 type 参数用于表示进程内、进程间共享 (USYNC_THREAD | USYNC_PROCESS);pthread_cond_init 也是如此,不过具体类型定义与 Solaris 上有所不同 (THREAD_PROCESS_SHARED | THREAD_PROCESS_PRIVATE),ACE 内部会做适当转换。这两组接口都不支持原生 name (虽然接口出于一致性提供了,但是内部都没有使用),是通过将条件变量放在共享内存中实现跨进程访问的,这一点需要注意。

用于 ACE_Condition 的 MUTEX 模板参数,只能是下面几类:

  • ACE_Thread_Mutex
  • ACE_Recursive_Thread_Mutex
  • ACE_Null_Mutex

前两个已经在前面介绍过了,ACE_Null_Mutex 请参考后面 NULL 那一章。

ACE_Thread_Condition <MUTEX>

进程内多线程等待与唤醒的通用的条件变量,派生自 ACE_Condition <MUTEX>,并指定了使用 USYNC_THREAD 类型。它与 ACE_Condition_Thread_Mutex 作用完全一致,其实 ACE 作者的本意是定义它的实例化来作为 ACE_Condition_Thread_Mutex:

代码语言:javascript复制
typedef ACE_Condition_Thread_Mutex ACE_Thread_Condition <ACE_Thread_Mutex>; 

不过由于某些古老编译器的限制,这一实例化受限,于是不得不重新定义了一个 ACE_Condition_Thread_Mutex。不过这个类型也有好处,就是可以指定 MUTEX 类型,目前支持的类型与其父类 ACE_Condition <MUTEX> 相同。

ACE_Condition_Recursive_Thread_Mutex

与  ACE_Condition_Thread_Mutex 相比,增加了同 ACE_Recursive_Thread_Mutex 配合使用的能力。底层类型为 ACE_cond_t 与 ACE_Recursive_Thread_Mutex,涉及 ACE_cond_t 类型的底层设施上面已经说明过了,这里没有改变。其实 ACE_Condition_Recursive_Thread_Mutex 是 ACE_Condition <MUTEX> 模板使用 ACE_Recursive_Thread_Mutex 作为 MUTEX 模板参数的一个特化,后者与 ACE_Condition_Thread_Mutex 的关系前面已经介绍过了,可以认为是等价的。

而新的特化专门为等待递归锁 (wait 的两个重载) 提供了一份新的实现,用于在等待条件时释放 nesting_level 级别个锁、并在条件满足被唤醒后重新获取 nesting_level 个锁,从而保证在等待期间其它线程可以进入锁,避免死锁的发生。其实条件变量一般为了避免这种多层加锁导致的死锁问题,很少和递归锁配合使用,一般是和非递归锁一起用,所以非不得已,一般不使用这个类型。

上面的类型可能有点让人眼晕,画个图说明一下它们之间的关系:

ACE 因为兼容大量老旧平台与编译器,不得不在某些场景舍弃他们最爱的模板,不然的话代码还可以更为精简。

信号灯

信号灯就是 semaphore 了,它提供经典的 PV 操作,是操作系统同步的基石之一,所以很多平台都会支持。依据 semaphore 的各种特性,又细分为以下几类:

ACE_Thread_Semaphore

这个主要是做进程内同步的,底层类型为 ACE_sema_t,这个类型在不同平台上依赖的设施也不尽相同,可以列表如下:

平台/接口/设施

windows

wince

unix like (posix)

unix like (sysv)

Solaris

VxWorks

unsupport

ACE_sema_t

HANDLE

自定义类型 I

sem_t

自定义类型 II

sema_t

SEM_ID

int

init

CreateSemaphore

参考自定义类型 I

sem_init / sem_open

参考自定义类型 II

sema_init

semCCreate

n/a

acquire

WaitForSingleObject (..INFINITE..)

参考自定义类型 I

sem_wait

参考自定义类型 II

sema_wait

semTake (..WAIT_FOREVER..)

n/a

acquire (..time..)

WaitForSingleObject (..time..)

参考自定义类型 I

n/a

参考自定义类型 II

n/a

semTake (..time..)

n/a

tryacquire

WaitForSingleObject (..0..)

参考自定义类型 I

sem_trywait

参考自定义类型 II

sema_trywait

semTake (..NOWAIT..)

n/a

release

ReleaseSemaphore (..1..)

参考自定义类型 I

sem_post

参考自定义类型 II

sema_post

semGive

n/a

release (..N..)

ReleaseSemaphore (..N..)

循环调用 release N 次

循环调用 release N 次

循环调用 release N 次

循环调用 release N 次

循环调用 release N 次

n/a

remove

CloseHandle

参考自定义类型 I

sem_unlink / sem_close / sem_destroy

参考自定义类型 II

sema_destroy

semDelete

n/a

对于上面的表做个简单说明:

  • windows 上就是使用原生 Semaphore 来做信号灯;
  • wince  (Windows CE) 某些版本之前不支持原生 Semaphore,这里使用事件 (event) 和临界区 (CRITICAL_SECTION) 来模拟,定义为类型 I;
  • unix like 一般都支持 posix 标准,可以直接使用 posix 定义的 sem_t 类型来实现信号灯,它既支持匿名信号灯 (sem_init / sem_destroy)、也支持命名信号灯 (sem_open / sem_unlink / sem_close),根据用户需求 (是否传递有效的 name 参数) 来决定使用的底层接口。奇怪的是 posix semaphore 有 sem_timedwait 接口,而 ACE 却没有封装,不知道是不是我使用的版本太老的缘故;
  • 一些早期的平台对 posix 标准支持不全,它们没有 posix semaphore 可用, 这里基于互斥量 (pthread_mutex_t) 和条件变量 (pthread_cond_t) 来模拟,定义为类型 II;
  • Solaris 有自己原生的 sema_t,不使用 posix 信号灯,注意它和 posix 上的 sem_t 不是一个类型,sem 与  sema 一字之差,但是完全是两套接口,Solaris 上不支持命名信号灯。不过 Solaris 后续版本也支持 posix 信号灯,所以具体使用哪个,要看系统版本而定;
  • VxWorks 有自己原生的 SEM_ID 来做信号灯;
  • 对于没有 semaphore 支持的系统,使用 int 来定义类别,函数体留空来避免编译报错 (相当于不起作用)。

另外由于线程同步对象没有对读写做分离,所以 acquire_read / acquire_write / tryacquire_read / tryacquire_write 均使用默认的 acquire / tryacquire 来实现;对于 release 接口,提供一个一次释放 N 次信号灯的重载,对于该重载,除 wince 平台是调整接口 (ReleaseSemaphore) 参数外,其它的都是通过循环调用释放接口来模拟的。

自定义类型 I 定义如下:

代码语言:javascript复制
 1 /**
 2  * @class ACE_sema_t
 3  *
 4  * @brief Semaphore simulation for Windows CE.
 5  */
 6 class ACE_Export ACE_sema_t
 7 {
 8 public:
 9   /// Serializes access to <count_>.
10   ACE_thread_mutex_t lock_;
11 
12   /// This event is signaled whenever the count becomes non-zero.
13   ACE_event_t count_nonzero_;
14 
15   /// Current count of the semaphore.
16   u_int count_;
17 };

因为限定了 wince 平台,所以 ACE_thread_mutex_t 就是 CRITICAL_SECTION,ACE_event_t 就是 HANDLE (Event) 了。自定义类型 II 定义如下:

代码语言:javascript复制
 1 /**
 2  * @class ACE_sema_t
 3  *
 4  * @brief This is used to implement semaphores for platforms that support
 5  * POSIX pthreads, but do *not* support POSIX semaphores, i.e.,
 6  * it's a different type than the POSIX <sem_t>.
 7  */
 8 class ACE_Export ACE_sema_t
 9 {
10 public:
11   /// Serialize access to internal state.
12   ACE_mutex_t lock_;
13 
14   /// Block until there are no waiters.
15   ACE_cond_t count_nonzero_;
16 
17   /// Count of the semaphore.
18   u_long count_;
19 
20   /// Number of threads that have called <ACE_OS::sema_wait>.
21   u_long waiters_;
22 };

因为限定了 unix 平台,所以 ACE_mutex_t 就是 pthread_mutex_t,ACE_cond_t 就是 pthread_cond_t 了。关于如何基于它们来实现信号灯,这个留在后面再说。

此外,为了保存命名信号灯的名称,支持 posix semaphore 的平台和 VxWorks 并不是直接使用 sem_t 和 SEM_ID 的,而是将它们和 name 组合成一个结构体一起来使用:

代码语言:javascript复制
 1 typedef struct
 2 {
 3   /// Pointer to semaphore handle.  This is allocated by ACE if we are
 4   /// working with an unnamed POSIX semaphore or by the OS if we are
 5   /// working with a named POSIX semaphore.
 6   sem_t *sema_;
 7 
 8   /// Name of the semaphore (if this is non-NULL then this is a named
 9   /// POSIX semaphore, else its an unnamed POSIX semaphore).
10   char *name_;
11 
12 #if defined (ACE_LACKS_NAMED_POSIX_SEM)
13   /// this->sema_ doesn't always get created dynamically if a platform
14   /// doesn't support named posix semaphores.  We use this flag to
15   /// remember if we need to delete <sema_> or not.
16   int new_sema_;
17 #endif /* ACE_LACKS_NAMED_POSIX_SEM */
18 } ACE_sema_t;

不过好像因为 VxWorks 本身不支持命名信号灯,所以它这个成员一直保持为 NULL:

代码语言:javascript复制
1 // Use VxWorks semaphores, wrapped ...
2 typedef struct
3 {
4   /// Semaphore handle.  This is allocated by VxWorks.
5   SEM_ID sema_;
6 
7   /// Name of the semaphore:  always NULL with VxWorks.
8   char *name_;
9 } ACE_sema_t;

ACE_Process_Semaphore

这个主要是做进程间线程同步的,底层类型视不同平台分别为 ACE_Semaphore 或 ACE_SV_Semaphore_Complex,在 Windows 与支持 posix semaphore 的平台上使用前者,因为它们原生的信号灯本身就支持跨进程使用;对于支持 SystemV semaphore 的平台则使用后者,它封装了 sysv 相关的信号灯。ACE_Semaphore 其实就是 ACE_Thread_Semaphore 的基类,因而它的一些封装和上一节完全一样,不同的地方主要在于 posix semaphore 的跨进程处理上:

  • 如果该信号灯是有名的,则使用 sem_open / sem_close / sem_unlink 接口来操作命名信号灯,不同进程可以通过名称来指定一个唯一的全局信号灯;
  • 如果该信号灯是匿名的,则使用 sem_init / sem_destroy  在共享内存上创建对应的信号灯,不同的进程都映射这个共享内存来操作匿名的信号灯。

对于 sysv 信号灯,则使用另外一套完全不同的接口,该接口早于 posix 信号灯存在,因而也被许多系统广泛的支持,具体依赖的接口列表如下:

平台/接口/设施

windows

unix like (posix)

unix like (sysv)

Solaris

VxWorks

unsupport

ACE_sema_t

HANDLE

sem_t

int

sema_t

SEM_ID

int

init

CreateSemaphore

sem_init / sem_open

semget / semctl (..SETVAL..)

sema_init

semCCreate

n/a

acquire

WaitForSingleObject (..INFINITE..)

sem_wait

semop (..-1..)

sema_wait

semTake (..WAIT_FOREVER..)

n/a

tryacquire

WaitForSingleObject (..0..)

sem_trywait

semop (..-1..IPC_NOWAIT..)

sema_trywait

semTake (..NOWAIT..)

n/a

release

ReleaseSemaphore (..1..)

sem_post

semop (..1..)

sema_post

semGive

n/a

remove

CloseHandle

sem_unlink / sem_close / sem_destroy

semctl (..IPC_RMID..)

sema_destroy

semDelete

n/a

op

n/a

n/a

semop

n/a

n/a

n/a

control

n/a

n/a

semctl

n/a

n/a

n/a

对于上面的表做个简单说明:

  • 除 unix like (sysv) 外,其它列无变化,底层使用 ACE_Semaphore 来实现;
  • 对于使用 sysv 信号灯的 unix like 系统,进程间线程同步不再使用自定义类型模拟,而是直接使用 sysv 原生信号灯。

sysv 原生信号灯类型定义就是一个整数,与 posix 信号灯最大的不同就是可以同时操作一组信号灯,因此在 ACE_SV_Semaphore_Complex 类型中接口都有一个下标参数,用来表示操作哪个信号灯。初始化时一般需要两步,即 semget 创建信号灯数组,semctl 设置其初始值。

同 ACE_Thread_Semaphore 一样,这里 acquire_read / acquire_write / tryacquire_read / tryacquire_write 均使用默认的 acquire / tryacquire 来实现,此外,虽然 sysv 信号灯支持一次 acquire 或 release 信号灯的值大于一,但是这里没有封装,对应的 release N 重载版本也从接口中移除了。同样,由于 sysv 信号灯不支持超时,对应的带超时时间的 acquire 重载版本也从接口中移除了。这里新加入的 op 与 control 接口只是针对 ACE_SV_Semaphore_Complex 类型。

其实针对 sysv 信号灯的封装,在 ACE 中有两个层面,一个是较直接的 ACE_SV_Semaphore_Simple 类型,就是直接的底层接口封装;另外一个才是 ACE_SV_Semaphore_Complex 类型,它主要是考虑到多进程并发的情况下,如何规避 sysv 信号灯本身的一些设计缺陷带来的竞争问题,这种竞争问题主要表现在几个方面:

  • 并不是当前进程退出时就要删除信号灯组,需要判断没有其它进程再使用该信号灯组时才发生删除动作;
  • 多个进程工作在同一个信号灯组时,创建与退出可能存在竞争关系,需要加以保护;
  • 创建信号灯与设置信号灯初始值是两个操作不是原子的,需要加以保护。

它空出两个信号灯专门用于整个信号灯组的创建、删除操作过程中的同步,其中一个就是简单的当作锁来用,另一个则记录了整个工作在信号灯组上的进程数量,当数量减为 0 时表示无进程工作在此实例上因而可以安全的释放整个信号灯组。至于如何做到这一点,留在以后再说。

事件

事件是 bool 状态的信号灯,适合一些简单的同步场景。事件可以有两种状态,有信号或无信号,无信号状态下,在上面等待的线程将被阻塞,直到事件被激发 (signal) 为有信号状态。

ACE_Event

这个主要用来做进程间线程间同步的。底层类型为 ACE_event_t ,这个类型在不同平台上依赖的设施也不尽相同,可以列表如下:

平台/接口/设施

windows

unix like

unsupport

ACE_event_t

HANDLE

自定义类型模拟

int

init

CreateEvent

参考自定义类型

n/a

wait

WaitForSingleObject (..INFINITE..)

参考自定义类型

n/a

wait(..timeout..)

WaitForSingleObject (..time..)

参考自定义类型

n/a

signal

SetEvent

参考自定义类型

n/a

pulse

PulseEvent

参考自定义类型

n/a

reset

ResetEvent

参考自定义类型

n/a

remove

CloseHandle

参考自定义类型

n/a

对于上面的表做个简单说明:

  • windows 上就是使用原生 Event 来做事件,基于 windows 的原生能力,可实现跨进程线程间同步、全局名称查找等能力;
  • unix like 系统,没有原生的事件对象,这里使用自定义类型来模拟,要求平台支持 mutex 与 condition variable,模拟实现的事件没有跨进程、全局名称查找的能力;
  • 对于没有 mutex 和 condition variable 支持的系统,使用 int 来定义 ACE_event_t 类型、函数体留空,来避免编译报错 (相当于不起作用)。

事件的接口和一般的线程同步对象差别较大,与条件变量接口有些相似,它使用 wait / signal / pulse / reset 来代替 acquire / release。

自定义类型的通用定义如下:

代码语言:javascript复制
 1 class ACE_Export ACE_event_t
 2 {
 3   /// Protect critical section.
 4   ACE_mutex_t lock_;
 5 
 6   /// Keeps track of waiters.
 7   ACE_cond_t condition_;
 8 
 9   /// Specifies if this is an auto- or manual-reset event.
10   int manual_reset_;
11 
12   /// "True" if signaled.
13   int is_signaled_;
14 
15   /// Special bool for auto_events alone
16   /**
17    * The semantics of auto events forces us to introduce this extra
18    * variable to ensure that the thread is not woken up
19    * spuriously. Please see event_wait and event_timedwait () to see
20    * how this is used for auto_events. Theoretically this is a hack
21    * that needs revisiting after x.4
22    */
23   bool auto_event_signaled_;
24 
25   /// Number of waiting threads.
26   unsigned long waiting_threads_;
27 };

关于如何使用 mutex 与 condition variable 来实现事件,这个留在后面详细说明。

ACE_Auto_Event

自动事件,派生自 ACE_Event。自动事件在激活时一次只唤醒一个线程,且线程唤醒后,事件自动重置为无信号状态。

  • windows 平台上原生的事件就支持该类型,接口只是传递了一个标志位给底层接口;
  • unix like 平台上模拟的自定义类型通过内部的一个变量记录了事件的类型,在处理时会参考它进行不同的操作。

由于自动重置的特性,很少使用 reset / pulse 接口,也没有类似条件变量 broadcast 那样的接口可以一次唤醒所有等待线程。

ACE_Manual_Event

手动事件,派生自 ACE_Event。手动事件在激活时唤醒全部线程,且线程唤醒后,事件仍保持有信号状态,直接用户手动重置事件。

  • windows 平台上原生的事件就支持该类型,接口只是传递了一个标志位给底层接口;
  • unix like 平台上模拟的自定义类型通过内部的一个变量记录了事件的类型,在处理时会参考它进行不同的操作。

由于没有自动重置的特性,非常依赖 reset / pulse 接口,signal 接口将唤醒所有在事件上等待的线程 (类似条件变量的 broadcast 接口),但没有类似条件变量唤醒单个线程的接口。

总之,由于需要事先指定事件类型、且创建后不能再修改类型,事件在使用过程中不如条件变量灵活。

线程局部存储

线程专有/局部存储 ,Thread Special/Local Storage,简写为 TSS (linux) 或 TLS (windows),它提供了一套访问变量的接口,通过这组接口,多个线程看上去访问的是一个全局变量,实际上是该变量对应到此线程的专有实例,从而从根本上避免了线程竞争的问题。

ACE_TSS <TYPE>

通用的 TSS 模板类,底层类型为 ACE_thread_key_t,这个类型在不同平台上依赖的设施也不尽相同,可以列表如下:

平台/接口/设施

windows

wince

unix like (posix)

unix like (non-posix)

Solaris

VxWorks

unsupport

ACE_thread_key_t

DWORD

u_int

pthread_key_t

u_long

thread_key_t

u_int

int

init

TlsAlloc

参考自定义类型

pthread_key_create

参考自定义类型

thr_keycreate

参考自定义类型

n/a

ts_get

TlsGetValue

参考自定义类型

pthread_getspecific

参考自定义类型

thr_getspecific

参考自定义类型

n/a

ts_set

TlsSetValue

参考自定义类型

pthread_setspecific

参考自定义类型

thr_setspecific

参考自定义类型

n/a

ts_object

ts_get

n/a

ts_object (ts)

init / ts_get / ts_set

n/a

operator -> operator TYPE*

init / ts_get / ts_set

n/a

remove

TlsFree

参考自定义类型

pthread_key_delete

参考自定义类型

thr_keydelete

参考自定义类型

n/a

对于上面的表做个简单说明:

  • windows 上就是使用原生 Tls 接口来实现线程局部存储;
  • wince  (Windows CE) 上的原生 Tls 接口有一些限制(?),这里使用自定义类型来模拟,需要定义宏 ACE_HAS_TSS_EMULATION;
  • unix like 一般都支持 posix 标准,可以直接使用 posix 定义的 pthread_key_xxx 接口来实现线程专有存储;
  • 早期对 posix 标准支持不全的 unix like 系统 ,使用与 wince 相同的自定义类型来模拟;
  • Solaris 有自己原生的 thr_keyxxx 接口,不使用 posix 设施;
  • VxWorks 也缺失线程局部存储能力,这里也使用与上面相同的自定义类型来模拟;
  • 对于其它没有线程局部存储支持的系统,使用 int 来定义类别,函数体留空来避免编译报错 (相当于不起作用)。

线程局部存储的接口和一般的线程同步对象差别较大,在进程创建或第一次使用时初始化根键、用来代表一个全局的变量,之后每个线程可以基于这个根键存取自己线程实例的值,ts_get / ts_set 代表了获取和设置两种操作,实际并不存在这样一个公开接口,只是为了描述下面接口实现方便而抽象出来的。例如:

  • ts_object 不带参数的版本表示获取实例值,底层基于 ts_get 实现,如果未初始化根键或没有对应的值,返回空指针;
  • ts_object 带 TYPE* 参数的版本表示设置实例值,因为要返回之前的旧值,所以内部同时调用了 ts_get 与 ts_set,如果根键未初始化,可能还需要调用到 init 来做第一次使用时的初始化动作;;
  • 类的 operator-> 与 operator TYPE* 操作符重载,底层实际是调用 ts_get 获取实例值,如果该线程还没有设置任何实例值,则返回一个新的值并通过 ts_set 将其绑定到根键所在的线程中,同理,如果根键未初始化,也需要调用一次 init 来初始化之。

最复杂的部分在于销毁,在创建线程局部存储根键时:

  • unix like (posix) 和 Solaris 平台会记录一个清理函数;
  • windows 和其它通过自定义类型模拟的平台由于不支持清理函数,只能通过外部的 ACE_TSS_Cleanup 类单例完成对象与清理函数的登记工作。

这样在线程退出时:

  • unix like (posix) 和 Solaris 平台会自动调用这个清理函数销毁创建的线程实例;
  • windows 和其它通过自定义类型模拟的平台则有两个时机来销毁根键:
    • 针对每个线程引用的 ACE_TSS <TYPE> 对象做处理,当该对象析构时,会尝试在  ACE_TSS_Cleanup 中找到对应的登记信息,对对应根键的引用计数减一,如果引用计数减为零,才尝试销毁这个根键;
    • 每个由 ace 接口启动的线程都会运行 ace 自己的线程托管函数,该函数在线程退出前,会尝试在 ACE_TSS_Cleanup 中针对每一个根键做遍历,找到该根键在此线程的实例并销毁之,但不会对根键做任何处理 (以防止销毁将要被其它线程访问的根键)。

ACE_TSS <TYPE> 本身是一个 c 模板类,模板参数就是线程使用的实例类型,可以为简单类型如 char / int / float / double,也可以为其它自定义的类或结构体。用户只需要定义自己的类型并传递给模板就可以实现线程隔离能力,这就是 c 的强悍之处。

自定义类型的通用定义如下:

代码语言:javascript复制
  1 // forward declaration
  2 class ACE_TSS_Keys;
  3 
  4 /**
  5  * @class ACE_TSS_Emulation
  6  *
  7  * @brief Thread-specific storage emulation.
  8  *
  9  * This provides a thread-specific storage implementation.
 10  * It is intended for use on platforms that don't have a
 11  * native TSS, or have a TSS with limitations such as the
 12  * number of keys or lack of support for removing keys.
 13  */
 14 class ACE_Export ACE_TSS_Emulation
 15 {
 16 public:
 17   typedef void (*ACE_TSS_DESTRUCTOR)(void *value) /* throw () */;
 18 
 19   /// Maximum number of TSS keys allowed over the life of the program.
 20   enum { ACE_TSS_THREAD_KEYS_MAX = ACE_DEFAULT_THREAD_KEYS };
 21 
 22   /// Returns the total number of keys allocated so far.
 23   static u_int total_keys ();
 24 
 25   /// Sets the argument to the next available key.  Returns 0 on success,
 26   /// -1 if no keys are available.
 27   static int next_key (ACE_thread_key_t &key);
 28 
 29   /// Release a key that was used. This way the key can be given out in a
 30   /// new request. Returns 0 on success, 1 if the key was not reserved.
 31   static int release_key (ACE_thread_key_t key);
 32 
 33   /// Returns the exit hook associated with the key.  Does _not_ check
 34   /// for a valid key.
 35   static ACE_TSS_DESTRUCTOR tss_destructor (const ACE_thread_key_t key);
 36 
 37   /// Associates the TSS destructor with the key.  Does _not_ check
 38   /// for a valid key.
 39   static void tss_destructor (const ACE_thread_key_t key,
 40                               ACE_TSS_DESTRUCTOR destructor);
 41 
 42   /// Accesses the object referenced by key in the current thread's TSS array.
 43   /// Does _not_ check for a valid key.
 44   static void *&ts_object (const ACE_thread_key_t key);
 45 
 46   /**
 47    * Setup an array to be used for local TSS.  Returns the array
 48    * address on success.  Returns 0 if local TSS had already been
 49    * setup for this thread.  There is no corresponding tss_close ()
 50    * because it is not needed.
 51    * NOTE: tss_open () is called by ACE for threads that it spawns.
 52    * If your application spawns threads without using ACE, and it uses
 53    * ACE's TSS emulation, each of those threads should call tss_open
 54    * ().  See the ace_thread_adapter () implementation for an example.
 55    */
 56   static void *tss_open (void *ts_storage[ACE_TSS_THREAD_KEYS_MAX]);
 57 
 58   /// Shutdown TSS emulation.  For use only by ACE_OS::cleanup_tss ().
 59   static void tss_close ();
 60 
 61 private:
 62   // Global TSS structures.
 63   /// Contains the possible value of the next key to be allocated. Which key
 64   /// is actually allocated is based on the tss_keys_used
 65   static u_int total_keys_;
 66 
 67   /// Array of thread exit hooks (TSS destructors) that are called for each
 68   /// key (that has one) when the thread exits.
 69   static ACE_TSS_DESTRUCTOR tss_destructor_ [ACE_TSS_THREAD_KEYS_MAX];
 70 
 71   /// TSS_Keys instance to administrate whether a specific key is in used
 72   /// or not.
 73   /// or not.
 74   // Static construction in VxWorks 5.4 and later is slightly broken.
 75   // If the static object is more complex than an integral type, static
 76   // construction will occur twice.  The tss_keys_used_ object is
 77   // statically constructed and then modified by ACE_Log_Msg::instance()
 78   // when two keys are created and TSS data is stored.  However, at
 79   // the end of static construction the tss_keys_used_ object is again
 80   // initialized and therefore it will appear to next_key() that no
 81   // TSS keys have been handed out.  That is all true unless the
 82   // tss_keys_used object is a static pointer instead of a static object.
 83   static ACE_TSS_Keys* tss_keys_used_;
 84 
 85 #   if defined (ACE_HAS_THREAD_SPECIFIC_STORAGE)
 86   /// Location of current thread's TSS array.
 87   static void **tss_base (void* ts_storage[] = 0, u_int *ts_created = 0);
 88 #   else  /* ! ACE_HAS_THREAD_SPECIFIC_STORAGE */
 89   /// Location of current thread's TSS array.
 90   static void **&tss_base ();
 91 #   endif /* ! ACE_HAS_THREAD_SPECIFIC_STORAGE */
 92 
 93 #   if defined (ACE_HAS_THREAD_SPECIFIC_STORAGE)
 94   // Rely on native thread specific storage for the implementation,
 95   // but just use one key.
 96   static ACE_OS_thread_key_t native_tss_key_;
 97 
 98   // Used to indicate if native tss key has been allocated
 99   static int key_created_;
100 #   endif /* ACE_HAS_THREAD_SPECIFIC_STORAGE */
101 };

 该类型有两种实现方式:

  • 如果平台有线程局部存储的能力,只是支持的不够完整,那么 ACE_TSS_Emulation 尝试使用这些现成的机制、并在此基础上做一些弥补工作;
  • 如果平台压根没有这方面的能力,那么 ACE_TSS_Emulation 将从无到有模拟这种能力。

ACE_TSS_Emulation 需要用到 ACE_TSS_Keys 辅助类,它的定义如下:

代码语言:javascript复制
 1 /**
 2  * @class ACE_TSS_Keys
 3  *
 4  * @brief Collection of in-use flags for a thread's TSS keys.
 5  * For internal use only by ACE_TSS_Cleanup; it is public because
 6  * some compilers can't use nested classes for template instantiation
 7  * parameters.
 8  *
 9  * Wrapper around array of whether each key is in use.  A simple
10  * typedef doesn't work with Sun C   4.2.
11  */
12 class ACE_TSS_Keys
13 {
14 public:
15   /// Default constructor, to initialize all bits to zero (unused).
16   ACE_TSS_Keys (void);
17 
18   /// Mark the specified key as being in use, if it was not already so marked.
19   /// Returns 1 if the had already been marked, 0 if not.
20   int test_and_set (const ACE_thread_key_t key);
21 
22   /// Mark the specified key as not being in use, if it was not already so
23   /// cleared.  Returns 1 if the key had already been cleared, 0 if not.
24   int test_and_clear (const ACE_thread_key_t key);
25 
26   /// Return whether the specific key is marked as in use.
27   /// Returns 1 if the key is been marked, 0 if not.
28   int is_set (const ACE_thread_key_t key) const;
29 
30 private:
31   /// For a given key, find the word and bit number that represent it.
32   static void find (const u_int key, u_int &word, u_int &bit);
33 
34   enum
35     {
36 #   if ACE_SIZEOF_LONG == 8
37       ACE_BITS_PER_WORD = 64,
38 #   elif ACE_SIZEOF_LONG == 4
39       ACE_BITS_PER_WORD = 32,
40 #   else
41 #     error ACE_TSS_Keys only supports 32 or 64 bit longs.
42 #   endif /* ACE_SIZEOF_LONG == 8 */
43       ACE_WORDS = (ACE_DEFAULT_THREAD_KEYS - 1) / ACE_BITS_PER_WORD   1
44     };
45 
46   /// Bit flag collection.  A bit value of 1 indicates that the key is in
47   /// use by this thread.
48   u_long key_bit_words_[ACE_WORDS];
49 };

关于具体如何实现模拟,这个留在后面详细说明。

ACE_TSS_Connection

每线程一个连接的实用类型,它是 ACE_TSS <TYPE> 的派生类,提供的 TYPE 是 ACE_SOCK_Stream,用来代表一个 tcp 连接。

这个类型可以理解成是 ACE_TSS <TYPE> 的现成应用,主要用于 ACE_Token_Connections 中,后者又用于 ACE_Remote_Token_Proxy 来实现远程令牌同步对象系统中的锁服务器,关于这方面的内容,可以参考后面 TOKEN 这一章。

原子操作

上面说的一些同步对象都比较重,面向的也是一些复杂的同步场景,而一些简单的算术运算,由于底层可能被解释成多条汇编指令,从而带来线程竞争的场景,完全可以由平台提供的 CPU 指令级别的原子操作来实现,可以大大提高并发性能。

ACE_Atomic_Op <LOCK, TYPE>

这个模板类封装了通用的原子操作类型,说它通用,是因为底层它是通过 LOCK 类型来获取线程互斥的能力,然后在锁的保护下执行一些类似 /--/ =/- … 的算术操作以及 >/>=/</<= … 的比较操作,这些操作都是直接委托给传入的 TYPE 类型。

对于锁的类型没有要求,只要提供以下四个通用接口即可:

  • acquire
  • tryacquire
  • release
  • remove

ACE 中符合这个约定的类型有不少,其实这里使用了 GUARD 辅助类,关于各种 GUARD 类型及其适配的锁类型,请参考下面 GUARD 一章。

有的人可能会说,这个没有实现性能的提高呀,事实确实是这样,但是不要急,它还有针对简单类型的特化,请耐心看下文。

ACE_Atomic_Op <ACE_Thread_Mutex, long>

对于一些简单类型,例如整型,大多数平台都会提供特有的 CPU 指令来提高多线程同步的性能,这个模板特化就封装了这方面的能力,它在各个平台上依赖的 cpu 指令列表如下:

平台/接口/设施

windows

gnuc (pentium)

unsupport

single increment

InterlockedIncrement

"xadd %0, (%1)" : " r"(1) : "r"(addr)

int

single decrement

InterlockedDecrement

"xadd %0, (%1)" : " r"(-1) : "r"(addr)

n/a

single exchange

InterlockedExchange

"xchg %0, (%1)" : " r"(rhs) : "r"(addr)

n/a

single exchange add

InterlockedExchangeAdd

"xadd %0, (%1)" : " r"(rhs) : "r"(addr)

n/a

multiple increment

InterlockedIncrement

"lock ; xadd %0, (%1)" : " r"(1) : "r"(addr)

n/a

multiple decrement

InterlockedDecrement

"lock ; xadd %0, (%1)" : " r"(-1) : "r"(addr)

n/a

multiple exchange

InterlockedExchange

"xchg %0, (%1)" : " r"(rhs) : "r"(addr)

n/a

multiple exchange add

InterlockedExchangeAdd

"lock ; xadd %0, (%1)" : " r"(rhs) : "r"(addr)

n/a

上面 8 个接口可分为两组,一组为单 CPU 场景下调用的 single xxx;另一组为多 CPU 场景下调用的 multiple xxx。ACE 会在初始化时根据系统 CPU 核心数来确定调用哪组接口,对于 windows 系统而言,两组接口底层是一致的,因为 Interlockedxxx 接口本身可以应对单核与多核两个场景。其它平台如果支持 gnuc 和奔腾处理器,则使用 x86 特有的 xadd 与 xchg 指令,该指令中多核与单核唯一不同就是有没有 lock 指令锁定相应内存,对于 xchg 指令,隐含 lock 指令,所以不用单独指定。关于接口与操作符的对应关系,可以参考下表:

操作符

接口

operator

single/multiple increment

operator --

single/multiple decrement

operator = / -=

single/multiple exchange add

operator =

single/multiple exchange

对于对比运算符,没有使用原子操作。这里模板参数中的 ACE_Thread_Mutex 只是一个占位符,并没有真正使用 (其实完全可以使用 ACE_Null_Mutex,请参考后面 NULL 这一章)。另外 windows 上非常有用的 InterlockedCompareExchange 没有封装进来 (对比并交换,只有在对比结果一致的情况下才交换,很多"无锁队列"都是基于这个实现的)。

ACE_Atomic_Op_Ex <LOCK, TYPE>

这个类型与 ACE_Atomic_Op <LOCK, TYPE> 非常类似,不同的是该类型使用外部传入的 LOCK 实例,从而可以在多个对象之间共享同一个锁,而 ACE_Atomic_Op 使用的是自己内部的锁。它们之间虽然接口完全一样,但是不存在派生关系。其实 ACE_Atomic_Op 内部聚合了一个 ACE_Atomic_Op_Ex 实例,将内部锁传递给后者的构造函数,并将所有操作都委托给后者实现。不过使用这个类型的一个缺点是,无法再针对整型进行特化,从而享受 ACE_Atomic_Op <ACE_Thread_Mutex, long> 带来性能提升的好处。

GUARD

上面讲了很多可以充当锁的同步对象,可以直接拿来使用,不过在 c 中,基于 RAII  的思想,一般将锁对象包装在守卫 (GUARD) 对象中,利用 c 构造、析构函数被编译器自动调用的特性,实现锁的自动释放,避免因 return / continue / break 甚至抛出异常等离开当前控制流、外加一些人为因素导致的锁未及时释放问题。

ACE_Guard <LOCK>

封装了通用的锁守卫类,凡是支持以下接口的锁类型都可适用:

接口

时机

acquire

构造函数 / 明确获取

tryacquire

非 block 类型的构造函数 / 明确尝试获取

release

析构函数 / 明确释放

remove

明确销毁

ACE_Guard 模板类一般是借用外部的锁实例,模板参数 LOCK 为借用的锁类型。在构造函数时加锁、析构函数解锁,但也提供了一些接口来提供一定灵活性,例如可以在 guard 实例生命周期内提前调用 release 接口来释放锁、在之后某个时机再调用 acqure / tryacquire 再次获取锁,甚至直接调用 remove 销毁底层的锁。guard 实例内部有一个 acquire / tryacquire 接口的返回值,通过检查该值 (locked) 来判断是否加锁成功,也可以重置该值 (disown) 来避免析构时自动调用 release (用于保持加锁状态)。该类型适配的锁范围较广,列表如下:

  • ACE_Thread_Mutex
  • ACE_Recursive_Thread_Mutex
  • ACE_RW_Thread_Mutex
  • ACE_Process_Mutex
  • ACE_RW_Process_Mutex
  • ACE_Thread_Semaphore
  • ACE_Process_Semaphore
  • 任何满足上面调用约定的自定义类型

具体的性质和使用的锁类型相关,例如对于 linux 上的 ACE_Thread_Mutex,是不支持递归加锁的,在使用时并不是随意搭配,而是要考虑使用的场景再选取合适的锁类型,这一点可以参考前面的说明。

ACE_Read_Guard <LOCK>

派生自 ACE_Guard <LOCK>,封装了读写锁中读的一方,凡是支持以下接口的锁类型即可适用:

接口

时机

acquire_read

构造函数 / 明确获取

tryacquire_read

非 block 类型的构造函数 / 明确尝试获取

release

析构函数 / 明确释放

remove

明确销毁

它也定义 acquire / tryacquire 接口,不过都重定向到了 acquire_read / tryacquire_read 接口,强制使用读锁。该类型适配的读写锁类型列表如下:

  • ACE_RW_Thread_Mutex
  • ACE_RW_Process_Mutex
  • 任何满足上面调用约定的自定义类型

ACE_Write_Guard <LOCK>

派生自 ACE_Guard <TYPE>,封装了读写锁中读的一方,凡是支持以下接口的锁类型即可适用:

接口

时机

acquire_write

构造函数 / 明确获取

tryacquire_write

非 block 类型的构造函数 / 明确尝试获取

release

析构函数 / 明确释放

remove

明确销毁

它也定义 acquire / tryacquire 接口,不过都重定向到了 acquire_write / tryacquire_write 接口,强制使用写锁。该类型适配的读写锁类型列表如下:

  • ACE_RW_Thread_Mutex
  • ACE_RW_Process_Mutex
  • 任何满足上面调用约定的自定义类型

上面罗列了三种类型守卫类型,如果直接使用的话,要这样写:

代码语言:javascript复制
ACE_Guard <ACE_Thread_Mutex> guard (mutex_); 

其中 mutex_ 可以理解成是类的锁成员,用于类内部所有并发的控制。ACE 提供了一些宏来简化守卫的定义:

代码语言:javascript复制
ACE_GUARD (LockType, GuardName, LockObject)
ACE_WRITE_GUARD (LockType, GuardName, LockObject)
ACE_READ_GUARD (LockType, GuardName, LockObject)

上面的代码就可以被简化成:

代码语言:javascript复制
ACE_GUARD (ACE_Thread_Mutex, guard, mutex_)

是不是清爽了很多?

ACE_TSS_Guard <LOCK>

将锁和线程局部存储结合起来,就得到每线程 (per-thread) 的同步锁。不过既然线程局部存储已经可以保证只有一个线程访问,那么加锁又有什么用呢?找到源码中的说明贴出来大家自己理解吧:

代码语言:javascript复制
/**
 * @class ACE_TSS_Guard
 *
 * @brief This data structure is meant to be used within a method or
 * function...  It performs automatic aquisition and release of
 * a synchronization object.  Moreover, it ensures that the lock
 * is released even if a thread exits via <thr_exit>!
 */

搜遍了整个源代码,没有找到这个类的调用点。所以个人理解这应该是单纯为了体现 c 模板各种组合带来的强大能力(?),有点“炫技”的感觉,所以下面只从开拓眼界的角度看一下这个类型的接口。

在构造函数里,通过 init_key 创建了一个 TSS 的根键,并将传入的外部锁实例作为键值设置进去。当调用相关接口时,再通过根键获取锁实例,并将调用委托给此实例实现。注意这里的实例并不是 LOCK 本身,而是 ACE_Guard <LOCK>,其实就是通过聚合重用了后者,因此接口及适配的锁类型与 ACE_Guard 完全一致,这里不再赘述。

从这里的实现也可以看出,如果线程不是当初创建这个对象的线程,那么当去调用它的一些接口时,对应的底层锁对象其实是 NULL,将会导致进程直接崩溃。好在 GUARD 类本身就是作为栈上的局部对象使用,一般不涉及超过函数级别共享的问题,如果是一个函数被多个线程并发访问,那么这种情况下每个线程使用自己的 ACE_Guard 对象其实更为合理。

而 TSS  本身是多个线程访问全局或共享变量时,每个线程访问其基于本线程的实例,如果这样来用这个类型的话,则会遇到我之前说的,只有创建该类型实例的线程能访问底层锁,其它线程将得到 NULL 从而崩溃。所以我实在想不出 ACE_TSS_Guard 的任何实用场景,有看出门道的看官可以指点一二则个~~

ACE_TSS_Read_Guard <LOCK>

派生自 ACE_TSS_Guard <LOCK>,封装了读写锁中读的一方,适配的锁类型与 ACE_Read_Guard <LOCK> 相同,与其父类相似,它底层其实是聚合了 ACE_Read_Guard 来实现接口的。

ACE_TSS_Write_Guard <LOCK>

派生自 ACE_TSS_Guard <LOCK>,封装了读写锁中写的一方,适配的锁类型与 ACE_Write_Guard <LOCK> 相同,与其父类相似,它底层其实是聚合了 ACE_Write_Guard 来实现接口的。

可能因为使用场景少,没有定义与 ACE_TSS_xxx_Guard 相关联的宏来简化守卫对象的声明。

BARRIER

从这节开始讨论一些基于基本同步对象构建的高级同步对象。BARRIER:栅栏同步,顾名思义,就是当线程没有达到指定的数量时,会堵塞在对应的 BARRIER 上,直到所期待的线程都到达后才一次性全部唤醒,从而保证不会有一些线程仍滞留在某些代码从而导致线程竞争的问题 (?)。

ACE_Barrier

通用的栅栏同步类,通过构造函数可以指定要同步的线程数量,相同数量的线程在需要同步的位置调用该实例的 wait 接口,当到达的线程数量不足时,wait 会让线程阻塞,直到到达同步点的线程达到指定的数量,才一次性唤醒所有线程继续执行。栅栏同步体可多次使用,linux 上有原生的栅栏同步 api ,不过 ace 考虑可移植性,并没有直接基于它进行封装,而是基于条件变量自己实现了一版:

代码语言:javascript复制
 1 struct ACE_Export ACE_Sub_Barrier
 2 {
 3   // = Initialization.
 4   ACE_Sub_Barrier (unsigned int count,
 5                    ACE_Thread_Mutex &lock,
 6                    const ACE_TCHAR *name = 0,
 7                    void *arg = 0);
 8 
 9   ~ACE_Sub_Barrier (void);
10 
11   /// True if this generation of the barrier is done.
12   ACE_Condition_Thread_Mutex barrier_finished_;
13 
14   /// Number of threads that are still running.
15   int running_threads_;
16 
17   /// Dump the state of an object.
18   void dump (void) const;
19 
20   /// Declare the dynamic allocation hooks.
21   ACE_ALLOC_HOOK_DECLARE;
22 };
23 
24 /**
25  * @class ACE_Barrier
26  *
27  * @brief Implements "barrier synchronization".
28  *
29  * This class allows <count> number of threads to synchronize
30  * their completion of (one round of) a task, which is known as
31  * "barrier synchronization".   After all the threads call <wait()>
32  * on the barrier they are all atomically released and can begin a new
33  * round.
34  *
35  * This implementation uses a "sub-barrier generation numbering"
36  * scheme to avoid overhead and to ensure that all threads wait to
37  * leave the barrier correct.  This code is based on an article from
38  * SunOpsis Vol. 4, No. 1 by Richard Marejka
39  * (Richard.Marejka@canada.sun.com).
40  */
41 class ACE_Export ACE_Barrier
42 {
43 public:
44   /// Initialize the barrier to synchronize <count> threads.
45   ACE_Barrier (unsigned int count,
46                const ACE_TCHAR *name = 0,
47                void *arg = 0);
48 
49   /// Default dtor.
50   ~ACE_Barrier (void);
51 
52   /// Block the caller until all <count> threads have called <wait> and
53   /// then allow all the caller threads to continue in parallel.
54   int wait (void);
55 
56   /// Dump the state of an object.
57   void dump (void) const;
58 
59   /// Declare the dynamic allocation hooks.
60   ACE_ALLOC_HOOK_DECLARE;
61 
62 protected:
63   /// Serialize access to the barrier state.
64   ACE_Thread_Mutex lock_;
65 
66   /// Either 0 or 1, depending on whether we are the first generation
67   /// of waiters or the next generation of waiters.
68   int current_generation_;
69 
70   /// Total number of threads that can be waiting at any one time.
71   int count_;
72 
73   /**
74    * We keep two <sub_barriers>, one for the first "generation" of
75    * waiters, and one for the next "generation" of waiters.  This
76    * efficiently solves the problem of what to do if all the first
77    * generation waiters don't leave the barrier before one of the
78    * threads calls wait() again (i.e., starts up the next generation
79    * barrier).
80    */
81   ACE_Sub_Barrier sub_barrier_1_;
82   ACE_Sub_Barrier sub_barrier_2_;
83   ACE_Sub_Barrier *sub_barrier_[2];
84 
85 private:
86   // = Prevent assignment and initialization.
87   void operator= (const ACE_Barrier &);
88   ACE_Barrier (const ACE_Barrier &);
89 };

具体的实现是委托给 ACE_Sub_Barrier 来实现的,它内部基于 ACE_Condition_Thread_Mutex 来实现,所以只有支持这个类型的平台才有栅栏同步体的支持,如果没有的话该类型声明为空以避免编译错误。ACE_Barrier 内部聚合了两个该对象进行循环切换以便支持后续的 wait 调用,具体实现细节留在后面详细说明。

ACE_Thread_Barrier

派生自 ACE_Barrier,可能想构建类似前面同步对象的体系——Thread 表示进程内多线程使用,Process 表示进程间多线程使用——但是目前底层的依赖的 ACE_Condition_Thread_Mutex 只支持进程内多线程,导致 ACE_Barrier 本身就不能跨进程使用,所以目前 ACE_Thread_Barrier 与 ACE_Barrier 完全等价,且没有对应的 ACE_Process_Barrier 可用 (被 ifdef 注释掉了)。除了 ACE_Condition_Thread_Mutex 的局限外,即使我们找到了可以跨进程使用的条件变量,ACE_Barrier 内部还有一些变量 (running_threads_ / count_ …) 也需要放在共享内存中,这个工作量也是蛮大的,所以目前没有跨进程的栅栏同步体提供。

TOKEN

TOKEN:令牌同步,是 ACE 抽象的高级同步对象,它实现了可递归锁定、读写分离、死锁检测、等待通知等高级特性,甚至还支持分布式锁。

ACE_Token_Proxy

是各种进程内 token 对象的基类,不能直接拿来用,如果想要扩展 token 对象的类型,可以从它派生。通过它我们先来了解一下 TOKEN 体系各个类之间的关系:

右侧三个类是对外接口,左侧三个类是实现,不能直接拿来用。它们存在一一对应的关系,例如 ACE_Local_Mutex 基于 ACE_Mutex_Token 实现;ACE_Local_RLock 和 ACE_Local_WLock  基于 ACE_RW_Token 实现。ACE_Token_Proxy 是所有对外接口类的基类,它是调用者 (线程) 的抽象;ACE_Tokens 是所有内部实现类的基类,它是锁的抽象。一个锁上可能有多个线程,但最多只有一个拥有者,其它线程则将自己加入锁的队列中,并在自己内部的一个条件变量 (ACE_Condition_Thread_Mutex) 上等待。当拥有者释放锁 (release) 时,会自动将等待队列头部的线程唤醒 (通知内部的条件变量),从而使其获取锁。反之,一个线程只能在一个锁上等待,不过可能同时拥有多个锁 (申请了多份资源)。

ACE_Local_Mutex

本地简单锁的抽象,通过派生 ACE_Token_Proxy 并提供 ACE_Mutex_Token 作为底层的实现来制作一个行为类似普通互斥量的同步对象。有的人可能会问了,ACE 自己封装了一大堆类最后却做了一个和 ACE_Thread_Mutex 一样功能的同步对象,有什么用处呢? 答案是令牌同步对象具有更多高级的功能:

  • 支持递归锁定;
  • 支持死锁检测,这是通过依赖另外一个类 (ACE_Token_Manager) 的单例来实现的,所有令牌对象都会在该单例中注册,在锁定前,会通过它进行查找,看有无导致死锁的可能,如果发生了导致死锁的锁定,则会直接返回 EDEADLK 错误,从而避免死锁。更进一步,如果打开了调试模式,还会在日志中打印丰富的信息,帮助开发者定位互锁的线程及它们竞争的锁,关于这方面的内容,可以参考我在这篇文章里的回复 《有什么办法检测死锁阻塞在哪里么? 》(附录 18);
  • 在 acquire 中还可以传递一个自定义的通知函数,当没有成功获取锁从而进入等待之前,可以调用该函数用来做一些通知工作,通过合理的设计,这个通知函数可以向持有锁的线程发送消息,告诉它释放锁,这样就可以让当前线程很快得到锁了,而不用“傻乎乎”的进入漫长的等待,关于这一 点,还会在后面 ACE_Token 一节中提到;
  • 最后就是在锁上等待的线程,严格的遵循 FIFO 顺序,不会出现在一些平台上锁的一些“不良”实现导致的饥饿问题——唤醒的线程是无序的从而有一定概率导致一些线程一直陷入等待。

 本类型可用于 ACE_Guard <TYPE> 守卫类型。

ACE_Local_RLock

本地读锁的抽象,通过派生 ACE_Token_Proxy 并提供 ACE_RW_Token 作为底层的实现来制作一个行为类似读写锁中读端的同步对象。后者内部其实也是使用 ACE_Thread_Mutex 外加一个等待者数量来实现的 (而没有采用平台原生的读写锁 ACE_RW_Thread_Mutex 之类),除了具备上一节中的高级功能外,它还具备以下读写锁的特有性质:

  • 读锁在获取锁时如果已经有写锁,则进入等待队列;
  • 读锁在获取锁时如果已经有读锁且不是本锁递归加锁,则进入等待队列,注意这一点与平台提供的读写锁概念有区别,后者在这种情况下是允许多个读锁共存的,而令牌系统的读写锁仅仅是优先级不同,相互之间也都是互斥的;
  • 当解锁写锁时,且等待队列的下一个线程要求读锁时,则会同时唤醒这批连续的读线程,让它们有同样的机率争抢这把锁。如果解锁的是读锁、或下一个等待线程要求写锁时,则只唤醒该线程,来避免语义错误或不必要的竞争;

 本类型可用于 ACE_Read_Guard <TYPE> 守卫类型。

ACE_Local_WLock

本地写锁的抽象,通过派生 ACE_Token_Proxy 并提供 ACE_RW_Token 作为底层的实现来制作一个行为类似读写锁中写端的同步对象。与 ACE_Local_RLock 几乎完全相同,只是返回的类型为写锁、在 ACE_RW_Token 中进入不同的分支条件,从而进入与上面不同的逻辑处理。

本类型可用于 ACE_Write_Guard <TYPE> 守卫类型。

限于这篇文章的主题,只讨论使用相关的问题,并不讨论实现相关的部分,关于 ACE_Mutex_Token / ACE_RW_Token 这里不展开说明。回顾一下之前讲过的模拟读写锁或互斥量 (因平台本身不支持而自定义的),它们内部的实现与这里的 Token 一定有相通之处,关于这方面的对比,留待后面详细说明。

ACE_Remote_Token_Proxy

从这节开始,介绍可以跨进程、跨机器协同的令牌系统。同 ACE_Token_Proxy 一样,ACE_Remote_Token_Proxy 是各种进程间 token 对象的基类,不能直接拿来用,如果想要扩展 token 对象的类型,可以从它派生。通过它我们先来了解一下远程 TOKEN 系统各个类之间的关系:

这个类图是在进程内 token 基础上添加的,其中红色的三个类是对外接口,位于本地;蓝色的三个类是实现,位于一个专门的锁服务进程内 (可能位于另一台机器)。它们存在一一对应的关系,例如 ACE_Remote_Mutex 对应 ACE_TS_Mutex;ACE_Remote_RLock  对应 ACE_TS_RLock;ACE_Remote_WLock 对应 ACE_TS_WLock,TS 意即 Token Server。

如何将进程内的令牌系统拓展到进程间甚至是跨机器呢?我想你已经猜到答案了,就是通过 tcp 连接,将锁定的请求发往一个集中的锁服务,该服务在内部根据机器名 锁名来唯一标识一把锁,当多个远程线程试图锁定同一把锁时,只有在本地真正获得锁的那个实例会向 tcp 回传确认数据,从而让对应的远程线程继续执行;而其它陷入等待的实例因为没有任何数据回传,导致对应的远程线程只能阻塞在同步的 tcp 读过程中,这相当于另一种形式的锁定。

当获得锁的线程解锁时,同样会向锁服务发送一个解锁请求,锁服务得到这个请求后,会在本地解锁对应的锁,这个过程和之前进程内的解锁过程并无二致。不同的是,释放锁后,锁会唤醒在队列上等待的本地线程,该线程获取锁后,将通过 tcp 向对应的远程线程回传一个应答,从而激活对应的远程线程继续执行。记得当年看到这里的实现时,心中不由的称赞一句——妙哇~ 计算机领域擅长将新问题归化为已解决问题、从而依赖之前的解决方案的思路,在这里又得到了一次充分体现。

ACE_Remote_Mutex

远程简单锁的抽象,通过派生 ACE_Remote_Token_Proxy 并提供 ACE_Mutex_Token 作为底层的实现来制作一个行为类似普通互斥量的同步对象。注意,上边说 ACE_Remote_Mutex 和 ACE_TS_Mutex 是一对一的关系,但这并不表示前者底层包含一个后者,因为他们分属两个进程,这里说的对应关系是指 tcp 通信层面的,ACE_Remote_Mutex 会将锁定、解锁的操作封装成一个请求,发往锁服务并分派给对应的 ACE_TS_Mutex 来实现对应的操作,而他们两个底层其实都是依赖的 ACE_Mutex_Token,这个和本地 token 并无二致。

可能有的人会问了,ACE_TS_Mutex 底层依赖 ACE_Mutex_Token 可以理解,毕竟要做锁定、解锁的动作嘛,但是为什么 ACE_Remote_Mutex 也要依赖这个 ACE_Mutex_Token?它不是把请求发往服务器了吗?确实是这样的,不过只说对了一半,因为 ACE 为进程内场景做了优化,它先尝试在本地获取锁,如果失败,就说明进程内已经有线程获取这个锁了,不用"千里迢迢"跑到锁服务器再问一遍,这样可以大大优化进程内场景的性能,只有当本地成功时,才去尝试远程锁服务器。 同理,在释放时,也需要记得释放本地这个“影子”锁。

 本类型可用于 ACE_Guard <TYPE> 守卫类型。

ACE_Remote_RLock

远程读锁的抽象,通过派生 ACE_Remote_Token_Proxy 并提供 ACE_RW_Token 作为底层的实现来制作一个行为类似读写锁中读端的同步对象。

ACE_Token_Handler 封装了与请求、应答传输相关的逻辑,当锁的第一个请求到达锁服务时,后者首先按照请求中声明的类型创建对应的锁 (ACE_TS_xxx),然后在它上面应用请求中声明的操作类型 (acquire / tryacquire / renew / release ...),而根据上面的讨论,我们知道对应的操作其实是委托给了底层的 ACE_Mutex_Token 或 ACE_RW_Token 去实现了,当操作顺利返回或明显出错时,ACE_TS_XXX 将通过底层的 Handler 回传应答,通知请求端结果;当操作被阻塞时,也不回送应答,从而阻塞请求端读应答操作,造成一种等待锁的“假象“。

本类型可用于 ACE_Read_Guard <TYPE> 守卫类型。

ACE_Remote_WLock

远程写锁的抽象,通过派生 ACE_Remote_Token_Proxy 并提供 ACE_RW_Token 作为底层的实现来制作一个行为类似读写锁中写端的同步对象。与 ACE_Remote_RLock 几乎完全相同,只是返回的类型为写锁、在 ACE_RW_Token 中进入不同的分支条件,从而进入与上面不同的逻辑处理。

与 ACE_Token_Handler 相关的还有 ACE_Token_Acceptor 和 ACE_TSS_Connection (未在上图中标出),前者用于锁服务器建立端口监听并创建 ACE_Token_Handler 实例来处理到达的连接上的数据;后者用于锁请求端建立和锁服务器的主动连接,从而发出锁上的各类请求,关于后者在前面介绍线程局部存储时已经提到,这里不再赘述,主要补充一点就是这里使用 TSS 的目的是保证即使同一个进程内的同一类型的 ACE_Remote_XXX 的多个实例也使用独立的连接,从而保证它们之间互不影响。另外他们都是从 ACE_Acceptor / ACE_Event_Handler / ACE_SOCK_Stream 派生而来,目的是为了重用 ACE 已有的 Acceptor-Connector 框架来简化连接的建立过程。

本类型可用于 ACE_Write_Guard <TYPE> 守卫类型。

其实聪明的读者已经发现一个问题,就是要想实现同样数量的远程线程锁定,锁服务必需使用同样多数量的本地线程,这确实是一个远程 TOKEN 的局限。限于这篇文章的主题,只讨论使用相关的问题,并不讨论实现相关的部分,关于 ACE_TS_Mutex / ACE_TS_RLock / ACE_TS_WLock 这里不展开说明,关于远程 token 的实现留在以后详细说明,关于分布式锁服务的一些内容可以参考我之前写的一篇文章《ACE 分布式锁服务介绍》(附录 13)。

ACE_Token

前面介绍的令牌系统已经非常丰富了,这里的 ACE_Token 却和他们不是一个体系。虽然相互之间没有什么直接联系,但是它们的设计理念与实现却非常相似,不同的地方比较少,下面罗列出来做个对比:

  • ACE_Token 一个类包含了 Tokens ACE_Token_Proxy 及其派生类的所有功能:递归锁定、等待通知 (sleep hook)、等待顺序,除了死锁检测,基本上都支持;
  • ACE_Token 中等待锁的线程可定制使用 FIFO 或 LIFO 顺序,前者用来保证线程分派的公平性,后者用来保证性能。而 Token 系统只支持 FIFO 顺序;
  • ACE_Token 针对不同平台,使用不同的唤醒机制,支持 posix 的 unix like 系统使用条件变量 (ACE_Condition_Thread_Mutex),否则使用信号灯 (ACE_Semaphore),这样在没有原生条件变量的平台上 (例如 windows) 上有更好的性能 (不用使用模拟的条件变量了)。而 Token 系统只使用条件变量;
  • ACE_Token 在 ACE 内部有重要应用,而 Tokens 系统虽然支持远程锁这种高大上的东东,最终在 ACE 内部使用的非常少。

ACE_Token 在 ACE 内部主要用于 Reactor 内部的通知,而且仅限基于 select 实现的反应器 (ACE_Select_Reactor)。这是由于 select 本身对多线程支持不足导致的,众所周知,当一个线程对一组 IO 句柄进行 select 操作且阻塞时,其它线程是没有办法同时操作这些句柄的,例如注册、移除或修改关心的事件类型 (读、写或OOB),也没有办法在上面同时 select。所以通常的做法是在等待线程时加一把锁,来保护这一过程不受其它线程竞争的影响。但是这样一来如何在运行过程中更新句柄集呢? 总不能碰运气吧,万一句柄集上一直没有事件,那用户的注册句柄请求岂不等到天荒地老了:

代码语言:javascript复制
1 template <class ACE_SELECT_REACTOR_TOKEN> int
2 ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::register_handler
3   (ACE_Event_Handler *handler,
4    ACE_Reactor_Mask mask)
5 {
6   ACE_TRACE ("ACE_Select_Reactor_T::register_handler");
7   ACE_MT (ACE_GUARD_RETURN (ACE_SELECT_REACTOR_TOKEN, ace_mon, this->token_, -1));
8   return this->register_handler_i (handler->get_handle (), handler, mask);
9 }

观察这段注册句柄的接口实现,貌似上来直接加把锁就去干活了,他是这么普通,却又如此自信,难道他就不担心上面我们提到的问题吗?在正式开始解答这一系列疑惑之前,先说明一下这里的 ACE_Select_Reactor_T,它本身是一个模板类,模板参数是加锁的类型:

代码语言:javascript复制
 1 template <class ACE_SELECT_REACTOR_TOKEN>
 2 class ACE_Select_Reactor_T : public ACE_Select_Reactor_Impl
 3 {
 4 ……
 5 }; 
 6 
 7 typedef ACE_Token ACE_SELECT_TOKEN;
 8 
 9 typedef ACE_Select_Reactor_Token_T<ACE_SELECT_TOKEN> ACE_Select_Reactor_Token;
10 
11 typedef ACE_Select_Reactor_T<ACE_Select_Reactor_Token> ACE_Select_Reactor;

而 ACE_Select_Reactor 正是 ACE_Select_Reactor_T 使用 ACE_Select_Reactor_Token 作为模板参数的 typedef,说到这里,有的人可能已经晕了,不过不要紧,这里只是说明上面那个接口确实是 ACE_Select_Reactor 的一部分。现在回到正题,为什么这段代码可以工作而不是死等呢?原因就在于他使用的 ACE_Select_Reactor_Token 其实就是 ACE_Select_Reactor_Token_T <ACE_Token>,这个形式表明这个类型其实就是一个从 ACE_Token 派生的类型,主要重写了后者的 sleep_hook 方法:

代码语言:javascript复制
1 template <class ACE_SELECT_REACTOR_MUTEX>
2 class ACE_Select_Reactor_Token_T : public ACE_SELECT_REACTOR_MUTEX
3 {
4 public:
5  ……
6   /// Called just before the ACE_Event_Handler goes to sleep.
7   virtual void sleep_hook (void);
8  ……
9 };

这样,当另外线程试图注册句柄时,如果因主线程阻塞在 select 上导致 token 获取失败时,将有机会通过 sleep hook 向 reactor 发出一个通知,这个通知将导致主线程的 select 被唤醒,从而让出 token 为我们所持有,进而这里可以进行句柄更新。当额外的线程更新完毕离开这个函数从而释放 token 时,又会将所有权重新转移给在锁上等待的主线程,让它继续 select 更新后的句柄集。

有的人可能会问为什么向 reactor 发一个通知就可以让阻塞在 select 上的主线程退出,其实这里涉及到了一个小技巧,即 self-pipe-trick,在初始化时创建一对自连接的 tcp / pipe 句柄,将他们默认加入到 select 的句柄集中,当需要解除阻塞时,在上面写入一个字节就可以了,巧妙吧~

ok,解释了这么多,或许还有人一头雾水。没关系,当初我看这段代码时,也没想到 ACE 会在这么不经意的一行锁定代码时塞入这么多逻辑,巧则巧矣,只是“伪装”的太好了,以至于我一开始根本没发现这里的玄机。是后来看到 token 的实现,又搜索整个代码库中的实现,才发现这里别有天地。关于 ACE_Select_Reactor 的更多内容,请参考我之前写的一篇文章《ACE_Select_Reactor 多线程通知机制分析》(附录 12)。

对于 ACE_Select_Reactor 而言,这个机制可能还不明显,毕竟有些同学是在单线程环境下使用这个反应器;但是对于 ACE_TP_Reactor 来说,这个 ACE_Token 就至为重要了,因为它本身就是要在只支持单线程的 select 上使用线程池来优化性能的 (TP 即 Thread Pool),一堆线程跑起来,如果只是“嘎嘣”一下上把锁,那和 ACE_Select_Reactor 又有何异呢?所以 ACE 这里别出新裁,只锁定事件侦测段,不锁定事件分发段,来保证不确定时间的事件分发回调不会影响处理其它连接上到达的请求。

这也是 ACE_TP_Reactor 与 ACE_Select_Reactor 最大的不同,虽然前者派生自后者,但是它为了保证一个连接的一个请求在处理过程中不会被另外的线程对同样的请求继续分派 (例如有读事件时,当数据未读取完成前,select 一直会报告该句柄有可读事件),从而导致的多线程竞争问题,它在分派一个连接上的事件时,会自动将对应的句柄从当前侦测句柄集中移除,直到连接上的数据被处理完成后,才将该句柄加回来 (resume_handler)。

于是我们看到 ACE_Token 使用最多的场景就是当连接上数据处理完成后 resume 的一刻,此时一般已经有一个线程在句柄集上侦测事件了,且陷入了阻塞,如果想把这个处理完数据的句柄再加入进去,必需先通知正在 select 的线程退出阻塞并让出所有权,所有这一切都是一行锁定代码搞定:

代码语言:javascript复制
1 template <class ACE_SELECT_REACTOR_TOKEN> int
2 ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::resume_handler (ACE_HANDLE handle)
3 {
4   ACE_TRACE ("ACE_Select_Reactor_T::resume_handler");
5   ACE_MT (ACE_GUARD_RETURN (ACE_SELECT_REACTOR_TOKEN, ace_mon, this->token_, -1));
6   return this->resume_i (handle);
7 }

记得当时为了印证我的观点,还特意增加了 sleep_hook 中的日志并重新编译 ACE 运行进程观察日志输出。关于 ACE_TP_Reactor 的更多内容,请参考我之前写过的一篇文章《ACE_TP_Reactor 实现 Leader-Follower 线程模型分析》(附录 14)。

既然 ACE_Token 如此好用,为什么不在所有的反应器中使用呢?答案是其它的多路事件分派 api 大多数是支持多线程的,例如 epoll 虽然不支持多个线程同时 epoll_wait,但是可以在一个线程 wait 时另外的线程修改句柄及事件集合,这种修改会实时的反映到当前 wait 的线程中,就大大减少了执行 epoll_wait 的线程无谓的频繁唤醒,提高了性能;更不要说,基于 windows 完成端口 (iocp) 实现的前摄器 (proactor) ,可以直接通过 PostQueuedComplectionStatus 向完成端口发送任意通知,且 GetQueuedCompletionStatus 本身就是支持多线程从 iocp 获取事件的。关于 epoll 和 iocp 的论述,请参考我另一篇文章 《[apue] epoll 的一些不为人所注意的特性》。这种 self-pipe-trick 广泛用于基于 select 的事件驱动库,例如 libevent,关于该技巧引发的一场血案,并由此衍生的 gevent 框架,请参考我写的另一篇文章:《一个工业级、跨平台、轻量级的 tcp 网络服务框架:gevent 》。

说了许多与 ACE_Token 本身不相关的内容,主要是解释这个类型存在的必要性,其实它在 ACE 中有特定的用途,不一定适合通用场景,反而是之前介绍的 Token 系统比较通用,如果你不在意扩展性和死锁检测功能,可以基于 ACE_Token 派生自己的类型去使用,特别是它的 sleep_hook 等待通知功能,一定要利用起来,不然和使用 Mutex 没有什么两样。

NULL

ACE 为了提供灵活性,对锁类型采用模板参数的方式提供,便于用户根据自己的实际场景选择合适的锁类型。但是这也带来了一个问题,就是当用户所在的场景明确是单线程环境不需要锁的时候,也要提供一个锁类型,从而造成性能下降。为了解决这个问题,ACE 使用空类型 (ACE_Null_XXX) 来适配单线程环境。

ACE_Null_Mutex

适配互斥量类型,包括但不限于:

  • ACE_Thread_Mutex
  • ACE_Recursive_Thread_Mutex
  • ACE_RW_Thread_Mutex
  • ACE_RW_Process_Mutex
  • ACE_RW_Mutex
  • ……

凡是可以使用以上类型的,都可以通过 ACE_Null_Mutex 来适配单线程版本。适用于以下守卫类型:

  • ACE_Guard
  • ACE_Read_Guard
  • ACE_Write_Guard

其实上述几个守卫类型针对 ACE_Mull_Mutex 参数做了模板特化,它们压根不会调用后者的接口,而是直接用返回 0 来获得更好的性能。

ACE_Null_Semaphore

适配信号灯类型,包括但不限于:

  • ACE_Thread_Semaphore
  • ACE_Process_Semaphore
  • ……

凡是可以使用以上类型的,都可以通过 ACE_Null_Semaphore 来适配单线程版本。适用于以下守卫类型:

  • ACE_Guard

对于带 timeout 参数的 acquire 操作,ACE_Null_Semaphore 直接返回超时错误,因为它无法模拟被另一个线程唤醒的场景,否则就不是 NULL object 了。

ACE_Null_Condition

适配条件变量类型,包括但不限于:

  • ACE_Condition_Thread_Mutex
  • ACE_Recursive_Condition_Thread_Mutex
  • ACE_Condition <TYPE>
  • ……

凡是可以使用以上类型的,都可以通过 ACE_Null_Condition 来适配单线程版本。对于条件变量,无守卫类型可用。

同 ACE_Null_Semaphore 一样,对于带 timeout 参数的 wait 操作,ACE_Null_Condition 直接返回超时错误。

ACE_Null_Barrier

适配栅栏同步体,包括但不限于:

  • ACE_Barrier
  • ACE_Thread_Barrier
  • ……

凡是可以使用以上类型的,都可以通过 ACE_Null_Barrier 来适配单线程版本。无守卫类型可用。

对于 wait 操作,ACE_Null_Barrier 直接返回 0 表示等到了所有需要同步的线程。

ACE_Null_Token

适配令牌同步体,包括但不限于:

  • ACE_Local_Mutex
  • ACE_Local_RLock
  • ACE_Local_WLock
  • ACE_Remote_Mutex
  • ACE_Remote_RLock
  • ACE_Remote_WLock
  • ……

凡是可以使用以上类型的,都可以通过 ACE_Null_Token 来适配单线程版本。适用于以下守卫类型:

  • ACE_Guard
  • ACE_Read_Guard
  • ACE_Write_Guard

由于它的 create_token 虚函数直接返回空,所以对应的所有操作都直接返回 ENOENT 错误。

ACE_Noop_Token

专门适配 ACE_Token 类型,适用的守卫类型与 ACE_Token 相同。

由于 ACE_Token 应用于 ACE_Select_Reactor,所以它的单线程版本其实就是使用 ACE_Noop_Token 实现的。

另外 ACE_Guard 专门针对 Reactor 中使用的令牌 (ACE_Select_Reactor_Token_T <ACE_Noop_Token>) 参数作了模板特化,它压根不会调用后者的接口,而是直接用返回 0 来获得更好的性能。

结语

以上内容根据 ACE 5.4.1 版本整理,现在最新版本已经到了 7.0.0,看上去根据功能拆分了模块,想使用哪一部分就包含哪一部分,不存在一带一大坨这种问题了,不过限于精力没有进一步详细研究,感兴趣的同学可以自行前往官方网站查看文档。

关于一些模拟类型的实现,限于篇幅就不在本文中展开详述了,后面将开一个系列分别介绍这些模拟类型的实现,名字我都想好了,就叫 simlock 吧,打算支持 linux / mac / windows  三个平台,基于 c 构建,可能没有 ACE 那样面面俱到,但每个设施做的尽量独立且轻量级,可以单独拿来使用那种。做这个库的目的,一是为了复用; 二就是为了学习,比如 ace 中线程局部存储的模拟实现,大大降低了操作系统的神圣感,基本就是一个大数组,让人有种不过如此的赶脚,有助于深入理解平台提供的各种同步设施的理解,感兴趣的同学可以持续关注。又给自己挖了个大坑,希望能如约填上……

参考

[1]. 用pthread进行进程间同步

[2]. Solaris 线程和 POSIX 线程的 API

[3]. 关于操作系统中进程、线程、和任务之间的关系

[4]. acejoy

[5]. system V信号量和Posix信号量

[6]. SunOS与Solaris系统的对应关系

[7]. C/C 跨平台的的预编译宏

[8]. Unix (Solaris) Threads and Semaphores

[9]. ACE网络编程 --ACE库入门:中篇-ACE程序员教程

[10]. ACE TSS 自动清理机制分析与应用

[11]. ACE 栅栏同步体介绍

[12]. ACE_Select_Reactor 多线程通知机制分析

[13]. ACE 分布式锁服务介绍

[14]. ACE_TP_Reactor 实现 Leader-Follower 线程模型分析

[15]. ACE Readers/Writer 锁介绍

[16]. Linux 的多线程编程的高效开发经验

[17]. ACE 示例中的一个多线程问题分析

[18]. 有什么办法检测死锁阻塞在哪里么?

0 人点赞