Threading Programming Guide 4 Synchronization

部分内容来源于:ios多线程同步

Synchronization Tools

Atomic Operations

在多进程(线程)访问资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源。原子操作(atomic operation)是不需要synchronized。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。通常所说的原子操作包括对非long和double型的primitive进行赋值,以及返回这两者之外的primitive。

需要硬件实现,但是维基说可以用其他锁机制软件实现。

Memory Barriers and Volatile Variables

介绍直接看wiki比较详细:

大多数现代计算机为了提高性能而采取乱序执行(ARM A8系列之后开始支持部分乱序执行,A15全面支持),这使得内存屏障成为必须。

内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障

GCD和POSIX API均有提供相应方法。

为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值。当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。这个时候就需要设置volatile属性,直接存取原始内存地址。

两者都会影响编译器优化,非必要时不要乱用。

详见 OSMemoryBarrier man page。

Locks

锁是最常用的线程同步工具了,类型如下:

Lock Description
Mutex 互斥锁是比较常用的一种锁,当一个线程试图获取被另一个线程占用的锁时,它将会被挂起,让出 CPU,直到该锁被释放。在iOS中可以使用POSIX api、@synchronized、NSLock实现。
Recursive lock 递归锁是互斥锁的变体,它允许一个线程在释放它之前多次获取它,并且只有在释放相同次数之后其它线程才能获取它。
Read-write lock 读写锁把访问对象划分为读者和写者,当读写锁在读加锁状态时,所有的试图以读加锁方式对其进行加锁时,都会获得访问权限。 所有的试图以写加锁方式对其加锁的线程都将阻塞,直到所有的读锁释放。 当在写加锁状态时,所有试图对其加锁的线程都将阻塞。
Distributed lock 严格来说,分布锁是进程间同步的工具,有点像 Unix 下的各种 lock 文件,比如 apt-get 的 “/var/lib/apt/lists/lock”。它并不强制进程休眠,只是起到告知的作用。具体如何处理资源被占,完全由进程自己决定。iOS 上几本用不上分布锁,在 OS X 中,可以用 NSDistributedLock 实现。
Spin lock 自旋锁与互斥锁不同的地方在于,自旋锁是非阻塞的,当一个线程无法获取自旋锁时,会自旋,直到该锁被释放,等待的过程中线程并不会挂起。它的优点是效率高,不用进行线程切换。缺点是如果一个线程霸占锁的时间过长,自旋会消耗 CPU 资源。
Double-checked lock 双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示"(Lock hint)) 是一种软件设计模式用来减少并发系统中竞争和同步的开销。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)1。该模式在iOS硬件平台的实现是不安全的,系统不提供完整支持也不提倡使用。

大部分类型的Lock都是用了Memory Barrier / 内存屏障来保证进入关键区前完成加载/保存操作。

详见下方的 Using Locks

Conditions

Wiki:Condition_variables

Condition Variable比起lock不同在于condition是倾向于用来解决线程之间的执行进度依赖问题。当一个lock被unlock的时候,另“一个”线程才可以访问。当condition被满足之后,“所有”test这个condition的线程都将被放行。

可以使用POSIX、NSCondition、NSConditionLock实现。

详见下方的 UsingConditions

Perform Selector Routines

这个方法由NSObject实现,可以方便的把object的一个方法交给指定线程运行。可以用在多线程计算后同步结果到汇总结果的线程。线程需要有run loop。详见之前章节的 Cocoa Perform Selector Sources

Synchronization Costs and Performance

Synchronization确保你的代码正确地运行的同时,即使在没有同步问题的情况下它也消耗着宝贵的系统资源。Lock和Atomic Operation通常需要使用Memory Barrier和kernel级别synchronzation。

详见之前章节的 Thread Costs

Thread Safety and Signals

Signal是低层级BSD的机制。前面就好像说了很多了……

这个看不懂:

The problem with signals is not what they do, but their behavior when your application has multiple threads. In a single-threaded application, all signal handlers run on the main thread. In a multithreaded application, signals that are not tied to a specific hardware error (such as an illegal instruction) are delivered to whichever thread happens to be running at the time. If multiple threads are running simultaneously, the signal is delivered to whichever one the system happens to pick. In other words, signals can be delivered to any thread of your application.

Tips for Thread-Safe Designs

Finding the right balance between safety and performance is an art that takes experience.

