天天看点

ios 多线程开发(二)线程管理

线程管理

iOS和OS X中每一个进程(或程序)由一个或多个线程组成。程序由一个运行main方法的线程开始,中间可以产生其他线程来执行一些指定的功能。

当程序产生一个新线程后,这个线程在程序进程空间内变为一个独立的实体。每个线程有它自己的执行栈。线程可以和其他线程通讯,执行I/O操作,以及其他你想要它做的事。由于他们在同一个进程中,所有一个程序的所有线程共享虚拟内容并且他们和进程有同样的访问权限。

线程消耗

多线程会多消耗内存和性能。每个线程都需要在系统内核和程序的内存空间上申请内存。用来管理和调度线程的核心数据结构存在内核上。每个线程的调用栈以及数据存在程序的内存中。大多数数据结构在第一次创建线程时就初始化了。

下面说一下自己创建线程的消耗。有一些是可以配置的,比如子进程的栈大小。下面写的创建线程用的时间只是个粗略估计,只能用来做相对比较。创建现在消耗的时间很大程度上依赖与处理器负载,电脑运行速度,可以使用的系统内存和程序内存。

项目 大概消耗 说明
kernel data structures 大概1KB 这个内存用来存储线程的数据结构和属性。
512KB(子线程)8MB(OS X主线程) 1MB(iOS主线程)  子线程最小的堆大小为16KB,堆的大小必须是4KB的倍数。在线程创建时这个内存就被预留出来了,但是在真正使用时在创建。
创建时间  大概90微秒 这个值时间是初始化调用和线程入口开始执行代码的时间。这个值是在intel 2GHz双核处理器1GB内存OS X 10.5上算出的均值。

注意:由于一些底层的支持,operation objects可以更快的创建线程。相对于每次重新创建线程,他们使用线程池中已有的线程来节省创建时间。 

创建线程

创建一个线程相对比较简单。所有情况下,都需要有一个方法作为线程的主入口。下面介绍一下基础的创建线程和经常要用到的线程技术。

使用NSThread

使用NSThread类创建线程有两种方法。

  1. 使用类方法detachNewThreadSelector:toTarget:withObject来产生一个新线程。
  2. 创建一个NSThread类,然后调用它的start方法。

这两种方法创建的线程,在结束时,系统会自动回收它的资源。也就是说你不需要在结束时调用join。detachNewThreadSelector:toTarget:withObject基本上在所有本版SDK中都支持。使用它创建一个线程,只需要提供线程入口的方法名,定义方法的对象,以及想要传递的参数。下面是一个调用这个方法的例子。

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

使用NSThread对象创建线程的方法和上面的相似。但是它不会马上启动,需要调用start方法来启动

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

提示:另外一个不使用initWithTarget:selector:object:的实现方法是继承NSThread类然后重写它的main方法。同时需要使用重写的方法作为线程的入口。

如果有一个NSThread对象线程在运行,一个给它发送消息的方法是调用performSelector:onThread:withObject:waitUntilDone: 方法。它提供了一种很方便的方法来实现线程间通讯。使用这种技术发送的消息会在另一个线程的run-loop上执行。(当然,这就意味着目标线程是在它的run loop上执行的)。当使用这种方式进行线程通讯时你仍然要处理线程同步问题。

提示:虽然偶尔的线程间通讯使用这个很方便,但是频繁的通讯不建议使用这种方法。

使用POSIX线程

OS X和iOS都提供了POSIX线程API。这个技术基本上可以在任何程序中使用(报错Cocoa和Cocoa Touch程序)并且如果需要写跨平台程序这种方法可能更方便一些。POSIX创建线程的方法是pthread_create.

下面展示了两个方法来创建POSIX线程。LaunchThread方法创建一个主入口是PosixThreadMainRoutine方法的线程。由于POSIX默认创建线程为joinable,这个例子改变了线程的默认属性,创建了一个detached线程。让线程的属性为detached可以让系统在线程结束后立刻回收它占用的资源。

#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.
    }
}      

如果把上面的代码加倒你的代码中然后调用LaunchThread方法,它会创建一个detached线程。不过新创建出的线程没有做任何有意义的事。它会创建然后立刻退出。如果要更有意思,你可以在PosixThreadMainRoutine方法中找点事让它做。也可以在创建线程的时候传递一些数据给它,在pthread_create的最后一个参数中传递。

如果要在新创建的线程和主线程间通讯,需要设置在线程间设置一些通讯通道。对于C程序,有好几种方法可以用来线程间通讯。对于长时间存在的线程,需要设置一种方式来让主线程可以查看它的状态以及中止它。

