进程和线程的区别

进程:在计算机中,进程是一个正在被执行的计算机程序实例。它包含了程序代码和当前的活动。根据操作系统的不同,一个进程可能由多个并行地执行指令的线程组成。

一个计算机程序是一个被动的指令集合,但一个进程则是对这些指令的实际执行。几个进程可能会与一个相同的程序相关;例如,打开一个相同程序的多个实例通常意味着多个进程正在被执行。

多任务是一个可以使多个进程共享CPU和其他系统资源的方法。每个CPU一次只能执行一个任务。但是,多任务使CPU在不用等待每个任务完成的情况下切换正在执行的任务。根据操作系统的实现而不同,切换过程可能会发生在任务进行I/O操作时、任务标记为可切换时,或是被硬件打断时。

多任务的一个普遍形式是分时系统。分时是一个能快速响应可交互程序的方法。在分时系统中,环境切换执行地非常快,这会让它看起来像多个进程被同时执行在一个处理器上。这种像是多个进程同时进行的执行被称为并发。

为了安全性和可靠性,大部分现代操作系统阻止独立的进程之间直接通信,并提供了非常间接和受限的进程间通信功能。

线程: 在计算机科学里,线程是最小的编制好的指令(可以由调度器独立管理)序列。不同的操作系统对线程和进程的实现是不同的,但是大部分情况下,线程是进程的组件。一个进程可以存在多个线程,它们并发执行并且共享内存等资源,但是不同的进程不会共享这些资源。尤其是,进程中的这些线程会在特定时刻共享它们的可执行代码和变量值。

单处理器和多处理器系统: 单处理器系统通常使用通过时间片实现多线程:中央处理器(CPU)在不同的线程之间切换。这个环境切换通常发生地非常频繁、迅速,使用户感觉线程或任务在同时运行。在多处理器或多核系统中,多个线程可以平行执行,每个处理器同时地执行一个单独的线程。在拥有多个硬件线程的处理器内,相互分隔的软件线程可以通过单独的硬件线程并发地执行。

进程、线程对比

线程与传统的多任务操作系统进程主要有这几点不同:

线程通常是独立的,但线程作为进程的子集存在。
由于一个进程中的多个线程共享进程状态(如内存和其他资源),进程比线程承载更多的状态信息。
由于线程共享进程的内存空间,所以每个进程有各自的地址空间。
进程与进程的通信只能通过系统提供的进程间通信机制。
在同一个进程中线程之间的环境(上下文)切换通常比进程间的环境切换迅速。

据说有些系统,像Windows NT和OS/2系统,线程实现对系统消耗小,而进程实现却非常大。一些其他的系统,这种差别不会那么大,除非在某些架构上(尤其是x86)的地址空间切换导致了传输后备缓冲器(TLB)的刷新。

补充:本文没有关于进程的过多介绍,如果想了解Mac/iOS进程间的通信方式,请查看进程间通信 (OSX/iOS)

线程安全、线程同步

线程安全是计算机编程中适用于多线程编码的概念。线程安全的代码在一定程度上理解为仅仅操作这样一部分共享数据结构,这些数据结构能够确保所有的线程正确地实现他们的设计而不会对彼此引起意想不到的影响。有许多可以实现线程安全的数据结构的策略。

一个程序可以在一个共享地址空间中同时执行几个线程,在这个空间中,每个线程几乎可以获取到其他线程的所有内存。线程安全是一个这样的属性:它可以借助同步工具,通过在控制流与程序代码之间重新建立一些通信的方式,来使代码运行在多线程环境中。

线程安全的级别

软件库可以提供一些线程安全保证。例如,并发的读操作可以保证线程安全,但是并发的写就未必了。使用了这些库的程序是否安全一定程度上取决于它使用的这些库是否保证了线程安全。

不同厂商使用了略微不同的线程安全术语:

线程安全: 这种实现保证了避免当多个线程同时访问时产生竞争条件。
有条件的安全: 不同的线程可以同时访问不同的对象,但不能访问共享数据以避免竞争条件。
非线程安全:不可以在不同的线程中同时访问。

