Thread

611 阅读15分钟

Threading Programming Guide

What Are Threads

Threads are a relatively lightweight way to implement multiple paths of execution inside of an application.

From a technical standpoint, a thread is a combination of the kernel-level and application-level data structures needed to manage the execution of code. The kernel-level structures coordinate the dispatching of events to the thread and the preemptive scheduling of the thread on one of the available cores. The application-level structures include the call stack for storing function calls and the structures the application needs to manage and manipulate the thread’s attributes and state.

In a non-concurrent application, there is only one thread of execution. That thread starts and ends with your application’s main routine and branches one-by-one to different methods or functions to implement the application’s overall behavior. By contrast, an application that supports concurrency starts with one thread and adds more as needed to create additional execution paths. Each new path has its own custom start routine that runs independently of the code in the application’s main routine.

Having multiple threads in an application provides two very important potential advantages:

  • Multiple threads can improve an application’s perceived responsiveness.
  • Multiple threads can improve an application’s real-time performance on multicore systems.
  • 应用内轻量级的方法实现执行多条代码路径
  • 技术角度看, 线程是内核级和应用级管理代码执行的数据结构的组合
    • 内核级(内核态)数据结构管理线程的调度, 抢占式调度线程到可用的内核上
    • 应用级(用户态)数据结构管理线程的调用栈, 属性及状态
  • 非并发应用只有一个线程-主线程, 串行的执行任务, 需要并发执行任务则要添加额外的执行路径
  • 多线程的好处
    • 提高应用的感知响应
    • 提高多核系统的执行效率(提高性能)

Of course, threads are not a panacea for fixing an application’s performance problems. Along with the benefits offered by threads come the potential problems. Having multiple paths of execution in an application can add a considerable amount of complexity to your code. Each thread has to coordinate its actions with other threads to prevent it from corrupting the application’s state information. Because threads in a single application share the same memory space, they have access to all of the same data structures. If two threads try to manipulate the same data structure at the same time, one thread might overwrite another’s changes in a way that corrupts the resulting data structure. Even with proper protections in place, you still have to watch out for compiler optimizations that introduce subtle (and not so subtle) bugs into your code.

  • 多线程不是解决应用执行性能的万能药, 使用多线程带来好处的同时, 也有潜在风险
  • 多条代码执行路径增加了代码的复杂度, 多路径执行可能会访问共享数据(数据访问的同步问题)

Threading Terminology

This document adopts the following terminology:

  • The term thread is used to refer to a separate path of execution for code.
  • The term process is used to refer to a running executable, which can encompass multiple threads.
  • The term task is used to refer to the abstract concept of work that needs to be performed.
  • thread 线程表示独立的代码执行路径
  • process进程表示正在执行中的程序(动态名词), 能够包含多个线程
  • task 任务表示一个抽象概念, 要执行的工作

Alternatives to Threads

可选择的多线程方案

  • Operation objects
    • 封装了执行的任务, 可调度在辅助线程中执行
    • 隐藏了线程的管理与调度, 使得开发更集中于自身的任务中
    • 配合Operation Queue使用, 其管理更多的线程
  • Grand Central Dispatch (GCD)
    • 使用GCD, 添加要执行的任务到特定的执行队列中, 系统会分配合适的线程调度任务执行, 详情查看Concurrency Programming Guide
  • Idle-time notifications
    • 对于一些短的, 低优的任务可以通过监听Idle-time通知, 在应用不忙的时候执行
    • Cocoa提供了 NSNotificationQueue 对象用于Idle-time通知监听, 向默认的NSNotificationQueue中添加NSPostWhenIdle项, 即可监听
  • Asynchronous functions
    • 系统接口中提供了很多异步功能的方法以执行并发任务
    • 这些API使用系统的守护程序和进程, 或是自行创建线程来执行我们的的任务并返回相应结果
    • 使用这些接口替代自定义的线程(自定义的线程要考虑调度, 数据结构线程控制块啥的)
  • Timers
    • 用来执行一些周期性的任务(很琐碎但仍需定期检查的任务), 使用参考Timer Sources
  • Separate processes
    • 相对于线程更为重量级, 要执行的任务与当前程序切面相关(有一丢丢关联), 并且还需要大量的内存, 需要根权限执行, 用个独立进程更合适些
    • 当使用 fork 函数加载独立进程的时候,必须总是在 fork 后面调用 exec 或者类似的函数

