• iOS开发:多线程技术概述


    一、概述

    线程(thread:用于指代独立执行的代码段。

    进程(process:用于指代一个正在运行的可执行程序,它可以包含多个线程。

    任务(task:用于指代抽象的概念,表示需要执行工作。

     

    多线程的替代方法:

    Operation objects(操作对象):操作对象可能创建线程更快,因为它们使用内核里面常驻线程池里面的线程来节省创建的时间,而不是每次都创建新的线程。

    Grand Central DispatchGCD:如果你更关注你任务的完成而不是线程的管理,那么GCD是很好的选择,它将你的任务添加到工作队列中,工作队列会综合考虑当前的内核和负载来执行你的任务,将比你自己操作线程更加高效。

    Idle-timenotifications(空闲时通知):空闲时间通知非常适合在应用程序不是很忙碌的时候对于比较短,而且非常低优先级的任务场景。 Cocoa提供了支持空闲时间通知,使用NSNotificationQueue对象的NSPostWhenIdle选项,在run loop变为空闲的时候发送通知。

    Asynchronous functions(异步函数):系统接口包括多个异步功能,提供了自动的并发。这些API可以使用系统守护进程和进程,或创建自定义的线程来执行他们的任务,并把结果返回给你。(实际的实现是不相关的,因为它是从你的代码中分离出来。)当应用程序需提供异步功能的时候,请考虑使用他们代替等效的自定义线程。

    Timers(定时器):在应用程序的主线程中执行的任务是过于琐碎,且需要定期维修的时候,可以考虑使用定时器。

    Separate processes(多进程):虽然进程比线程更重量级,但有时创建一个单独的进程仍可能是必要的。比如一个任务需要一个显着的内存量,或必须使用root权限执行的时候。例如,你可以使用一个64位的服务器进程来计算大型数据集,而你的32位应用程序给用户显示的结果。

     

    线程启动之后,线程就进入三个状态中的任何一个:

    运行(running)

    就绪(ready)

    阻塞(blocked)

     

    线程栈默认大小:

    512 KB(secondary threads)

    8 MB (Mac OS X main thread)

    1 MB (iOS main thread)

     

    死锁(Deadlocks)和活锁(Livelocks)

    任何时候线程试图同时获得多于一个锁,都有可能引发潜在的死锁。当两个不同的线程分别保持一个锁(而该锁是另外一个线程需要的)又试图获得另外线程保持的锁时就会发生死锁。结果是每个线程都会进入持久性阻塞状态,因为它永远不可能获得另外那个锁。

    一个活锁和死锁类似,当两个线程竞争同一个资源的时候就可能发生活锁。在发生活锁的情况里,一个线程放弃它的第一个锁并试图获得第二个锁。一旦它获得第二个锁,它返回并试图再次获得第一个锁。线程就会被锁起来,因为它花费所有的时间来释放一个锁,并试图获取其他锁,而不做实际的工作。

     

    二、创建线程

    1NSThread

    1)使用detachNewThreadSelector:toTarget:withObject:类方法来生成一个新的线程。

    2)创建一个新的NSThread 对象,并调用它的start方法。(仅在 iOS Mac OSX v10.5 及其之后才支持)

    这两种创建线程的技术都在你的应用程序里面新建了一个脱离的线程。一个脱离的线程意味着当线程退出的时候线程的资源由系统自动回收。这也同样意味着之后不需要在其他线程里面显式的连接(join)

     

    2、使用POSIX的多线程

    使用pthread_系列API,下面是一个可能的实现示例:

    #include <assert.h>

    #include <pthread.h>

    void* PosixThreadMainRoutine(void* data)

    {

        // Do some work here.

        returnNULL;

    }

    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.

        }

    }

     

    3、使用NSObject来生成一个线程

    NSObject对象的performSelectorInBackground:withObject:方法。

     

    三、配置线程本地存储

    每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。比如,你可以使用它来存储在你的整个线程过程中Run loop 里面多次迭代的状态信息。

    Cocoa里面,你使用NSThreadthreadDictionary 方法来检索一个NSMutableDictionary 对象,你可以在它里面添加任何线程需要的键。在POSIX里面,你使用pthread_setspecificpthread_getspecific 函数来设置和访问你线程的键和值。

     

    四、设置线程的脱离状态

    大部分上层的线程技术都默认创建了脱离线程(Datachedthread)。大部分情况下,脱离线程更受欢迎,因为它们允许系统在线程完成的时候立即释放它的数据结构。脱离线程不需要显式的和你的应用程序交互。相比之下,系统不回收可连接线程(Joinablethread)的资源直到另一个线程明确加入该线程,这个过程可能会阻止线程执行加入。

     

    五、创建一个自动释放池(AutoreleasePool)

    Objective-C框架链接的应用程序,通常在它们的每一个线程必须创建至少一个自动释放池。如果应用程序使用管理模型,即应用程序处理的retainrelease 对象,那么自动释放池捕获任何从该线程autorelease的对象。

    如果应用程序使用的垃圾回收机制,而不是管理的内存模型,那么创建一个自动释放池不是绝对必要的。在垃圾回收的应用程序里面,一个自动释放池是无害的,而且大部分情况是被忽略。

     

    六、线程同步

    1、原子操作(AtomicOperations

    OSAtomic系列函数,如OSAtomicAdd32,OSAtomicAdd32Barrier等。

    int32_t theValue = 0;

    OSAtomicTestAndSet(0, &theValue); // (0x80>>(n&7))

    // theValue is now 128.

     

    2、内存屏障和Volatile变量(MemoryBarriers and Volatile Variables

    1)内存屏障:

    为了达到最佳性能,编译器通常会对汇编基本的指令进行重新排序来尽可能保持处理器的指令流水线。如果看似独立的变量实际上 是相互影响,那么编译器优化有可能把这些变量更新位错误的顺序,导致潜在不不正确结果。

    内存屏障(memorybarrier)是一个使用来确保内存操作按照正确的顺序工作的非阻塞的同步工具。内存屏障的作用就像一个栅栏,迫使处理器来完成位于障碍前面的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。内存屏障同样使用来确保一个线程(但对另外一个线程可见)的内存操作总是按照预定的顺序完成。

    为了使用一个内存屏障,你只要在你代码里面需要的地方简单的调用OSMemoryBarrier函数。

     

    2Volatile变量

    编译器优化代码通过加载这些变量的值进入寄存器。对于本地变量,这通常不会有什么问题。但是如果一个变量对另外一个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化。在变量之前加上关键字volatile可以强制编译器每次使用变量的时候都从内存里面 加载。如果一个变量的值随时可能给编译器无法检测的外部源更改,那么你可以把该变量声明为volatile变量。

    因为内存屏障和volatile变量降低了编译器可执行的优化,因此你应该谨慎使用它们,只在有需要的地方时候,以确保正确性。

     

    3、锁(Locks

    1Mutex [互斥锁]

    1>使用POSIX互斥锁

    pthread_mutex_t mutex;

    void MyInitFunction()

    {

        pthread_mutex_init(&mutex,NULL);

    }

     

    void MyLockingFunction()

    {

        pthread_mutex_lock(&mutex);

        // Dowork.

        pthread_mutex_unlock(&mutex);

    }

    2>使用NSLock

     

    BOOL moreToDo =YES;

    NSLock *theLock = [[NSLockalloc]init];

    while (moreToDo) {

        /* Doanother increment of calculation */

        /* untilthere’s no more to do. */

       if ([theLock tryLock]) {

            /* Update display used by all threads. */

            [theLock unlock];

        }

    }

    3>使用@synchronized指令

    @synchronized指令是在 Objective-C 代码中创建一个互斥锁非常方便的方法。@synchronized指令做和其他互斥锁一样的工作(它防止不同的线程在同一时间获取同一个锁)。然而在这种情况下,你不需要直接创建一个互斥锁或锁对象。相反,你只需要简单的使用Objective-C对象作为锁的令牌,如下面例子所示:

    - (void)myMethod:(id)anObj

    {

        @synchronized(anObj)

        {

            // Everything between the braces is protected by the @synchronizeddirective.

        }

    }

     

    使用其他Cocoa

    2Recursive lock [递归锁]

    1>使用 NSRecursiveLock对象

    NSRecursiveLock类定义的锁可以在同一线程多次获得,而不会造成死锁。一个递归锁会跟踪它被多少次成功获得了。每次成功的获得该锁都必须平衡调用锁住和解 锁的操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

    NSRecursiveLock *theLock = [[NSRecursiveLockalloc]init];

     

    void MyRecursiveFunction(int value)

    {

        [theLocklock];

       if (value != 0)

        {

            --value;

            MyRecursiveFunction(value);

        }

        [theLockunlock];

    }

     

    MyRecursiveFunction(5);

     

    2>使用 NSConditionLock对象

    NSConditionLock对象定义了一个互斥锁,可以使用特定值来锁住和解锁。通常,当多线程需要以特定的顺序来执行任务的时候,你可以使用一个NSConditionLock对象,比如当一个线程生产数据,而另外一个线程消费数据。

    下面的例子显示了生产者-消费者问题如何使用条件锁来处理。想象一个应用程序包含一个数据的队列。一个生产者线程把数据添加到队列,而消费者线程从队列中取出数据。生产者不需要等待特定的条件,但是它必须等待锁可用以便它可以安全的把数据添加到队列。

    id condLock = [[NSConditionLock alloc]initWithCondition:NO_DATA];

    while(true)

    {

        [condLock lock];

        /* Adddata to the queue. */

        [condLockunlockWithCondition:HAS_DATA];

    }

     

    因为消费者线程必须要有数据来处理,它会使用一个特定的条件来等待队列。当生产者把数据放入队列时,消费者线程被唤醒并获取它的锁。它可以从队列中取出数据,并更新队列的状态。下列代码显示了消费者线程处理循环的基本结构。

     

     

     

    3Read-write lock [读写锁]

    4Distributed lock [分布锁]

    使用 NSDistributedLock对象

     

    5Spin lock [自旋锁]

    6Double-checked lock [双重检查锁]

     

    4、条件(Conditions

    条件是信号量的另外一个形式,它允许在条件为真的时候线程间互相发送信号。

    当线程测试一个条件时,它会被阻塞直到条件为真。它会一直阻塞直到其他线程显式的修改信号量的状态。

    条件和互斥锁(mutexlock)的区别在于多个线程被允许同时访问一个条件。

    条件是一个特殊类型的锁,你可以使用它来同步操作必须处理的顺序。它们和互斥锁有微妙的不同。一个线程等待条件会一直处于阻塞状态直到条件获得其他线程显式发出的信号。

    由于微妙之处包含在操作系统实现上,条件锁被允许返回伪成功,即使实际上它们并没有被你的代码告知。为了避免这些伪信号操作的问题,你应该总是在你的条件锁里面使用一个断言。该断言是一个更好的方法来确定是否安全让你的线程处理。条 件简单的让你的线程保持休眠直到断言被发送信号的线程设置了。

     

    1>使用NSCondition

    [cocoaCondition lock];

    while (timeToDoWork <= 0)

    [cocoaConditionwait];

    timeToDoWork--;

    // Do real work here.

    [cocoaCondition unlock];

     

    用于给Cocoa条件发送信号的代码,并递增他断言变量。你应该在给它发送信号前锁住条件。

    [cocoaCondition lock];

    timeToDoWork++;

    [cocoaCondition signal];

    [cocoaCondition unlock];

     

    2>使用 POSIX条件

    POSIX线程条件锁要求同时使用条件数据结构和一个互斥锁。多线程等待某一信号应该 总是一起使用相同的互斥锁和条件结构。

    下面显示了基本初始化过程,条件和断言的使用。在初始化之后,条件和互斥锁,使用ready_to_go变量作为断言等待线程进入一个while循环。仅当断言被设 置并且随后的条件信号等待线程被唤醒和开始工作。

    pthread_mutex_t mutex;

     

    pthread_cond_t condition;

    Boolean    ready_to_go =true;

    void MyCondInitFunction()

    {

        pthread_mutex_init(&mutex);

        pthread_cond_init(&condition,NULL);

    }

    void MyWaitOnConditionFunction()

    {

        // Lock themutex.

        pthread_mutex_lock(&mutex);

        // If thepredicate is already set, then the while loop is bypassed;

        // otherwise,the thread sleeps until the predicate is set.

        while(ready_to_go ==false)

        {

            pthread_cond_wait(&condition, &mutex);

        }

        // Do work.(The mutex should stay locked.)

        // Reset thepredicate and release the mutex.

        ready_to_go =false;

        pthread_mutex_unlock(&mutex);

    }

     

    信号线程负责设置断言和发送信号给条件锁。下面显示了实现该行为的代码。在该例子中,条件被互斥锁内被发送信号来防止等待条件的线程间发生竞争条件

    void SignalThreadUsingCondition()

    {

        // At thispoint, there should be work for the other thread to do.

        pthread_mutex_lock(&mutex);

        ready_to_go =true;

        // Signal theother thread to begin work.

        pthread_cond_signal(&condition);

        pthread_mutex_unlock(&mutex);

    }

     

    5、执行Selector例程(PerformSelector Routines

    NSObject类声明方法来在应用的一个活动线程上面执行selector 的方法。这些方法允许你的线程以异步的方式来传递消息。

    每个执行selector的请求都会被放入一个目标线程的run loop 的队列里面,然后请求会按照它们到达的顺序被目标线程有序的处理。

  • 相关阅读:
    简析时序数据库 InfluxDB
    tensorflow_1.x(四):线性回归问题初步(准备数据、构建模型、训练模型、进行预测)
    (二) 差分隐私直观理解
    (一) 差分隐私
    (四)PyTorch 的 torch.backends.cudnn.benchmark
    (三)PyTorch 的 Autograd
    (二)PyTorch 中的 tensor 及使用
    (一)PyTorch 中的 ModuleList 和 Sequential
    文本分类(六):不平衡文本分类,Focal Loss理论及PyTorch实现
    tensorflow_1.x(三):Tensorflow2入门(基础、张量、常量与变量、变量的赋值、计算模型、图执行模式、兼容1.0、会话、变量、占位符、feed提交数据)
  • 原文地址:https://www.cnblogs.com/psp429504257/p/5342532.html
Copyright © 2020-2023  润新知