线程安全保证通常也包括避免或者限制各种形式的死锁,还包括对最大并发执行的优化。但是,并不能一定保证没有死锁,因为死锁可能是库本身的架构层级中的回调或不稳定产生的。

通过上面得知,线程安全本质上是为了数据同步,若想实现真正的线程安全,就需要借助同步工具,线程同步则是负责协调代码执行和控制流的工具。总的来说就是:线程安全是要解决的问题,而线程同步则是方法,那么线程同步具体是如何定义的呢:

线程同步

线程同步被定义为是一种机制,这种机制能确保一个或多个并发线程不能同时执行一些被称为临界区的特殊程序段。程序对临界区的存取被通过使用同步技术控制。当一个线程开始执行临界区,其他的线程应该等待,直到第一个线程结束访问。如果不使用合适的同步技术,会导致竞争条件,竞争条件则是一种变量的值不可预测而依赖于线程环境切换的时机的状态。

例如,假设有三个任务,名字为 Process 1,Process 2,Process 3。这三个任务都并发执行,他们需要共享一个公共资源(临界区),如图示: 这里应该使用同步来避免对共享资源的访问冲突。因此,当任务Process 1和2都视图访问那个资源时,它在同一时刻应该只能被一个任务赋值。如果它被任务1赋值,其他的任务(Process 2)需要等待,直到Process 1释放了这个资源,如下图:

另一个需要考虑的同步必要条件是特殊的线程的执行的顺序。例如,我们买了票才能登飞机。相似地,我们不能还没经过账户验证就查看邮件。同样的,只有我们提供正确的PIN,ATM才会提供服务。

除了刚才说的互斥问题,同步还要解决以下问题:

deadlock死锁,发生在许多程序等待正在被其他程序持有的共享资源(临界区)时。遇到这种情况,程序只有继续等待而不再继续执行。
starvation饥饿,发生在程序正在等待进入临界区,但其他程序独占临界区,因而当前程序被强制无线等待的时候。
priority inversion优先级倒置,发生在一个高优先级程序正在临界区,但它被一个中等优先级程序打断时。这种优先级的违规会在某种情况下发生,并可能会对运行时系统产生严重的后果。
busy waiting忙等待,发生在当程序经常争取对临界区的访问权限时。这个经常性操作掠夺了对其他程序的处理时间。

下面要说的就是用来实现线程安全的线程同步工具:

线程同步工具

iOS常用的线程同步方法主要有有以下5种

1.原子操作
2.内存屏障 和 Volatile 变量
3.锁
4.条件
5.Perform Selector例程

下面分别介绍。

原子操作

原子操作是处理简单的数据类型的一个简单的实现同步的方式。原子操作的优势是它们不阻塞竞争的线程。对于简单的操作,比如递增一个计数器,原子操作比使用锁具有更高的性能优势。

OS X 和 iOS 包含许多对32位和64位的值执行基本的数学和逻辑运算的操作。 这些操作都使用了原子版本的比较和交换,测试和设置,测试和清理等。查看支持原子操作的列表,参阅/user/include/libkern/OSAtomic.h 头文件或atomic手册页。

使用原子操作

非阻塞同步是作用在某些类型上并避免使用锁带来的性能消耗的一种同步方式。尽管锁是同步两个线程的有效方式,但即使是在无竞争的状态下,获取锁都是相对高消耗的操作。相比之下,许多原子操作花费很少的时间就可以完成工作,并能达到和使用锁相同的效率。

原子操作可以让你对32位或64位值进行简单的数学和逻辑操作。这些操作依赖于特定的硬件指令(和可选的内存屏障)来保证给定的操作在它影响到的内存再次被访问之前执行完成。在多线程情况下,你应该总是使用包含内存屏障的原子操作,来确保内存在线程之间被正确地同步。

下表列出了可用的原子数学和逻辑操作和响应的函数名。这些函数都被声明在/usr/include/libkern/OSAtomic.h头文件,你可以在这个文件找到完整的语法。这些函数的64位版本只能在64位程序里使用。