使用NSObject创建线程

所有的对象都可以创建线程然后用它来执行他们的方法。performSelectorInBackground:withObject: 方法可以创建一个你指定入口的线程。假如有一个对象(myObj)并且你想在后台线程中运行它的doSomething,你可以用下面的代码来实现

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

这样创建的效果和使用NSThread的detachNewThreadSelector:toTarget:withObject:是一样的。新的线程立刻就被创建出来并且以默认配置开始运行了。在selector里,可以设置你想要的线程的属性。比如,设置一个autorelease pool(如果不使用垃圾回收机制的话),配置线程的run loop(如果你想使用的话)。

在Cocoa程序中使用POSIX线程

虽然在Cocoa程序中NSThread类是创建线程的主要方法,但是如果觉得POSIX线程更方便的话,也可以使用。比如,你已经有使用POSIX线程的代码,但是不想重写他们。如果打算使用POSIX线程的话,最好也了解一下与Cocoa程序间的交互。

保护Cocoa框架

对于多线程程序,Cocoa框架使用锁以及一些其他方式来保证他们正常工作。为了防止这些锁降低单线程程序的性能,Cocoa知道NSThread创建第一个新线程时才初始化他们。如果只使用POSIX线程,Cocoa不会收到当前程序是多线程程序的通知。这种情况下,调用Cocoa框架的操作有可能变的不稳定或者crash。

要让Cocoa知道打算使用多线程,只需要用NSThread类创建一个线程,然后让它立刻退出就可以了。线程的入口不需要做任何事。只需要创建NSThread线程来让Cocoa框架中的锁初始化。

如果不确定Cocoa是否认为当前程序是多线程的,可以调用NSThread的isMultiThreaded来查看。

混合POSIX和Cocoa锁

在一个程序中使用POSIX和Cocoa锁是安全的。Cocoa的lock和condition对象本质上是封装的POSIX mutexes和conditions。对于一个锁,必须使用同样的接口来管理它。换句话说,不能使用Cocoa的NSLock来管理pthread_mutex_init创建的mutex。反过来也不行。

配置线程属性

配置线程的栈大小

对于每个创建的线程,系统会在进程的空间中申请一部分内存作为线程的栈。这个栈管理栈信息以及线程中的局部变量。

如果想要改变栈的大小,必须要在创建线程之前做。所有的多线程技术都提供了一些方法来设置栈的大小,虽然NSThread只能在iOS以及OS X v10.5之后才支持。

设置线程栈大小

技术 备注
Cocoa 在iOS和OS X v10.5之后,创建一个Thread对象,在调用start方法之前,调用setStackSize:方法来指定栈的大小
POSIX 创建一个pthread_attr_t数据结构然后使用pthread_attr_setstacksize方法来改变默认的栈大小。在创建线程的时候把这个属性传递给pthread_create方法。
Multiprocessing Service 创建线程时传递合适的栈大小给MPCreateTask

配置基于线程的存储

每个线程都维护了一个在线程任何地方都可以访问的键值对字典。可以使用这个字典来维护线程运行过程中的数据。例如,想要在在线程运行时保存一些状态信息。

Cocoa和POSIX使用不同的方式保存字典,所以两种技术不能混着调用。在Cocoa中,使用NSThread的threadDictionary来获得字典对象。在POSIX中,使用pthread_setspecific和pthread_getspecific方来来设置和获取值。

配置线程的detached状态

大部分上层线程技术默认创建detached线程。大多数情况都会选择detached线程,因为他们在线程结束后可以让系统自动回收资源。作为对比,系统不会自动回收joinable类型的线程直到其他线程join它。进程会阻塞执行join的线程。

可以把joinable线程看作子线程。虽然他们以独立线程运行,一个joinable线程只有在其他线程join后资源才会被回收。joinable线程同时也提供了一个明确的方式在退出是传递数据。在退出之前,joinable线程可以传递一个数据指针或其他的返回值给pthread_exit方法。其他线程可以通过调用pthread_join来或者这个数据。

注意:在程序退出时,detached线程或被立即中止但是joinable线程不会。每个joinable线程被调用join后才能退出。线程在做很重要的事并且不想被中断时推荐使用joinable线程,比如保存数据倒磁盘。

如果确实需要创建joinable线程,唯一的方法是使用POSIX线程。POSIX默认创建joinable类型的线程。如果要指定线程是detached或joinable,在创建线程前使用pthread_attr_setdetachstate方法来修改线程的属性就可以了。在线程开始后,可以通过调用pthread_detach方法来把joinable线程转化为detached线程。

