Date Notes Swift Xcode
2019-03-08 首次提交 4.2 10.1
2019-12-14 内容重整 5.1 10.3

0

Preface

在现代计算机中,操作系统一般都会支持多进程(Process)以及多线程(Thread)技术,使得其可以同时运行多个程序且效率更高。而我们在开发 iOS app 中也时常需要利用到这些特性,以为用户提供更加良好的使用体验。通常来说,一个 iOS app 为一个进程,其中又至少有一个线程,即主线程;前者进程由操作系统创建我们很难干预,而线程则「自由」许多,可以为我们所用。由于多线程技术内容较多,我将把相关内容进行拆分,本文作为该系列第一篇,先从 pthreads 说起。

What

POSIX Threads, usually referred to as pthreads, is an execution model that exists independently from a language, as well as a parallel execution model. It allows a program to control multiple different flows of work that overlap in time. Each flow of work is referred to as a thread, and creation and control over these flows is achieved by making calls to the POSIX Threads API.

– POSIX Threads, Wikipedia

译:

POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)线程,即 pthreads,是一种不依赖于语言的执行模型,也称作并行(Parallel)执行模型。其允许一个程序控制多个时间重叠的不同工作流。每个工作流即为一个线程,通过调用 POSIX 线程 API 创建并控制这些流。

– POSIX 线程,维基百科

如上所述,pthreads 即 POSIX Threads,是一套跨平台的多线程 API,由 C 语言编写。在 Xcode 中,使用 #import <pthread.h> 即可引入 pthreads 相关的 API。但正是由于纯 C 的 API,使用起来不够友好,也需要手动管理线程的整个生命周期。

线程创建

想要开辟一条新的线程来执行任务,我们首先要知道如何创建线程。

PTHREAD_CREATE(3)        BSD Library Functions Manual        PTHREAD_CREATE(3)

NAME
     pthread_create -- create a new thread

SYNOPSIS
     #include <pthread.h>

     int
     pthread_create(pthread_t *restrict thread,
         const pthread_attr_t *restrict attr, void *(*start_routine)(void *),
         void *restrict arg);

DESCRIPTION
     The pthread_create() function is used to create a new thread, with
     attributes specified by attr, within a process.  If attr is NULL, the
     default attributes are used.  If the attributes specified by attr are
     modified later, the thread's attributes are not affected.  Upon success-ful successful
     ful completion, pthread_create() will store the ID of the created thread
     in the location specified by thread.

     Upon its creation, the thread executes start_routine, with arg as its
     sole argument.  If start_routine returns, the effect is as if there was
     an implicit call to pthread_exit(), using the return value of
     start_routine as the exit status.  Note that the thread in which main()
     was originally invoked differs from this.  When it returns from main(),
     the effect is as if there was an implicit call to exit(), using the
     return value of main() as the exit status.

     The signal state of the new thread is initialized as:

           oo   The signal mask is inherited from the creating thread.

           oo   The set of signals pending for the new thread is empty.

RETURN VALUES
     If successful,  the pthread_create() function will return zero.  Other-wise, Otherwise,
     wise, an error number will be returned to indicate the error.

ERRORS
     pthread_create() will fail if:

     [EAGAIN]           The system lacked the necessary resources to create
                        another thread, or the system-imposed limit on the
                        total number of threads in a process
                        [PTHREAD_THREADS_MAX] would be exceeded.

     [EINVAL]           The value specified by attr is invalid.

SEE ALSO
     fork(2), pthread_cleanup_pop(3), pthread_cleanup_push(3),
     pthread_exit(3), pthread_join(3)

STANDARDS
     pthread_create() conforms to ISO/IEC 9945-1:1996 (``POSIX.1'').

BSD                              April 4, 1996                             BSD

pthreads 通过 pthread_create 函数来创建新的线程:

// pthreads.h

/* <rdar://problem/25944576> */
#define _PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT \
	defined(SWIFT_CLASS_EXTRA) && (!defined(SWIFT_SDK_OVERLAY_PTHREAD_EPOCH) || (SWIFT_SDK_OVERLAY_PTHREAD_EPOCH < 1))