操作 函数名 描述
Add OSAtomicAdd32
OSAtomicAdd32Barrier
OSAtomicAdd64
OSAtomicAdd64Barrier
Adds two integer values together and stores the result in one of the specified variables.
Increment OSAtomicIncrement32
OSAtomicIncrement32Barrier
OSAtomicIncrement64
OSAtomicIncrement64Barrier
Increments the specified integer value by 1.
Decrement OSAtomicDecrement32
OSAtomicDecrement32Barrier
OSAtomicDecrement64
OSAtomicDecrement64Barrier
Decrements the specified integer value by 1.
Logical OR OSAtomicOr32
OSAtomicOr32Barrier
Performs a logical OR between the specified 32-bit value and a 32-bit mask.
Logical AND OSAtomicAnd32
OSAtomicAnd32Barrier
Performs a logical AND between the specified 32-bit value and a 32-bit mask.
Logical XOR OSAtomicXor32
OSAtomicXor32Barrier
Performs a logical XOR between the specified 32-bit value and a 32-bit mask.
Compare and swap OSAtomicCompareAndSwap32
OSAtomicCompareAndSwap32Barrier
OSAtomicCompareAndSwap64
OSAtomicCompareAndSwap64Barrier
OSAtomicCompareAndSwapPtr
OSAtomicCompareAndSwapPtrBarrier
OSAtomicCompareAndSwapInt
OSAtomicCompareAndSwapIntBarrier
OSAtomicCompareAndSwapLong
OSAtomicCompareAndSwapLongBarrier
Compares a variable against the specified old value. If the two values are equal, this function assigns the specified new value to the variable; otherwise, it does nothing. The comparison and assignment are done as one atomic operation and the function returns a Boolean value indicating whether the swap actually occurred.
Test and set OSAtomicTestAndSet
OSAtomicTestAndSetBarrier
Tests a bit in the specified variable, sets that bit to 1, and returns the value of the old bit as a Boolean value. Bits are tested according to the formula (0x80 » (n & 7)) of byte ((char*)address + (n » 3)) where n is the bit number and address is a pointer to the variable. This formula effectively breaks up the variable into 8-bit sized chunks and orders the bits in each chunk in reverse. For example, to test the lowest-order bit (bit 0) of a 32-bit integer, you would actually specify 7 for the bit number; similarly, to test the highest order bit (bit 32), you would specify 24 for the bit number.
Test and clear OSAtomicTestAndClear
OSAtomicTestAndClearBarrier
Tests a bit in the specified variable, sets that bit to 0, and returns the value of the old bit as a Boolean value. Bits are tested according to the formula (0x80 » (n & 7)) of byte ((char*)address + (n » 3)) where n is the bit number and address is a pointer to the variable. This formula effectively breaks up the variable into 8-bit sized chunks and orders the bits in each chunk in reverse. For example, to test the lowest-order bit (bit 0) of a 32-bit integer, you would actually specify 7 for the bit number; similarly, to test the highest order bit (bit 32), you would specify 24 for the bit number.

大部分原子函数的行为都比较直接了当。但是原子的test-and-set和compare-and-swap操作比较复杂一些。前三行对OSAtomicTestAndSet函数的调用演示了如何对整型值执行位操作,它的结果可能会和你想的略有不同。后面两行代码展示了OSAtomicCompareAndSwap32函数的行为。在任何情况下,这些函数都在没有竞争的情况下(没有其他线程操作这些值)被调用。

int32_t  theValue = 0;
OSAtomicTestAndSet(0, &theValue);
// theValue is now 128.
 
theValue = 0;
OSAtomicTestAndSet(7, &theValue);
// theValue is now 1.
 
theValue = 0;
OSAtomicTestAndSet(15, &theValue)
// theValue is now 256.
 
OSAtomicCompareAndSwap32(256, 512, &theValue);
// theValue is now 512.
 
OSAtomicCompareAndSwap32(256, 1024, &theValue);
// theValue is still 512.

内存屏障 和 Volatile 变量