设置线程优先级

任何创建的线程都有一个默认的优先级。内核调度算法会查看优先级来决定运行哪个线程,高优先级的线程会比低优先级的线程优先运行。高优先级并不保证运行时间,只是比低优先级的线程更容易被选中。

注意:最好是让线程保持默认优先级。增加一些线程的优先级的同时也降低了另一些线程的优先级。如果程序中有高优先级和低优先级的线程交互,低优先级的线程可能会导致性能瓶颈。

如果确实要修改这个属性,Cocoa和POSIX都提供了修改的方法。对于Cocoa线程,可以使用NSThread的setTHreadPriotiry:方法来设置当前运行线程的优先级。对于POSIX线程,可以使用pthread_setschedparam方法。

编写线程的入口

对于大部分,线程入口的结构在OS X和其他平台是相同的。初始化数据,设置run loop(可选的),线程结束后清理现场。根据设计的不同,在线程的入口可能有些其他事情要做。

创建Autorelease Pool

对于Objective c框架下的程序,每个线程至少创建一个Autorelease Pool。如果程序使用内存管理机制(也就是程序retain,release对象),autorelease pool会管理线程autoreleased的对象。

如果程序使用垃圾回收机制,创建autorelease pool就不是必须的了。在有垃圾回收机制的程序中创建一个autorelease pool是无害的,大部分时候它被无视了。不过只有程序既使用垃圾回收机制又使用autorelease pool时才这么用。这种情况下autorelease pool必须被创建,使用垃圾回收机制的部分会自动忽视它。

如果程序使用内存管理机制,线程入口的第一件事就是创建autorelease pool。同样的,销毁autorelease pool是最后一件事。它确保自动释放的对象被捕捉到,虽然直到线程结束才释放掉。下面展示了线程中使用autorelease pool的基本方法。

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
 
    // Do thread work here.
 
    [pool release];  // Release the objects in the pool.
}      

由于最上层的autorelease pool直到线程结束时才释放对象,长时间存活的线程需要另外的创建autorelease pool来更快的释放对象。比如,使用run loop的线程会在每个熏昏中创建并销毁autorelease pool。更频繁的释放对象可以防止程序消耗内存太大,内存消耗太大可能会导致性能问题。对于任何对性能有要求的程序,都应该查看代码的性能并且适当的使用autorelease pool。

设置异常处理器

如果程序需要捕捉处理异常,那么线程应当时刻准备着捕捉可能发生的任何异常。虽然最好的方式是在可能发生异常的地方捕捉,但是如果没有捕捉到可能会导致程序退出。在线程的入口设置一个try/catch可以让你捕捉到任何异常并且有机会来处理他们。

使用C++或Objective C方式的异常处理都可以。

设置Run Loop

编写在另一个线程上运行的代码,有两种选择。一种是写一个长时间不间断或小间断运行的代码,然后在运行完后退出。另一种是把线程放到run loop中然后在执行请求触发后动态处理。第一种不需要特使处理,只需要做它需要做的事就可以了。第二种需要设置线程的run loop。

OS X和iOS都对每个线程都提供了内置的run loop支持。APP框架在主线程启动时自动启动了它。如果创建了另外的线程,就需要配置并且启动对应线程的run loop了。

中止线程

建议的退出线程方式是让它正常退出。虽然Cocoa, POSIX都提供了直接杀掉线程的方法,非常不建议这样使用。杀掉一个线程会导致它不能清理现场。线程使用的内存会泄漏,并且其他使用的资源也没有被清理,这会导致潜在的问题。

如果确实需要在线程执行中中止线程,需要设计线程来相应外部的取消或退出消息。对于长时间执行的操作,就意味着需要周期性的查看是否有这样的消息到达。如果有退出的消息到达,这个线程就有机会来执行清理工作然后退出。没有的话它就回去继续工作。

一种相应退出消息的方式是使用run loop的输入源来接受消息。下面的代码展示了入口的程序长什么样。(这个例子只包含了主要的流程,不包括设置autorelease pool以及其他的配置工作)这个例子 假设有一个自定义的run loop输入源可接收其他线程的消息。在做了一些实际的工作之后,这个线程运行run loop来查看是否收到消息。如果没有,run loop立刻结束然后继续接下来的工作。由于处理程序不能直接访问exitNow局部变量,所有退出条件的通讯通过线程的dictionary来做。

- (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];
    }
}      

转载于:https://www.cnblogs.com/fengquanwang/p/3778397.html

继续阅读