__API_AVAILABLE(macos(10.4), ios(2.0))
#if !_PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT
int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
		const pthread_attr_t * _Nullable __restrict,
		void * _Nullable (* _Nonnull)(void * _Nullable),
		void * _Nullable __restrict);
#else
int pthread_create(pthread_t * __restrict,
		const pthread_attr_t * _Nullable __restrict,
		void *(* _Nonnull)(void *), void * _Nullable __restrict);
#endif // _PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT

⚠️

可以注意到的是,pthread.h 中关于 pthread_create 函数的声明使用了 _PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT 宏进行了区分。关于该宏以及其内部判断条件其实没有太多资料可以参考,但大体上可以理解是为了兼容 Swift 中的可选类型,但两种声明本质上并无其他区别。

以 Obj-C 中实际使用的 pthread_create(pthread_t _Nullable *restrict _Nonnull, const pthread_attr_t *restrict _Nullable, void * _Nullable (* _Nonnull)(void * _Nullable), void *restrict _Nullable) 声明为例,其中 _Nullable_Nonnull 是 Obj-C 桥接 Swift 可选(Optional)类型时是否隐式或显式可选的标志;__restrict 是 C99 标准引入的关键字,类似于 restrict,可以用在指针声明处,使得编译器将只能通过该指针本身修改指向的内容,便于其优化。去掉这些不影响函数本身功能的标志,补全参数名即 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)

pthread_t

pthread_create 中的第一个参数是 pthread_t *thread,即指向 pthread_t 的指针。pthread_t 的本质是 _opaque_pthread_t 结构体,这里的 opaque(不透明)意味着外界通常无需关心其内部实现细节:

// sys/_pthread/_pthread_t.h

typedef __darwin_pthread_t pthread_t;

// sys/_pthread/_pthread_types.h

struct _opaque_pthread_t {
	long __sig;
	struct __darwin_pthread_handler_rec  *__cleanup_stack;
	char __opaque[__PTHREAD_SIZE__];
};

typedef struct _opaque_pthread_t *__darwin_pthread_t;

我们可以将这个参数理解为线程的引用即可,当 pthread_create 成功创建好线程,传入的 pthread_t 地址即为一个新的线程。

pthread_attr_t

pthread_create 中的第二个参数是 const pthread_attr_t *attr,即指向 pthread_attr_t 的指针。 pthread_attr_t 的本质是 _opaque_pthread_attr_t 结构体,也是一个不透明类型:

// sys/_pthread/_pthread_attr_t.h

typedef __darwin_pthread_attr_t pthread_attr_t;

// sys/_pthread/_pthread_types.h

struct _opaque_pthread_attr_t {
	long __sig;
	char __opaque[__PTHREAD_ATTR_SIZE__];
};

typedef struct _opaque_pthread_attr_t __darwin_pthread_attr_t;

pthread_attr_t 指的是线程属性,我们可以使用 pthread_attr_init 函数来初始化,以及使用 pthread_attr_set 开头的函数来进行设置:

// 声明 pthread_attr_t
pthread_attr_t attr;

// 初始化
pthread_attr_init(&attr);

// 设置
pthread_attr_setscope(&attr, int);                     // 作用域
pthread_attr_setstack(&attr, void * _Nonnull, size_t); // 栈
pthread_attr_setguardsize(&attr, size_t);              // 栈末尾的警戒缓冲区大小
pthread_attr_setstackaddr(&attr, void * _Nonnull);     // 栈地址
pthread_attr_setstacksize(&attr, size_t);              // 栈大小
pthread_attr_setschedparam(&attr, const struct sched_param *restrict _Nonnull); // 调度参数
pthread_attr_setdetachstate(&attr, int);               // 分离状态
pthread_attr_setschedpolicy(&attr, int);               // 调度策略
pthread_attr_setinheritsched(&attr, int);              // 继承
pthread_attr_set_qos_class_np(&attr, qos_class_t __qos_class, int __relative_priority); // 分配 QoS 类

// 创建线程时将 attr 地址传入
// pthread_create(&thread, &attr, ...);

// 使用完毕销毁
pthread_attr_destroy(&attr);