为了达到最佳性能,编译器经常对汇编级的指令重新排序来保持处理器的指令管道尽可能满。作为优化的一部分,编译器会对访问主存储器的指令重排序,这个行为当编译器任务这样做不会产生错误数据时进行。不幸的是,编译器并不总是能发现依赖内存的操作。如果有分隔的变量互相被影响了,编译器的优化操作会以错误的顺序更新这些变量,这样就产生了潜在的错误。

内存屏障是一种非阻塞的同步工具,他被用来保证内存操作以正确的顺序进行。内存屏障的行为像栅栏,强制处理器完成任何在屏障之前的加载和存储操作,之后它才被允许执行位于屏障后面的加载和存储操作。内存屏障通常用来确保一个线程(但它对其他线程可见)的内存操作总是按照预计的顺序执行。如果在这种情况下没有设置内存屏障,可能会导致其他线程发生意想不到的后果。(例如,参加Wikipedia关于内存屏障的实例。)为了使用一个内存屏障,你可以在你的代码合适的地方调用OSMemoryBarrier函数。

Volatile变量是适用于一些特别变量的另一种类型的内存约束。编译器经常通过加载变量值到寄存器的方式优化代码。对于局部变量,这通常不是问题。但如果变量在另一个线程可见,这种优化会让其他线程无法发现值的变化。对变量上使用volatile关键字,强制编译器每次使用它的时候从内存加载这个变量。如果一个变量在某个时刻不能被编译器没有探测到的外部资源改变它的值,可以将这个变量声明为volatile

因为内存屏障和volatile变量降低了编译器的优化性能,你应该少使用它们,并仅当在需要确保正确性的时候使用它们。关于内存屏障的更多信息,参见OSMemoryBarrier手册页。

锁是最常使用的同步工具。你可以使用锁来保护代码临界区(一个同一时刻只能一个线程访问的代码段)。例如,临界区可能操控着特殊的数据结构或使用一些在同一时刻只支持一个客户的资源。通过放置一个锁包围这些部分的方式,将其他线程排除在外,避免它们影响程序的正确性。

下面列出了一些程序员经常使用的锁。OS X 和 iOS 对其中的大部分类型提供了实现,但并非所有的。对于不支持的类型,描述栏解释了为什么平台没有直接实现这些锁。

Lock 描述
Mutex 互斥锁像环绕在资源周围的保护屏障。mutex是一类在同一时刻只授权一个线程访问的信号。如果mutex正在使用,而另一个线程试图获取它,这个线程会阻塞直到mutex被之前的持有者释放。如果多个线程对同一个mutex竞争,同一时刻只允许一个线程访问它。
Recursive lock 递归锁是互斥锁的一个变种。递归锁允许一个线程能够在释放它之前多次获取。其他线程保持阻塞直到锁的持有者释放了与获取相同的次数。递归锁主要被用在递归一个迭代器的时候,但也可以在多个方法都要获取锁的时候。
Read-write lock 读写锁也被用作是互相排斥的锁。这个类型的锁通常用在大规模操作,如果被保护的数据经常读而只是偶尔修改,它能够在这种情况下显著地提升性能。当线程想要在获取锁后进行写操作时,尽管它被阻塞直到所有的读操作释放这个锁,但是可以更新数据。当写线程正在等待这个锁时,新的读线程被阻塞直到写线程完成。系统仅通过POSIX线程支持读写锁。想获取更多关于如何使用这些锁的信息,参见pthread手册页。
Distributed lock 分部锁在进程级提供了相互排斥的访问权限。不想一个真正的互斥锁,分部所不会阻塞进程或者阻止它的运行。它只是在锁正在使用时进行简单地报告,并让进程决定如何处理。
Spin lock 自旋锁反复地轮询加锁条件直到条件变为真。自旋锁最常用在多核系统中,这种系统中对锁的预计的等待非常小。在这种情况下,通常自旋锁轮询比阻塞线程更高效,它更专注于环境(上下文)的切换和对线程数据的更新。系统没有提供任何关于自旋锁的实现,因为它的轮询机制,但是可以在特定情况下很容易实现他们。想获取更多关于在内核实现自旋锁的信息,参见Kernel Programming Guide
Double-checked lock 双检查锁试图通过在上锁之前测试加锁原则的方式来减少对加锁过程的管理。因为双检查锁有潜在危险,系统没有对它们提供明确的支持也不鼓励使用它们。
使用锁