Threading Support

OS X and iOS上的多线程技术

  • Cocoa threads

    • NSThread
    • NSobjectperformSelectorInBackground:withObject: detachNewThreadSelector:toTarget:withObject:等方法
  • POSIX threads

  • Multiprocessing Services

应用层(用户态), 线程同其他平台线程执行周期是一致的, 在启动一个线程之后, 线程主要有三个执行状态

  • running运行
  • ready就绪(已有运行所需的所有资源, 只缺CPU资源调度)
  • blocked阻塞

线程在这三个状态间持续切换, 直到退出到达终止状态

Run Loops

A run loop is a piece of infrastructure used to manage events arriving asynchronously on a thread. A run loop works by monitoring one or more event sources for the thread. As events arrive, the system wakes up the thread and dispatches the events to the run loop, which then dispatches them to the handlers you specify. If no events are present and ready to be handled, the run loop puts the thread to sleep.

You are not required to use a run loop with any threads you create but doing so can provide a better experience for the user. Run loops make it possible to create long-lived threads that use a minimal amount of resources. Because a run loop puts its thread to sleep when there is nothing to do, it eliminates the need for polling, which wastes CPU cycles and prevents the processor itself from sleeping and saving power.

To configure a run loop, all you have to do is launch your thread, get a reference to the run loop object, install your event handlers, and tell the run loop to run. The infrastructure provided by OS X handles the configuration of the main thread’s run loop for you automatically. If you plan to create long-lived secondary threads, however, you must configure the run loop for those threads yourself

  • Run loop是一种基础结构, 在线程中管理异步到达线程的事件
  • 给线程检测观察一个或多个事件源
  • 事件到达, 系统唤醒线程执行相应的事件任务, 没有事件时, 则让线程处于休眠状态
  • 正常使用线程时, 不需要用到Run loop
  • 使用少量资源就可创建长生命周期的线程, 可以做到用时忙, 闲时休息, 可减少CPU调度, 节约电量
  • 主线程系统自动启动了Run loop, 辅助线程需要自行启动
  • 详情Run Loops

Synchronization Tools

多线程访问共享资源时, 容易产生同步问题

When maintaining completely separate resources is not an option though, you may have to synchronize access to the resource using locks, conditions, atomic operations, and other techniques.

可以使用, 条件, 原子操作等保证共享资源的同步访问

  • 锁 提供了一个时刻只有一个线程访问共享资源(执行代码)的能力, 常用的锁是互斥锁, 某个线程想要访问共享资源时, 需要等待锁被释放处于空闲状态, 线程持有锁后才能执行代码, 否则会线程阻塞, 一直等待其他线程将锁资源释放
  • 条件 系统提供条件机制, 确保应用内的任务按照既定顺序执行, 某个线程未满足条件时, 这个线程就会被阻塞, 只有这线程的条件满足时(条件为真), 线程才被释放, 并允许继续执行
  • 原子操作 一般都是通过硬件指令达到原子操作, 常见的有TestAndSet, Swap
  • 详情查看Synchronization Tools

Inter-thread Communication

好的设计应该尽可能少的进行通讯, 但线程间的通讯又是十分必要的

线程通讯方式

  • Direct messaging
    • Cocoa应用支持了在其他线程执行selector的能力, 这表明一个线程可以在另一个线程执行方法
    • 应为方法都执行在目标线程的上下文中, 消息也都会在目标线程中自动序列化
  • Global variables, shared memory, and objects
    • 多线程间共享全局变量, 内存和对象来通信
    • 虽然这种方式更加简单, 但是是很脆弱的, 共享数据的同步问题, 容易导致数据错乱奔溃等情况
  • Conditions
    • 控制线程执行代码的同步工具
    • 可以看做是守门员, 条件满足时才让线程继续执行
    • 详情Using Conditions
  • Run loop sources
    • 自定义的run loop source就是为了在线程上接收来自应用的特定消息
    • 是由事件驱动的, run loop sources可以使得线程做到无事可做时, 自动休眠
  • Ports and sockets
    • 更为复杂且精细的通讯方式, 也是一种可靠的技术
    • 可以用于外部的进程和服务使用
    • ports也是基于run loop sources实现的, 它是基于端口的事件
  • Message queues
  • Cocoa distributed objects
    • 是Cocoa中基于端口通讯的高度实现
    • 尽管可以用做线程间的通讯, 但是这样会有着大量的开销, 并不建议使用
    • 更适合于进程间的通讯
    • 详情Distributed Objects Programming Topics.(已过期,已使用 XPC Services替代)