Avoid Synchronization Altogether

对于新的项目,官方建议尽量避免使用Synchronization tools,对性能影响大。尽量让线程可以独立运行使用私有数据。

Understand the Limits of Synchronization

不要lock错,也不要忘了unlock……

Be Aware of Threats to Code Correctness

当你使用locks或者memory barriers的时候,要特别注意它们在你代码中的位置。

看这个例子:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;

[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];

[anObject doSomething];

因为myArray是mutable的,所有在调用前后加了锁。而anObject是immutable的,所以不需要加锁。但是当你unlock了之后,其他线程在你使用anOjbect的doSomething方法之前清空了myArray,anOjbect也会为null。

所以需要把doSomething也写进关键区去。

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;


[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];

但是这样的话doSomething方法运行消耗的时间也会算进这次lock里面而增加了其他线程的等待时间。仔细分析问题就会发现,问题在于内存管理,于是再改:

NSLock* arrayLock = GetArrayLock();
  NSMutableArray* myArray = GetSharedArray();
  id anObject;
  [arrayLock lock];
  anObject = [myArray objectAtIndex:0];
  [anObject retain];
  [arrayLock unlock];

  [anObject doSomething];
  [anObject release];

这样我们既防止了anObject又不需要把doSomething的时间算进lock里面去了。

关于线程安全,见后面的 Thread Safety Summary

Watch Out for Deadlocks2 and Livelocks3

老师说过很多次了,看看wiki就可以。

Use Volatile Variables Correctly

volatile关键字会强行让系统每次后从内存读取数据,无法缓存到cpu内部register,会影响性能。一般情况下mutex会帮你做好不需要用。

Using Atomic Operations

原子操作依赖于硬件实现,可以用来实现简单的数学和逻辑运算。

Operation Function name Description
Add OSAtomicAdd32
OSAtomicAdd32Barrier
OSAtomicAdd64
OSAtomicAdd64Barrier
相加,把结果保存在制定变量里。
Increment OSAtomicIncrement32
OSAtomicIncrement32Barrier
OSAtomicIncrement64
OSAtomicIncrement64Barrier
+1
Decrement OSAtomicDecrement32
OSAtomicDecrement32Barrier
OSAtomicDecrement64
OSAtomicDecrement64Barrier
-1
Logical OR OSAtomicOr32
OSAtomicOr32Barrier
逻辑或,仅32bit
Logical AND OSAtomicAnd32
OSAtomicAnd32Barrier
逻辑与,仅32bit
Logical XOR OSAtomic32
OSAtomicXor32Barrier
逻辑异或,仅32bit
Compare and swap OSAtomicCompareAndSwap32
OSAtomicCompareAndSwap32Barrier
OSAtomicCompareAndSwap64
OSAtomicCompareAndSwap64Barrier
OSAtomicCompareAndSwapPtr
OSAtomicCompareAndSwapPtrBarrier
OSAtomicCompareAndSwapInt
OSAtomicCompareAndSwapIntBarrier
OSAtomicCompareAndSwapLong
OSAtomicCompareAndSwapLongBarrier
变量与一个数值对比,如果一样就把该值赋值给变量并返回true,否则返回false。
Test and set OSAtomicTestAndSet
OSAtomicTestAndSetBarrier
Test指定变量中的一个bit,把它设置为‘1’并返回旧值。
test公式:(0x80>>(n&7)),bit地址:((char*)address + (n >> 3)),其中n是第几个bit,address是变量地址。
特别说明,test目标值会被以8bit的形式分解然后倒序。
如果你要指定32bit integer最低位,n应该是7。
如果要指定最低位,n应该是24.
Test and clear OSAtomicTestAndClear
OSAtomicTestAndClearBarrier
和上面的一样,只是设置为‘0’。

看栗子之前,一些具体函数及其参数说明4

  • OSAtomicCompareAndSwap**[Barrier](type __oldValue, type __newValue, volatile type *__theValue):这组函数用于比较__oldValue是否与__theValue指针指向的内存位置的值匹配,如果匹配,则将__newValue的值存储到__theValue指向的内存位置。可以根据需要使用barrier版本。

  • OSAtomicTestAndClear/OSAtomicTestAndClearBarrier( uint32_t __n, volatile void *__theAddress ):这组函数用于测试__theAddress指向的值中由__n指定的bit位,如果该位未被清除,则清除它。需要注意的是最低bit位应该是1,而不是0。对于一个64-bit的值来说,如果要清除最高位的值,则__n应该是64。

  • OSAtomicTestAndSet/OSAtomicTestAndSetBarrier(uint32_t __n, volatile void *__theAddress):与OSAtomicTestAndClear相反,这组函数测试值后,如果指定位没有设置,则设置它。