如果我们不太会去自定义这里的属性,可以将 pthread_create 中的该参数传 NULL 即可使用默认属性来创建线程。

pthread_create

pthread_create 中的最后两个参数是 void *(*start_routine) (void *)void *argstart_routine() 是新线程运行时所执行的函数,arg 是传入 start_routine() 的唯一参数。当 start_routine() 执行终止或者线程被明确杀死,线程也将会终止;pthread_create 的返回值是 int 类型,当返回 0 时为成功,否则将返回错误码:

#import <Foundation/Foundation.h>
#import <pthread.h>

void runForThread_1(char *arg) {
    // pthread_self():返回当前线程 pthread_t
    printf("%s (%p) is running.\n", arg, pthread_self());
    // 退出线程
    pthread_exit(arg);
}

void runForThread_2(void *arg) {
    printf("%s (%p) is running.\n", arg, pthread_self());
    // 函数返回则自动退出线程
}

int main(int argc, const char * argv[]) {
    // 声明两个线程 thread_1 & thread_2
    pthread_t thread_1, thread_2;

    // 使用默认属性创建 thread_1
    int result_1 = pthread_create(&thread_1, NULL, (void *)runForThread_1, "thread_1");

    // 打印 thread_1 创建函数返回值 & 线程地址
    printf("result_1 - %d - %p\n", result_1, thread_1);

    // 检查线程是否创建成功
    if (result_1 != 0) {
        printf("pthread_create thread_1 error.\n");
    }

    // 声明线程属性 attr
    pthread_attr_t attr;
    // 初始化线程属性
    pthread_attr_init(&attr);
    // 销毁线程属性
    pthread_attr_destroy(&attr);

    // 使用 attr 创建 thread_2
    int result_2 = pthread_create(&thread_2, &attr, (void *)runForThread_2, "thread_2");

    printf("result_2 - %d - %p\n", result_2, thread_2);

    if (result_2 != 0) {
        printf("pthread_create thread_2 error.\n");
    }

    // 人为休息 1 秒
    sleep(1);

    return 0;
}

// OUTPUT:
// result_1 - 0 - 0x70000799e000
// result_2 - 22 - 0x0
// pthread_create thread_2 error.
// thread_1 (0x70000799e000) is running.

这里我们使用默认属性 NULL 即可创建通用化的线程;而如果使用已经销毁的线程属性 pthread_attr_t 来创建线程时,就会出现错误从而无法成功创建线程。

需要注意的一点是,pthread_create 函数的第四个参数声明的类型是 void *,即可以指向任意类型的指针。但当我们直接传入 int 等类型的变量时,虽然执行没有问题,但编译器将总是有类似这样的警告 Incompatible integer to pointer conversion passing 'int' to parameter of type 'void *'

#import <Foundation/Foundation.h>
#import <pthread.h>

#define N 5

void *thread_func(void *arg) {
    // WARNING: Format specifies type 'int' but the argument has type 'void *'
    printf("arg = %d\n", arg);
    return NULL;
}

int main(int argc, const char * argv[]) {
    int i;
    pthread_t threads[N];
    for (i = 0; i < N; i++) {
        // WARNING: Incompatible integer to pointer conversion passing 'int' to parameter of type 'void *'
         pthread_create(&threads[i], NULL, thread_func, i);
    }

    // 人为休息 1 秒
    sleep(1);

    return 0;
}

// OUTPUT:
// arg = 1
// arg = 2
// arg = 3
// arg = 4
// arg = 0

首先可以想到的是强制类型转换,即 (void *)i 即可将 int 类型的 i 转换为 void * 来满足需要。但由于 int 类型只占用 4 个字节,小于 void * 指针占用的 8 个字节,因此这里又有了新的警告 Cast to 'void *' from smaller integer type 'int';另外一个可以想到的方法是将变量的地址即 &i 传入,这正好符合此处对于类型的要求,因此警告可以消除,线程函数内部使用时也需要将参数进行取值操作:

#import <Foundation/Foundation.h>
#import <pthread.h>

#define N 5

void *thread_func(void *arg) {
    int *ptr = arg;
    printf("arg = %d\n", *ptr);

    return NULL;
}