Design Tips

  • Avoid Creating Threads Explicitly
    • 手动实现线程枯燥且容易发生错误(用户态, 内核态数据结构, 调度, 线程状态等), 尽量不显示创建线程
    • 使用系统提供的线程方案: GCD, Operation Object, 异步执行的APIs
  • Keep Your Threads Reasonably Busy
    • 线程消耗宝贵的系统资源(CPU, 内存, 寄存器..), 手动管理线程时, 要保证线程的使用效率, 与此同时, 也不用担心终止那些空闲的线程(可以及时回收资源)
    • 线程使用的资源是有限的, 及时释放线程, 可以释放出资源(内存空间)共系统的其他进程去使用
    • 在终止线程时, 要有最基础的测量标准去检测, 终止线程确实能够提高性能, 而不是相反作用
  • Avoid Shared Data Structures
    • 代码执行路径最好平行(线程间做好没有通信, 或是只有少量通信)
    • 在多线程应用里, 要小心共享数据的使用
  • Threads and Your User Interface
    • 应用含有图形界面, 推荐在主线程接收用户的操作事件以及初始化/更新界面显示
    • Cocoa框架就是如此, 在主线程管理界面相关的一切操作
    • 也有一些例外, 在辅助线程中做一些图形界面的渲染工作(图像处理, 图像计算等耗时的任务)
  • Be Aware of Thread Behaviors at Quit Time
    • 进程一直运行直到所有非独立线程都已经退出为止, 默认情况下, 只有应用程序的主线程是以非独立的方式创建的, 但是你也可以使用同样的方法来创建其他线程
    • 以非独立的方式创建线程(又称作为可连接的)需要做一些额外的工作, 因为大部分上层线程封装技术默认情况下并没有供创建可连接的线程, 必须使用POSIX API来创建想要的线程, 此外须在主线程添加代码,当最终退出的时候连接非独立的线程
  • Handle Exceptions
    • 异常处理的发生依赖于当前调用栈
    • 每个线程都有自己的调用栈, 所以每个线程负责捕获自己发生的异常
    • 辅助线程获取异常失败, 那么主线程获取异常也会失败
    • 当前线程想要告知其他线程(通常为主线程)发生的异常, 就得给其他线程发送消息
    • 捕获异常后,视情况而定(取决于设计), 后续是继续执行, 还是等待指示亦或是退出
    • 在Cocoa中, 异常对象(NSException)是一个自包含对象, 如果被引用了, 发生异常了, 可从一个线程传递到另一个线程
    • 某些情况下, 异常处理是自动创建的, OC中@synchronized块中隐式创建了异常处理逻辑
  • Terminate Your Threads Cleanly
    • 最好的退出方式就是执行完后自然退出
    • 通常由很多函数可以立即终止线程, 但一般这些函数都是最后的手段, 如果线程访问了一些资源, 用这些函数突出线程, 应该释放的资源没有释放, 可能会出问题
  • Thread Safety in Libraries
    • 开发库时, 应该考虑多线程的情况, 假定会在多线程的情况下使用库, 库的开发就要保证线程安全, 该加锁加锁
    • 使用的库来保持线程安全, 而不是依赖调用库的线程去确保
    • 开发Cocoa类库时, 可以监听NSWillBecomeMultiThreadedNotification通知, 明确应用是否是多线程, 当然也不可太依赖这个通知, 类库代码调用的时候, 这个通知可能就已经发送过了, 这就收不到通知了

Thread Costs

线程创建是需要用到内核的数据结构, 要和内核交互, 所以创建线程是一件成本很高的操作

线程所需数据结构的开销

  • Kernel data structures
    • 大约1KB
  • Stack space
    • Mac OS X主线程 8M
    • iOS主线程 1M
    • 辅助线程512KB
  • Creation time
    • 大约90微妙(依赖于处理器, 运算器)

Operation对象由于有内核的支持, 创建线程更加迅速, 创建的线程在内核的线程池中管理, 多次线程使用可以减少线程创建