下面栗子有助于让你更加直观地理解。此时没有其他线程可以操作这些值。:

  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.

详见atomic man page和 /usr/include/libkern/OSAtomic.h 头文件。

Using Locks

锁是线程编程同步工具的基础。锁可以让你很容易保护代码中一大块区域以便你可以确保代码的正确性。Mac OS X和iOS都位所有类型的应用程序提供了互斥锁,而Foundation框架定义一些特殊情况下互斥锁的额外变种。以下个部分显式了如何使用这些锁的类型。

Using a POSIX Mutex Lock

  • 使用 pthread_mutex_t create lock

  • 使用 pthread_mutex_lock lock

  • 使用 pthread_mutes_unlock unlock

  • 使用 pthread_mutex_destroy free lock

pthread_mutex_t mutex;

  void MyInitFunction()
  {
      pthread_mutex_init(&mutex, NULL);
  }


  void MyLockingFunction()
  {
      pthread_mutex_lock(&mutex);
      // Do work.
      pthread_mutex_unlock(&mutex);
  }

注意:上面的代码只是简单的使用了一个POSIX线程互斥锁。你的代码应该检查这些函数返回的错误并适当的处理。

Using the NSLock Class

NSLock是Cocoa框架下的mutex,所有接口都定义在NSLocking协议里面(包括lock、unlock、tryLock和lockBeforeDate方法)。

  • tryLock会直接尝试去lock,失败的话不会block线程,而是返回NO。

  • lockBeforeDate和上面一样,加上时间限制(在指定时间点之前能锁就锁,不能就返回NO,不会block线程)。

下面栗子使用NSLock来update用户界面:

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

Using the @synchronized Directive

@synchronized又进一步简化了mutex操作:

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

如果上面这个方法同时在两个线程里面运行,并传入不同object的话是各锁各的,不会block两个线程。传入同一个object的话就会锁其中一个了。

@synchronized方法还隐式地包含了一个exception handler,在有exception被抛出的时候自动释放mutex(先enable Objective-C exception handling)。如果你想要有自己定义的exception handler,还是用NSLock吧。

详见 The Objective-C Programming Language

Using Other Cocoa Locks

Using an NSRecursiveLock Object

可以被同一个线程多次lock:

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

lock了多少次最后就要unlock多少次。

Using an NSConditionLock Object

可以使用特定值lock和unlock,可以用来规划task的执行顺序(实现生产者消费者模型等)。

这里的condition仅仅是一个你自己定义的interger。

实现生产者消费者模型:

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

Using an NSDistributedLock Object

严格来说,分布锁是进程间同步的工具,有点像 Unix 下的各种 lock 文件,比如 apt-get 的 “/var/lib/apt/lists/lock”。

它并不强制进程休眠,只是起到告知的作用。具体如何处理资源被占,完全由进程自己决定。

iOS 上几本用不上分布锁。在 OS X 中,可以用 NSDistributedLock 实现,或者可以直接通过写 lock 文件的方式来实现。

Using Conditions

关于spurious success和使用predicate

Due to the subtleties involved in implementing operating systems, condition locks are permitted to return with spurious success even if they were not actually signaled by your code. To avoid problems caused by these spurious signals, you should always use a predicate in conjunction with your condition lock. The predicate is a more concrete way of determining whether it is safe for your thread to proceed. The condition simply keeps your thread asleep until the predicate can be set by the signaling thread.

Using the NSCondition Class

NSCondtion不仅仅封装了POSIX conditions,还负责了必要的lock和创建condition数据结构的步骤。

栗子,其中timeToDoWork是predicate variable:

[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];

timeToDoWork--;

// Do real work here.

[cocoaCondition unlock];

signal来源线程代码,修改predicate variable要加锁:

[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];

Using POSIX Conditions

POSIX Condition Locks需要用到condition date structure和mutex,它们两在runtime里是捆绑在一起使用的。

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);
}

设置predicate和signal condition:


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); }

标签:ios, object-c