锁是线程编程的基本同步工具。锁可以让你很容易地保护大块代码来保证代码的正确性。OS X 和 iOS 多所有应用类型提供了基本的互斥锁,同时Foundation框架对特殊场景定义了另外一些互斥锁的变种。下面这个部分向你展示如何使用这些锁。

使用POSIX互斥锁

POSIX 互斥锁在任何应用中都非常容易使用。创建一个互斥锁,你需要声明和初始化一个pthread_mutex_t类型。加锁和解锁,你需要使用pthread_mutex_lock和pthread_mutex_unlock函数。下面展示了初始化和使用POSIX线程互斥锁使用的基本代码。当你使用完这个锁后,简单地调用一下pthread_mutex_destroy来释放锁产生的数据。(注意在实际使用时要检查和处理函数返回的错误)

pthread_mutex_t mutex;
void MyInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
}
 
void MyLockingFunction()
{
    pthread_mutex_lock(&mutex);
    // Do work.
    pthread_mutex_unlock(&mutex);
}
使用NSLock类

NSLock为Cocoa应用实现了基本的互斥锁。这个所有锁(包括NSLock)的接口实际上是通过NSLocking协议定义的,这个协议定义了lockunlock方法。在需要互斥操作时,使用这些方法来获取和释放锁。

除了标准的锁行为,NSLock类还添加了tryLocklockBeforeDate:方法。tryLock方法试图获取锁但是锁不可用时也不会阻塞,而是简单地返回NO。lockBeforeDate:方法也不阻塞线程来试图获取锁,如果在指定的时间内获取不到则返回NO。

下面的例子演示了使用NSLock对象协调对数据展示(显示的数据是通过几个线程计算的来的)的更新。如果线程不能立即获取锁,它会简单地继续他的计算直到他可以获取锁并更新显示。

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
// ...
while (moreToDo) {
    /* Do another increment of calculation */
    /* until there’s no more to do. */
    if ([theLock tryLock]) {
        /* Update display used by all threads. */
        [theLock unlock];
    }
}

使用@synchronized指令

@synchronized指令是方便快捷的在Objective-C中创建互斥锁的方式。@synchronized指令的行为如同其他互斥锁,阻止不同线程获取在同一时间获取同一个锁。但是使用它你可以不用创建mutex或者锁对象。取而代之的是,只需简单地使用任何一个Objective-C对象作为加锁的token,如下所示:

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}

传递给@synchronized指令的对象是一个用来辨别保护区块的唯一标识。如果你在两个不同的线程执行这个过程方法,在每个线程中为anObj参数传递了不同的对象,每个线程都会继续执行而不会被其他线程阻塞。但是如果你传递一个相同的对象,其中一个线程获取锁会阻塞其他线程直到先获取锁的线程执行完它的临界区代码。

作为一个防范措施,@synchronizedblock隐式地添加了一个异常处理来保护代码。当异常抛出时,这个处理自动释放mutex。这意味着为了使用@synchronized指令,你必须也保证Objective-C的异常处理。如果你不想添加对隐式异常处理的额外管理,你应该考虑使用lock类。

想要获取更多关于@synchronized指令的信息,参见The Objective-C Programming Language。

使用其他Cocoa锁

以下部分描述了如何使用其他的Cocoa锁。

使用 NSRecursiveLock 对象

NSRecursiveLock类定义了这样一个锁,可以在同一个线程多次获取而不会导致线程死锁。递归锁保持对成功获取次数的追踪。每次成功获取必须使用相应的unlock调用来平衡。仅当所有的lock调用都对应调用unlock时,递归锁才会释放出来来让其他线程获取它。