在应用中也不可过多的创建线程, 也不要让多个线程等待少量的锁资源来执行, 这反而会降低多线程的效率

Creating a Thread

  • 创建low-level的线程是简单容易的
  • 要有线程的启动入口(一般为函数/方法)

NSThread方式

[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];

NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                        selector:@selector(myThreadMainMethod:)
                                        object:nil];
[myThread start];  // Actually create the thread

POSIX Threads方式

#include <assert.h>
#include <pthread.h>
 
void* PosixThreadMainRoutine(void* data)
{
    // Do some work here.
 
    return NULL;
}
 
void LaunchThread()
{
    // Create the thread using POSIX routines.
    pthread_attr_t  attr;
    pthread_t       posixThreadID;
    int             returnVal;
 
    returnVal = pthread_attr_init(&attr);
    assert(!returnVal);
    returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    assert(!returnVal);
 
    int     threadError = pthread_create(&posixThreadID, &attr, &PosixThreadMainRoutine, NULL);
 
    returnVal = pthread_attr_destroy(&attr);
    assert(!returnVal);
    if (threadError != 0)
    {
         // Report an error.
    }
}

NSObject to Spawn a Thread方式

[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];

Configuring Thread Attributes

  • Configuring the Stack Size of a Thread
    • Cocoa
      • 使用NSthreadsetStackSize:方法
    • POSIX
      • 创建pthread_attr_t结构
      • pthread_attr_setstacksize设置大小
      • 使用attr结构做参pthread_create创建线程
    • Multiprocessing Services
      • 使用MPCreateTask方法设置
  • Configuring Thread-Local Storage
    • Cocoa
      • 使用threadDictionary存储数据
    • POSIX
      • pthread_setspecific pthread_getspecific这两个方法存储和获取数据
  • Setting the Detached State of a Thread
    • 设置分离状态的线程, 这样系统就能在线程执行完毕后自动回收
    • 分离线程也不需要显式地和程序进行交互
    • 相应的有可连接线程, 可连接线程在系统回收资源之前需要有其他线程进行连接, 可连接线程也提供了简明的方式来传递数据, 在调用pthread_exit函数的时候, 会传递数据指针或者直接将数据返回
  • Setting the Thread Priority
    • Cocoa
      • setThreadPriority:
    • POSIX
      • pthread_setschedparam

Writing Your Thread Entry Routine

设置线程的执行入口

  • 需要初始化数据结构
  • 执行任务
  • 可选择的设置Run loop
  • 线程执行完毕执行清理工作

Creating an Autorelease Pool

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
 
    // Do thread work here.
 
    [pool release];  // Release the objects in the pool.
}
  • 最外层的autoreleasepool只有在线程退出的时候才会执行清理操作
  • 生命周期较长的线程需要额外添加autoreleasepool定期的去清除对象

Setting Up an Exception Handler

  • 线程需要捕获可能发生的任何异常
  • 线程中异常捕获失败可能导致应用程序退出
  • 在线程起始出使用try/catch规避可能发生的异常

Setting Up a Run Loop

  • 线程等待事件达到(事件驱动任务执行)需要配置run loop

Terminating a Thread

  • 线程退出推荐的方式是自然退出, 执行任务结束后退出
  • Cocoa, POSIX, Multiprocessing Services都提供了直接退出线程的方法, 但这些方法都强烈不建议使用
  • 强行杀死线程, 会阻止线程的清理工作, 可能会造成内存泄漏等一些列潜在问题
  • 如果先要在任务运行中终止线程, 则需要考虑设计线程外部响应线程的退出或者取消
  • 一种取消方式就是使用Run loop source添加取消线程的事件, 以此来达到接收取消线程的消息

Checking for an exit condition during a long job

- (void)threadMainRoutine
{
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
 
    // Add the exitNow BOOL to the thread dictionary.
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
 
    // Install an input source.
    [self myInstallCustomInputSource];
 
    while (moreWorkToDo && !exitNow)
    {
        // Do one chunk of a larger body of work here.
        // Change the value of the moreWorkToDo Boolean when done.
 
        // Run the run loop but timeout immediately if the input source isn't waiting to fire.
        [runLoop runUntilDate:[NSDate date]];
 
        // Check to see if an input source handler changed the exitNow value.
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}

理解如有错误 望指正 转载请说明出处