多线程安全问题及各种锁

175 阅读7分钟

1.多线程的安全

多线程使用不当会出现资源竞争,比如多个线程同时对一块资源进行修改,就会很容易引发数据错乱和数据安全问题。

示例:

以购票系统为例,

@property (nonatomic, assign)   int tickets;

- (void)initData {
    
    self.tickets = 10;
}

- (void)sample {
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (self.tickets) {
            [self sellTickets];
        }
        NSLog(@"窗口A, 票卖完了");
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (self.tickets) {
            [self sellTickets];
        }
        NSLog(@"窗口B, 票卖完了");
    });
}


- (void)sellTickets {
            
    if (self.tickets) {
        sleep(0.2);         // 出票中
        self.tickets--;
        NSLog(@"出票一张, 余票:%d", self.tickets);
    }
}

// ----------------------------------------------------------------------------------
log:
出票一张, 余票:9
出票一张, 余票:9
出票一张, 余票:8
出票一张, 余票:8
出票一张, 余票:7
出票一张, 余票:7
出票一张, 余票:6
出票一张, 余票:5
出票一张, 余票:4
出票一张, 余票:3
出票一张, 余票:2
出票一张, 余票:1
出票一张, 余票:0
窗口A, 票卖完了
窗口B, 票卖完了

这里没有在窗口A或者B中打印余票, 是因为某个窗口将要打印时, 有可能被另一窗口卖掉一张从而影响余票. 正确做法是在sellTickets里实时监听票数然后通知窗口A和B来刷新显示. 这种情况归根还是因多个线程同时访问同一内存地址导致的不可预计结果.为了解决这类问题, 我们需要引入线程安全机制.

2.线程同步技术

对于多线程出现的这种问题,我们的解决办法就是使用线程同步技术,而常见的就是加锁。

2.1 OSSpinLock 自旋锁

自旋锁等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。 并且现在已经不安全,可能出现优先级反转的问题。 如果等待锁的优先级较高,它会一直占用着CPU的资源,优先级低的线程就无法释放锁。 在iOS10被苹果废弃。由于已经不再安全, 这里不讨论用法了.

2.2 os_unfair_lock

导入头文件#import <os/lock.h>

@property (assign, nonatomic) os_unfair_lock lock;

- (instancetype)init {
  	if (self = [super init]) {
  		 // 初始化
  		 self.lock = OS_UNFAIR_LOCK_INIT;
  	}
  	return self;
}

- (void)saleticket {
		
  	// 加锁
  	os_unfair_lock_lock(&lock);
  	int oldTicketsCount = self.ticketsCount;
  	sleep(.2);
  	oldTicketsCount--;
  	self.ticketsCount = oldTicketsCount;
  	NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
  	// 解锁
  	os_unfair_lock_unlock(&lock);
  }

调用ticketTest方法 打印结果:

还剩14张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩13张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩12张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩11张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩10张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩9张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩8张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩7张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩6张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩5张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩4张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩3张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩2张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩1张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩0张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}

os_unfair_lock用于取代不安全的OSSpinLock。 等待os_unfair_lock锁的线程处于休眠状态,不是忙等状态。 在iOS10开始使用。

2.3 pthread_mutex 互斥锁

普通用法

导入头文件#import <pthread.h>

@property (assign, nonatomic) pthread_mutex_t mutex;

- (void)__initMutex:(pthread_mutex_t *)mutex {
//    // 初始化属性
//    pthread_mutexattr_t attr;
//    pthread_mutexattr_init(&attr);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
//    // 初始化锁
//    pthread_mutex_init(mutex, &attr);
//    // 销毁属性
//    pthread_mutexattr_destroy(&attr);
  
  	// 初始化锁 NULL等同于PTHREAD_MUTEX_DEFAULT
  	pthread_mutex_init(mutex, NULL);
}

- (instancetype)init {
  	if (self = [super init]) {
  		 // 初始化
  		 [self __initMutex:&_mutex];
  	}
  	return self;
}

- (void)saleticket {
		
  	// 加锁
  	pthread_mutex_lock(&_mutex);
  	int oldTicketsCount = self.ticketsCount;
  	sleep(.2);
  	oldTicketsCount--;
  	self.ticketsCount = oldTicketsCount;
  	NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
  	// 解锁
  	pthread_mutex_unlock(&_mutex);
}

调用ticketTest方法 打印结果:

还剩14张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩13张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩12张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩11张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩10张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩9张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩8张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩7张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩6张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩5张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩4张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩3张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩2张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩1张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩0张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}