int main(int argc, const char * argv[]) {
    int i;
    pthread_t threads[N];
    for (i = 0; i < N; i++) {
        pthread_create(&threads[i], NULL, thread_func, &i);
    }

    // 人为休息 1 秒
    sleep(1);

    return 0;
}

// OUTPUT:
// arg = 3
// arg = 0
// arg = 4
// arg = 0
// arg = 4

但我们发现最后的输出似乎有些重复,这是因为传入地址是有隐患的,按地址传入会使得外界更改变量值时,线程内部使用的参数也会被同时改变。

那么如何仍然使用按值传递又可以避免警告呢?其实只需要将 int 类型先转为 8 个字节长度的 longintptr_t 类型,再转为 void * 即可:

#import <Foundation/Foundation.h>
#import <pthread.h>

#define N 5

void *thread_func(void *arg) {
    printf("arg = %ld\n", (intptr_t)arg);

    return NULL;
}

int main(int argc, const char * argv[]) {
    int i;
    pthread_t threads[N];
    for (i = 0; i < N; i++) {
        pthread_create(&threads[i], NULL, thread_func, (void *)(intptr_t)i);
    }

    // 人为休息 1 秒
    sleep(1);

    return 0;
}

// OUTPUT:
// arg = 0
// arg = 3
// arg = 1
// arg = 2
// arg = 4

pthread_create in Swift

如上文所述,pthreads API 在 Swift 中也可以使用。我们可以在 Swift 中 pthread_create 函数声明处发现,其四个参数均与 Unsafe...Pointer 相关的类型有关,这样的命名是因为 Swift 中的指针语法也被抽象为类型而非 *,并通过 Unsafe 告知开发者其中可能存在隐患。而这里由于 pthread_create 本质是一个 C 语言函数,但 Swift 又无法直接与 C/C++ 进行混编,因此需要做一些转换:

C 语法 Swift 语法
const Type * UnsafePointer<Type>
Type * UnsafeMutablePointer<Type>
Type * const * UnsafePointer<Type>
Type * __strong * UnsafeMutablePointer<Type>
Type ** AutoreleasingUnsafeMutablePointer<Type>
const void * UnsafeRawPointer
void * UnsafeMutableRawPointer

pthread_create 函数中,第一个参数 pthread_t *thread 在 Swift 中将被表示为 UnsafeMutablePointer<pthread_t>,第二个参数 const pthread_attr_t *attr 将被表示为 UnsafePointer<pthread_attr_t>,第三个参数 void *(*start_routine) (void *) 被表示为 (UnsafeMutableRawPointer) -> (UnsafeMutableRawPointer),以及最后一个参数 void *arg 将被表示为 UnsafeMutableRawPointer,这些参数再加上可空性对应的可选类型,最终形成了 Swift 中 pthread_create 函数的 API:

@available(iOS 2.0, *)
public func pthread_create(_: UnsafeMutablePointer<pthread_t?>!, _: UnsafePointer<pthread_attr_t>?, _: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer?, _: UnsafeMutableRawPointer?) -> Int32
// _PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT

前两个参数我们在 Swift 中仍然可以使用 & 来按地址传递,那么后两个参数 C 语言函数和参数该如何传递呢?

为了兼容 Swift 中的各种类型,我们可以将值封装在一个支持泛型的 Box 类型中;并额外定义两个方法,一是 encode:将 Swift 中的值封装在 UnsafeMutableRawPointer 指针类型中,二是 decode:将 UnsafeMutableRawPointer 指针类型中真正的值取出。这两个方法需要成对使用:

class Box<T> {
    let value: T

    init(_ value: T) {
        self.value = value
    }
}

private func decode<T>(_ memory: UnsafeMutableRawPointer) -> T {
    let unmanaged = Unmanaged<Box<T>>.fromOpaque(memory)
    defer { unmanaged.release() } // 
    return unmanaged.takeUnretainedValue().value
}

private func encode<T>(_ t: T) -> UnsafeMutableRawPointer {
    return Unmanaged.passRetained(Box(t)).toOpaque()
}

接下来的使用就十分类似 Obj-C 了:

//  Swift 
func runForThread_1(_ arg: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? {
    print("\(decode(arg) as String) (\(pthread_self())) is running.")

    return nil
}

// 线 thread_1 & thread_2
var thread_1, thread_2: pthread_t!

//  thread_1
let result_1 = pthread_create(&thread_1, nil, runForThread_1, encode("thread_1"))

print("result_1 - \(result_1) - \(String(describing: thread_1))");

// 线
if result_1 != 0 {
    print("pthread_create thread_1 error.")
}

// 线 attr使 pthread_attr_t 
var attr = pthread_attr_t()
// 线
pthread_attr_init(&attr)
// 线
pthread_attr_destroy(&attr)

let result_2 = pthread_create(&thread_2, &attr, runForThread_1, encode("thread_1"))

print("result_2 - \(result_2) - \(String(describing: thread_2))");

if result_2 != 0 {
    print("pthread_create thread_2 error.")
}

//  1 
sleep(1)

// OUTPUT:
// result_1 - 0 - Optional(0x0000700002cd8000)
// thread_1 (0x0000700002cd8000) is running.
// result_2 - 22 - nil
// pthread_create thread_2 error.

线程执行

在「线程创建」一节中,我们可以发现 pthread_create 函数不仅创建了新的线程,其也会执行线程函数,而不像某些 API 的创建与执行是分开的。

其实当 pthread_create 函数返回 0 时,它只能保证线程被成功创建,但并不能保证线程可以得到立即执行。只有当新的线程获得了 CPU 的时间片(Timeslice),其才可以得到执行。而是否能够优先获得时间片,则取决于线程的优先级。线程默认属性的调度策略是 SCHED_OTHER,该策略下没有优先级选择,因此我们可以通过 pthread_attr_setschedpolicy 更改调度策略:

// pthread/pthread_impl.h

/*
 * POSIX scheduling policies
 */
#define SCHED_OTHER                1
#define SCHED_FIFO                 4
#define SCHED_RR                   2

// main.m

pthread_attr_setschedpolicy(&attr, SCHED_RR);

线程同步

pthread_join

在上面代码示例中的 main 函数中,都在末尾会有这么一句 sleep(1),作用是让当前线程休息 1 秒,这是为什么呢?我们可以尝试注释掉这一行 sleep() 函数的调用再运行一次:

#import <Foundation/Foundation.h>
#import <pthread.h>

void runForThread(char *arg) {
    printf("%s (%p) is running.\n", arg, pthread_self());
}

int main(int argc, const char * argv[]) {
    // 输出主线程信息
    printf("(%p) is running.\n", pthread_self());

    pthread_t thread;

    if (pthread_create(&thread, NULL, (void *)runForThread, "thread") != 0) {
        printf("pthread_create thread error.\n");
    }

    // sleep(1);

    return 0;
}

// OUTPUT:
// (0x1000d2dc0) is running.

此时将只有主线程的输出,而新创建的子线程 thread 却没有输出。这是因为主线程执行结束即程序结束, thread 子线程根本来不及去执行。所以 sleep(1) 目的是人为将主线程休眠 1 秒,等待子线程去执行,但这样硬核去休眠固定的时间并不是合理的操作。其实 pthreads 为我们提供了 pthread_join API 来阻塞当前线程,等待目标线程执行完毕后再继续执行当前线程:

PTHREAD_JOIN(3)          BSD Library Functions Manual          PTHREAD_JOIN(3)

NAME
     pthread_join -- wait for thread termination

SYNOPSIS
     #include <pthread.h>

     int
     pthread_join(pthread_t thread, void **value_ptr);

DESCRIPTION
     The pthread_join() function suspends execution of the calling thread
     until the target thread terminates, unless the target thread has already
     terminated.

     On return from a successful pthread_join() call with a non-NULL value_ptr
     argument, the value passed to pthread_exit() by the terminating thread is
     stored in the location referenced by value_ptr.  When a pthread_join()
     returns successfully, the target thread has been terminated.  The results
     of multiple simultaneous calls to pthread_join(), specifying the same
     target thread, are undefined.  If the thread calling pthread_join() is
     cancelled, the target thread is not detached.

RETURN VALUES
     If successful,  the pthread_join() function will return zero.  Otherwise,
     an error number will be returned to indicate the error.

ERRORS
     pthread_join() will fail if:

     [EDEADLK]          A deadlock was detected or the value of thread speci-fies specifies
                        fies the calling thread.

     [EINVAL]           The implementation has detected that the value speci-fied specified
                        fied by thread does not refer to a joinable thread.

     [ESRCH]            No thread could be found corresponding to that speci-fied specified
                        fied by the given thread ID, thread.

SEE ALSO
     wait(2), pthread_create(3)

STANDARDS
     pthread_join() conforms to ISO/IEC 9945-1:1996 (``POSIX.1'').

BSD                              April 4, 1996                             BSD

pthread_join 函数在 Obj-C 下的声明如下,其中,第一个参数即等待的目标线程;第二个参数保存了线程函数的返回值;返回值是 int 类型,当返回 0 时为成功,否则将返回错误码:

// pthread.h

__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_join(pthread_t , void * _Nullable * _Nullable)
		__DARWIN_ALIAS_C(pthread_join);

我们尝试使用 pthread_join 改写上面的代码示例:

#import <Foundation/Foundation.h>
#import <pthread.h>

void runForThread(char *arg) {
    printf("%s (%p) is running.\n", arg, pthread_self());
}

int main(int argc, const char * argv[]) {
    printf("(%p) is running.\n", pthread_self());

    pthread_t thread;

    if (pthread_create(&thread, NULL, (void *)runForThread, "thread") != 0) {
        printf("pthread_create thread error.\n");
    }

    pthread_join(thread, NULL);

    printf("main thread exit.\n");

    return 0;
}

// OUTPUT:
// (0x1000d2dc0) is running.
// thread (0x70000289c000) is running.
// main thread exit.

这次主线程将一直等待 thread 子线程执行结束后才继续执行,这种两个或多个线程协作执行,可以称之为线程同步。当然,pthread_join 只是线程同步的一种方式。

pthread_join 中仍需注意的一点是,其第二个参数类型是 void **,因此传入的应当是 void * 变量的地址:

#import <Foundation/Foundation.h>
#import <pthread.h>

void *runForThread(char *arg) {
    printf("%s (%p) is running.\n", arg, pthread_self());

    // 使用 malloc 分配内存
    double *result = (double *)malloc(sizeof(double));
    *result = 3.14;

    // 将结果作为线程退出函数的参数,或直接返回结果
    // pthread_exit((void *)result);
    return (void *)result;
}

int main(int argc, const char * argv[]) {
    printf("(%p) is running.\n", pthread_self());

    pthread_t thread;

    if (pthread_create(&thread, NULL, (void *)runForThread, "thread") != 0) {
        printf("pthread_create thread error.\n");
    }

    void *result = NULL;
    pthread_join(thread, &result);

    printf("thread result - %lf\n", *(double *)result);

    // 使用 free 释放内存
    free(result);

    printf("main thread exit.\n");

    return 0;
}

// OUTPUT:
// (0x1000d2dc0) is running.
// thread (0x70000537e000) is running.
// thread result - 3.140000
// main thread exit.

Swift 下的 pthread_join 用法基本一致:

func runForThread(_ arg: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? {
    print("\(decode(arg) as String) (\(pthread_self())) is running.")

    return encode(3.14)
}

print("(\(pthread_self())) is running.")

var thread: pthread_t!

if pthread_create(&thread, nil, runForThread, encode("thread")) != 0 {
    print("pthread_create thread error.")
}

var res: UnsafeMutableRawPointer!
pthread_join(thread, &res)

print("res - \(decode(res) as Double)")

print("main thread exit.")

// OUTPUT:
// (0x00000001000d6dc0) is running.
// thread (0x0000700007bb2000) is running.
// res - 3.14
// main thread exit.

互斥锁

pthreads 中还提供了互斥锁(Mutex)来实现线程同步,但由于锁相关的内容将会统一整理,本文不再赘述。

条件变量

由于条件变量内容与互斥锁有所关联,一并统一整理,本文不再赘述。

Reference