顾名思义,这个类型的锁通常被用在递归函数里,来阻止递归过程被线程阻塞。类似的,你可以在非递归的情况下使用它来调用具有需要锁这种语义的函数。这有一个例子,这个例子中的递归函数需要锁来贯穿递归过程。如果你不在这段代码中使用NSRecursiveLock对象,当函数再次调用时线程会死锁。

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
 
void MyRecursiveFunction(int value)
{
    [theLock lock];
    if (value != 0)
    {
        --value;
        MyRecursiveFunction(value);
    }
    [theLock unlock];
}
 
MyRecursiveFunction(5);

注意:因为递归锁直到所有的lock调用被unlock调用平衡之后才会释放,你应该谨慎权衡主动的lock调用与潜在的执行。延长对任何lock的持有时间会导致其他线程阻塞直到递归过程结束。如果你可以重写你的代码来排除递归过程或排除使用递归锁的需要,会有更好的性能。

使用 NSConditionLock 对象

NSConditionLock对象定义了一个可以用一个指定的值来加锁和解锁的互斥锁。你不应该对这种有条件(参见Conditions)的锁感到不解。这种行为有点像实现非常困难的条件。

通常,当线程需要按照指定顺序执行任务的时候,使用NSConditionLock对象,例如当一个线程生产数据另一个线程则消费数据。(条件本身仅仅是一个你定义的的整型值。)当生产者完成工作,这个锁就会解锁并设置锁条件为一个合适的整型值来唤醒消费者线程,消费者接着处理数据。

NSConditionLock对象的加锁和解锁方法可以以任何组合的形式使用。例如,你可以使用unlockWithCondition:方法来配合加锁消息,或者使用lockWhenCondition:消息来配合解锁。当然,后一个组合解锁了条件锁,但是可能不会消除任何线程的等待状态,因为线程等待着指定的条件值。

下面的例子展示了如何使用条件锁解决生产者-消费者问题。假设程序包含着这样一个数据队列。一个生产者线程向队列中添加数据,一个消费者线程从队列中提取数据。生产者不需要等待指定的条件,但是它必须等待锁为可用状态才可以安全地向队列中添加数据。

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
 
while(true)
{
    [condLock lock];
    /* Add data to the queue. */
    [condLock unlockWithCondition:HAS_DATA];
}

因为初始条件设置为NO_DATA,生产者线程很容易在最初获取到锁。它向队列填充数据并设置条件为HAS_DATA。当接下来的迭代中,生产者线程可以在它到达时添加新的数据,不管队列是否为空或是否已经有数据。它仅当在一个消费者线程从队列中提取数据时会被阻塞。

因为消费者线程必须要有数据处理,它使用指定的条件等待队列。当生产者向队列放入数据,消费者线程被唤醒并获得锁。随后它可以提取一些数据并更新队列的状态。下面的例子展示了消费者线程实现的循环的基本的结构。

while (true)
{
    [condLock lockWhenCondition:HAS_DATA];
    /* Remove data from the queue. */
    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
 
    // Process the data locally.
}

使用 NSDistributedLock 对象

NSDistributedLock类可以在多个主机上的多个程序使用来限制一些共享资源(例如,文件)的访问权限。这个锁本身是一个高效的用文件系统实体(如,文件或者目录)实现的互斥锁。为使NSDistributedLock对象可用,这个锁必须是对所有使用它的程序都是可写的。这通常意味着把它放在一个所有程序都有访问权限的文件系统中。

不像其他类型的锁,NSDistributedLock没有遵守NSLocking协议,因此没有lock方法。lock方法会阻塞线程的执行,还需要系统以一定的频率对锁轮询。相比较给你的程序强加的这些坏处,NSDistributedLock提供了tryLock方法让你决定是否轮询。

因为它是使用文件系统实现的,NSDistributedLock对象不能被释放,除非所有者明确地释放了它。如果在持有一个这种锁时程序崩溃了,其他客户不能再访问受保护的资源。对于这种情况,你可以使用breakLock方法来打破存在的锁以使你可以获取它。通常情况下打破锁应该被避免,除非你确定持有锁的程序已经消亡而且没有释放锁。

像使用其他锁那样,当NSDistributedLock对象使用完毕时,通过调用unlock方法释放它。