mutex也叫“互斥锁”,等待锁的线程处于休眠状态,不是忙等状态。

/*
 * Mutex type attributes
 */
 #define PTHREAD_MUTEX_NORMAL		   0  // 正常类型
 #define PTHREAD_MUTEX_ERRORCHECK	   1  // 错误检查
 #define PTHREAD_MUTEX_RECURSIVE	   2  // 递归类型
 #define PTHREAD_MUTEX_DEFAULT	PTHREAD_MUTEX_NORMAL

上面使用的是Normal类型同OSSpinLockos_unfair_lock作用一样

递归用法

递归锁在被同一线程重复获取时不会产生死锁 递归类型PTHREAD_MUTEX_RECURSIVE

- (void)__initMutex:(pthread_mutex_t *)mutex
{
	// 递归锁:允许同一个线程对一把锁进行重复加锁

	// 初始化属性
	pthread_mutexattr_t attr;
	pthread_mutexattr_init(&attr);
	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
	// 初始化锁
	pthread_mutex_init(mutex, &attr);
	// 销毁属性
	pthread_mutexattr_destroy(&attr);
}

- (instancetype)init
{
	if (self = [super init]) {
    	[self __initMutex:&_mutex];
	}
	return self;
}

/**
线程1:otherTest(+-)
    	otherTest(+-)
     	 otherTest(+-)

线程2:otherTest(等待)
*/

- (void)otherTest
{
	pthread_mutex_lock(&_mutex);

	NSLog(@"%s", __func__);

	static int count = 0;
	if (count < 10) {
    	count++;
    	[self otherTest];
	}

	pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
	pthread_mutex_destroy(&_mutex);
}

条件用法

@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;

- (instancetype)init
{
	if (self = [super init]) {
    	// 初始化属性
    	pthread_mutexattr_t attr;
    	pthread_mutexattr_init(&attr);
    	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    	// 初始化锁
    	pthread_mutex_init(&_mutex, &attr);
    	// 销毁属性
    	pthread_mutexattr_destroy(&attr);
    
    	// 初始化条件
    	pthread_cond_init(&_cond, NULL);
    
    	self.data = [NSMutableArray array];
	}
	return self;
}