条件

条件是另一种类型的信号,它在某些条件为真时,让线程互相发送signal信号。条件通常被用来象征资源的可用性或者来确保任务按指定的顺序执行。一个线程只有测试条件为真的时候才不会被阻塞。它保持阻塞直到一些其他线程显式地改变了条件并发送signal。条件和互斥锁的不同之处是多线程可以在同一时刻访问条件。条件比’门卫’做了更多的任务,它使不同的线程依赖指定的规则通过’大门’。

一个可以使用条件的场景是:管理一组正在等待事件。当队列中有事件时,事件队列会使用条件变量发送signal给正处于等待的线程。如果一个事件到达了,队列会适当地发送向条件发送signal。如果一个线程已经处于等待状态,它会被唤醒,于是它会从事件队列出队并执行任务。如果两个事件几乎在同一时刻进入队列,队列会向两个线程发送signal并唤醒两个线程。

系统对条件有几种不同的技术实现。然而正确的实现需要谨慎的编码,因此你需要在编写自己的代码之前看看使用条件的例子。

使用条件

条件是一种特殊类型的互斥锁,使用它来同步程序执行时必须遵守的顺序。他们与互斥锁有略微的不同。一个线程等待条件时保持阻塞,直到条件被其他线程显式地signal。为了避免假信号带来的问题,你应该总是使用谓词连接条件锁。谓词是一个决定线程执行是否安全的更具体的方式。条件仅仅保持你的线程休眠直到在signal线程设置了谓词。

下面的部分展示了符合使用条件。

使用NSCondition类

NSCondition类提供了与POSIX条件相同的语义,但是将必要的锁和条件结构包装为一个对象。有了这个对象,你可以像使用互斥锁那样执行lock,还可以像使用条件那样等待信号。

下面的代码展示了使用NSCondition对象进行等待事件序列。cocoaCondition变量包含了NSCondition对象、timeToDoWork变量是一个在signal发出前在其他线程递增的整型值。

[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];
 
timeToDoWork--;
 
// Do real work here.
 
[cocoaCondition unlock];
//  signal the Cocoa condition and increment the predicate variable.
// You should always lock the condition before signaling it
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
使用 POSIX 条件

POSIX线程条件锁需要使用条件数据和一个互斥锁。尽管这个两个数据是分隔的,但互斥锁是在运行时与条件数据紧密联系的。线程在等待一个signal时,总是将同一个互斥锁和条件数据一起使用。改变这种组合会导致错误。

下面展示了基本的初始化及使用条件和谓词。在初始化条件和互斥锁之后,正处于等待的线程使用ready_to_go变量作为它的谓词进入了wile循环。仅当谓词被设置并且条件随即发出signal时,正在等待的线程被唤醒并开始它的工作。

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 the mutex.
    pthread_mutex_lock(&mutex);
 
    // If the predicate 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 the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}

signal线程要负责对谓词的设置和对signal信号的发送。下面展示了这个行为的实现。这个例子中,条件在互斥锁中被signal来防止处于等待的线程之间产生竞争条件。

void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
 
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
 
    pthread_mutex_unlock(&mutex);
}

Perform Selector例程

Cocoa应用有对线程同步管理的方便的发送消息的方式。NSObject类声明了在应用的活动线程上执行selector的方法。这些方法在保证他们会在目标线程同步执行的前提下,使你的线程异步地发送消息。例如,你可以使用向这样的消息执行perform selector,他们来自于相对于主线程或自定义的其他线程的分布式的计算。每个perform a selector请求会入队到目标线程的runloop中,这些请求随即会按照被接受的顺序执行。

想获取更多关于perform selector例程的总结信息和它们的使用信息,参见Cocoa Perform Selector Sources

// <Foundation/NSRunLoop.h>
/**************** 	Delayed perform	 ******************/

@interface NSObject (NSDelayedPerforming)

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end

@interface NSRunLoop (NSOrderedPerform)

- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;

@end
// <Foundation/NSThread.h>
@interface NSObject (NSThreadPerformAdditions)

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
	// equivalent to the first method with kCFRunLoopCommonModes

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
	// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);

@end

线程通信

尽管好的设计会减少一些必须的通信,但是在某些情况下,线程间的通信是必不可少的。(一个线程的职责是为应用工作,但如果这个工作的结果从不会被使用,这有什么好处呢?)线程可能需要进行新的职责请求或向主线程报告他们的进度。在这种情况下,你需要个从其他线程获取信息的方式。幸好,由于线程共享同一个进程空间,这样便有很多通信的方式。

线程之间有许多通信的方式,每种都有各自的优缺点。配置线程本地存储列表列出了大部分的可以在OS X上公用的通信机制。(除了消息队列和Cocoa分布式对象,这些技术都可以在iOS上使用。)这个表按照复杂度递增的顺序排列。

机制 描述
Direct messaging
直接通信
Cocoa应用支持在线程上直接perform selectors。这个功能意味着任何线程可以直接在另一个线程上执行。因为他们执行在目标队列的环境(上下文)中,这个方式发送的消息自动序列化在线程上。获取更多信息,参见Cocoa Perform Selector Sources.
Global variables, shared memory, and objects
全局变量,共享内存,对象
是另一种在两个线程间通信的简单方式是使用全局变量,共享对象,共享内存区块。尽管共享变量非常快而且简单,但是他们比起直接发送消息更不稳定。共享变量必须使用锁或其他同步机制来保护,以确保程序的正确性。如果做得不够好,可能会导致产生竞争条件,数据破坏或者崩溃。
Conditions 条件 条件是一个同步工具,你可以使用它来控制线程执行一个特殊代码区。你可以把它想象成是一个门卫,仅当状态符合的时候才让线程执行。获取更多如何使用条件的信息,参见
Run loop sources 一个自定义的runloop源是一个使用它来接收程序特定消息的对象。因为他们是事件驱动的,如果没有任务时,runloop源让线程自动休眠,以此来提升线程的效率。更多关于run loop 和 run loop 源的信息 参见 Run Loops.
Ports and sockets
端口和套接字
基于端口的通信是两个线程之间通信的更复杂的方式,它也是非常依赖技术的。更重要的是,端口和sockets可以被用来外部实体通信,如其他程序和服务。为了更高效,端口使用run loop源来实现,因此你的线程会在端口没有等待数据的时候休眠。更多关于run loop和关于基于端口的输入源的信息,参见 Run Loops.
Message queues
消息队列
遗留的多进程服务定义了先进先出(FIFO)的队列,这个队列是对管理进出数据的抽象。尽管消息队列简单又方便,但是他们不像其他通信技术那样高效。更多关于使用消息队列的信息,参见 Multiprocessing Services Programming Guide.
Cocoa distributed objects
分布式对象
分布式对象是Cocoa为基于端口的通信实现的高级技术。尽管使用这个技术在进程间通信是可能的,但是这样做事非常不鼓励的,因为它导致非常大的系统开销。分布式对象更适合于同其他进程通信,进程简单通信消耗本身就很高。获取更多信息,参见 Distributed Objects Programming Topics.

4.死锁、活锁、可重入

死锁一组任务中的成员相互等待对同一个锁的释放。它的产生有以下四个必要条件:

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

参看iOS产生死锁的原因

活锁 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

这里有一个例子来解释活锁和死锁:死锁与活锁

可重入性:Reentrancy一个计算机程序或子程符合这些特性被称为可重入:可在执行过程中被打断,然后可在之前的调用完成之前安全地再次进入。打断可能是由于内部行为像跳转、调用等导致的,可能是外部行为像interrupt或signal。一旦可重入的调用完成了,之前的调用会恢复正常执行。

可重入形容的主体是一段程序,因此它可能是一个进程、一个函数、或者一段代码。

可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。

可重入函数有这样几个特点: 不持有静态(全局)数据。 不返回指向静态数据的指针;所有数据都由函数的调用者提供。 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。 如果必须访问全局变量,利用互斥信号量来保护全局变量。 绝不调用任何不可重入函数。