- (void)otherTest
{
	[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];

	[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 线程1
// 删除数组中的元素
- (void)__remove
{
	pthread_mutex_lock(&_mutex);
	NSLog(@"__remove - begin");

	if (self.data.count == 0) {
    	// 等待 先解锁进入等待状态
    	// 接收到条件信号后,先加锁执行下面的代码,在解锁
    	pthread_cond_wait(&_cond, &_mutex);
	}

	[self.data removeLastObject];
	NSLog(@"删除了元素");

	pthread_mutex_unlock(&_mutex);
}

// 线程2
// 往数组中添加元素
- (void)__add
{
	pthread_mutex_lock(&_mutex);

	sleep(1);

	[self.data addObject:@"Test"];
	NSLog(@"添加了元素");

	// 信号
	pthread_cond_signal(&_cond);
	// 广播
//    pthread_cond_broadcast(&_cond);

	pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
	pthread_mutex_destroy(&_mutex);
	pthread_cond_destroy(&_cond);
}

2.4 NSLock、NSRecursiveLock

NSLock是对mutex普通锁的封装

NSRecursiveLock是对mutex递归锁的封装

@property (strong, nonatomic) NSLock *lock;

- (instancetype)init {
  	if (self = [super init]) {
  		 // 初始化
  		 self.lock = [[NSLock alloc] init];
  	}
  	return self;
}

- (void)saleticket {
		
  	// 加锁
  	[self.lock lock];
  	int oldTicketsCount = self.ticketsCount;
  	sleep(.2);
  	oldTicketsCount--;
  	self.ticketsCount = oldTicketsCount;
  	NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
  	// 解锁
  	[self.lock unlock];
  }

调用ticketTest方法 打印结果:

还剩14张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩13张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩12张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩11张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩10张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩9张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩8张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩7张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩6张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩5张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩4张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩3张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩2张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩1张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩0张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}

NSRecursiveLock用法与NSLock的用法一样。

2.5 NSCondition

NSCondition是对mutexcond的封装

@property (strong, nonatomic) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;

- (instancetype)init
  {
  	if (self = [super init]) {
      	self.condition = [[NSCondition alloc] init];
      	self.data = [NSMutableArray array];
  	}
  	return self;
  }

  - (void)otherTest
  {
  	[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
  
  	[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
  }

  // 生产者-消费者模式

  // 线程1
  // 删除数组中的元素
  - (void)__remove
  {
  	[self.condition lock];
  	NSLog(@"__remove - begin");
  
  	if (self.data.count == 0) {
      	// 等待
      	[self.condition wait];
  	}
  
  	[self.data removeLastObject];
  	NSLog(@"删除了元素");
  
  	[self.condition unlock];
  }

  // 线程2
  // 往数组中添加元素
  - (void)__add
  {
  	[self.condition lock];
  
  	sleep(1);
  
  	[self.data addObject:@"Test"];
  	NSLog(@"添加了元素");
  	// 信号
  	[self.condition signal];
  
  	// 广播
  //    [self.condition broadcast];
  	[self.condition unlock];
  
  }

2.6 NSConditionLock

它是对NSCondition的进一步封装,可以设置具体的条件值

@property (strong, nonatomic) NSConditionLock *conditionLock;

- (instancetype)init
{
		if (self = [super init]) {
			// 初始化的时候 设置条件为1
      	self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
  	}
  	return self;
}

- (void)otherTest
{
  	[[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
  
  	[[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
  
  	[[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one
{
		// 加锁
  	[self.conditionLock lock];
  
  	NSLog(@"__one");
  	sleep(1);
  	// 解锁时设置锁的条件为2
  	[self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
		// 当条件为2时 加锁
  	[self.conditionLock lockWhenCondition:2];
  
  	NSLog(@"__two");
  	sleep(1);
  	// 解锁时设置锁的条件为3
  	[self.conditionLock unlockWithCondition:3];
}

- (void)__three
{
		// 当条件为3时 加锁
  	[self.conditionLock lockWhenCondition:3];
  
  	NSLog(@"__three");
  	// 解锁
  	[self.conditionLock unlock];
}

2.7 dispatch_queue

直接使用GCD的串行队列,也可以实现线程同步

@property (strong, nonatomic) dispatch_queue_t queue;

- (instancetype)init {
  	if (self = [super init]) {
  		 // 初始化
  		 self.queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
  	}
  	return self;
}

- (void)saleticket {
		
  	dispatch_sync(self.moneyQueue, ^{
      	int oldTicketsCount = self.ticketsCount;
  		sleep(.2);
  		oldTicketsCount--;
  		self.ticketsCount = oldTicketsCount;
  		NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
  	});
  }

调用ticketTest方法 打印结果:

还剩14张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩13张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩12张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩11张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩10张票 - <NSThread: 0x6000003114c0>{number = 3, name = (null)}
还剩9张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩8张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩7张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩6张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩5张票 - <NSThread: 0x600000310cc0>{number = 4, name = (null)}
还剩4张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩3张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩2张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩1张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}
还剩0张票 - <NSThread: 0x600000311580>{number = 5, name = (null)}

2.8 dispatch_semaphore

信号量的初始值可以控制线程并发访问的最大值

初始值为1时,代表同时只允许1条线程访问资源,保证线程的同步

@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;


- (instancetype)init {
  	if (self = [super init]) {
  		 // 初始化
  		 self.semaphore = dispatch_semaphore_create(5); 
  		 self.ticketSemaphore = dispatch_semaphore_create(1);  
 		}
  	return self;
}

- (void)saleticket {
		// 让信号量的值-1 为0 则等待,相当于加锁
  	dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
  	int oldTicketsCount = self.ticketsCount;
  	sleep(.2);
  	oldTicketsCount--;
  	self.ticketsCount = oldTicketsCount;
  	NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
  	// 让信号量的值+1,相当于解锁
  	dispatch_semaphore_signal(self.ticketSemaphore);
  }
  
  - (void)otherTest
  {
  	for (int i = 0; i < 20; i++) {
      	[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
  	}
  }

  // 线程的最大并发数为 5
  - (void)test
  {
  	// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
  	// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
  	dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
  
  	sleep(2);
  	NSLog(@"test - %@", [NSThread currentThread]);
  
  	// 让信号量的值+1
  	dispatch_semaphore_signal(self.semaphore);
  }

调用otherTest会发现每隔两秒打印5条信息。

2.9 @synchronized

是对mutex的递归锁的封装,源码objc4中的objc-sync.mm文件查看 @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁解锁操作

- (void)saleticket {
	@synchronized(self) { // objc_sync_enter
    	int oldTicketsCount = self.ticketsCount;
		sleep(.2);
		oldTicketsCount--;
		self.ticketsCount = oldTicketsCount;
		NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
	} // objc_sync_exit
}

本文来自:旺仔大包子

推荐上篇文章:iOS多线程GCD详细总结