线程同步之互斥锁

LockMind.png

这是并发控制方案的系列文章,介绍了各种锁的使用及优缺点。

  1. 自旋锁
  2. os_unfair_lock
  3. 互斥锁
  4. 递归锁
  5. 条件锁
  6. 读写锁
  7. @synchronized

OSSpinLock、os_unfair_lock、pthread_mutex_t、pthread_cond_t、pthread_rwlock_t 是值类型,不是引用类型。这意味着使用 = 会进行复制,使用复制的可能导致闪退。pthread 函数认为其一直处于初始化的内存地址,将其移动到其他内存地址会产生问题。使用copy的OSSpinLock不会崩溃,但会得到一个全新的锁。

如果你对线程、进程、串行、并发、并行、锁等概念还不了解,建议先查看以下文章:

互斥锁(mutual exclusion,缩写为 mutex)是一种并发控制方案,用于解决竞争条件(race condition)。

这篇文章包含两种实现互斥锁的方案:pthread_mutex_tNSLock

1. pthread_mutex_t

可移植操作系统接口(Portable Operating System Interface,缩写为 POSIX)是 IEEE 为在各种 UNIX 操作系统上运行软件,而定义的一系列互相关联的标准总和,X 表明其对 Unix API 的传承。

POSIX Thread 常被称为 pthreads,是 POSIX 的线程标准,定义了创建和操作线程的一套 API。通常,组合为 libpthread 库。

Pthreads 定义了一套 C 语言的函数、常量、类型,以pthread.h头文件和一个线程库实现。Pthreads API 中大致有100个函数调用,全部以pthread_开头,可以分为以下四类:

  • 线程管理:如创建线程、等待线程、查询线程状态等。
  • 互斥锁:创建、销毁、加锁、解锁、设置属性等。
  • 条件变量(Condition Variable):创建、销毁、等待、通知、设置、查询属性等操作。
  • 使用互斥锁的线程间同步管理。

1.1 初始化pthread_mutex_init()

使用pthread_mutex_init()初始化pthread_mutex_tpthread_mutex_init()第二个参数为nil时,默认 mutex type 为PTHREAD_MUTEX_NORMAL。此时,不提供死锁检测。同一线程尝试多次加锁会导致死锁,从其他线程解锁,或解锁未锁定的锁,都会产生无法预期的结果。

如果 mutex type 为PTHREAD_MUTEX_ERRORCHECK,则会提供错误检测。同一线程尝试多次加锁会返回错误,从其他线程解锁,或解锁未锁定的锁,都会返回错误。PTHREAD_MUTEX_ERRORCHECK类型锁可用于调试。

如果 mutex type 为PTHREAD_MUTEX_RECURSIVE,则锁为递归锁

这里初始化递归锁,方法如下:

    private var ticketMutex: pthread_mutex_t = pthread_mutex_t()
    private var moneyMutex: pthread_mutex_t = pthread_mutex_t()
    
    override init() {
        pthread_mutex_init(&ticketMutex, nil)
        pthread_mutex_init(&moneyMutex, nil)
        
        super.init()
    }

pthread_mutex_init()函数使用指定的 att 属性初始化 mutex。如果 att 为 nil,则使用默认的 mutex attribute。mutex 初始化后为未加锁状态。

尝试初始化已经初始化的 mutex 会产生无法预期的结果。

1.2 加锁pthread_mutex_lock()

调用pthread_mutex_lock()函数给 mutex 对象加锁。如果 mutex 已经加锁,尝试加锁的线程会被堵塞,直到 mutex 解锁。该函数返回加锁的 mutex。

加锁方法如下:

        pthread_mutex_lock(&moneyMutex)

pthread_mutex_trylock()pthread_mutex_lock()用法一样,但当 mutex 已加锁时,pthread_mutex_trylock()会立即返回。

1.3 解锁pthread_mutex_unlock()

pthread_mutex_unlock()解锁 mutex。如果当前有被堵塞的线程,解锁后 mutex 将可用,调度器会决定哪个线程获取 mutex。

        pthread_mutex_unlock(&moneyMutex)

1.4 销毁pthread_mutex_destory()

pthread_mutex_destory()函数销毁 mutex。销毁的 mutex 可以再次使用pthread_mutex_init()初始化,其他方式使用已销毁的 mutex 会产生无法预期的结果。

可以销毁已初始化、未锁定的 mutex,但不能销毁已加锁的 mutex。

        pthread_mutex_destroy(&moneyMutex)

pthread_mutex_init()pthread_mutex_destory()执行成功后,返回0;反之,返回错误码。

完整代码如下:

class NormalMutexDemo: BaseDemo {
    private var ticketMutex: pthread_mutex_t = pthread_mutex_t()
    private var moneyMutex: pthread_mutex_t = pthread_mutex_t()
    
    override init() {
        pthread_mutex_init(&ticketMutex, nil)
        pthread_mutex_init(&moneyMutex, nil)
        
        super.init()
    }
    
    override func saveMoney() {
        pthread_mutex_lock(&moneyMutex)
        
        super.saveMoney()
        
        pthread_mutex_unlock(&moneyMutex)
    }
    
    override func drawMoney() {
        pthread_mutex_lock(&moneyMutex)
        
        super.drawMoney()
        
        pthread_mutex_unlock(&moneyMutex)
    }
    
    override func saleTicket() {
        pthread_mutex_lock(&ticketMutex)
        
        super.saleTicket()
        
        pthread_mutex_unlock(&ticketMutex)
    }
    
    deinit {
        pthread_mutex_destroy(&moneyMutex)
        pthread_mutex_destroy(&ticketMutex)
    }
}

2. NSLock

NSLock使用 POSIX thread 实现锁,是对 pthread_mutex_t 的封装。

不要使用NSLock实现递归锁,在同一线程调用两次 lock 会永久锁定线程。如需实现递归锁,请使用NSRecursiveLock类。

2.1 GNUstep

GNUstep 是 GNU 计划的项目之一,它将 Cocoa 库以自由软件方式重新实现。虽然其不是 Cocoa 的源码,但具有一定参考价值。

GNUstep Downloads下载 GNUstep Base 源码。

2.2 NSLocking协议

NSLocking协议实现了lock()unlock()方法,

  • lock():尝试加锁。会堵塞线程,直到可以获取到锁。
  • unlock():解锁之前获取的锁。

NSRecursiveLockNSConditionNSConditionLock均遵守NSLocking协议。

2.3 初始化NSLock()

使用以下方法初始化锁:

    private var ticketLock = NSLock()

在 GNUstep 的NSLock.m文件中,其初始化代码如下:

/* Use an error-checking lock.  This is marginally slower, but lets us throw
 * exceptions when incorrect locking occurs.
 */
- (id) init
{
    if (nil != (self = [super init])) {
        if (0 != pthread_mutex_init(&_mutex, &attr_reporting)) {
            DESTROY(self);
        }
    }
    return self;
}

可以看到,NSLock底层使用的是pthread_mutex_t

2.4 加锁

NSLocklock()lock(before:)try()三种加锁方式:

2.4.1 lock()

如果已经加锁,lock()会堵塞线程直到可以获取到锁。

        moneyLock.lock()

GNUstep 中实现如下:

#define MLOCK \
- (void) lock\
{\
  int err = pthread_mutex_lock(&_mutex);\
  if (EDEADLK == err)\
    {\
      (*_NSLock_error_handler)(self, _cmd, YES, @"deadlock");\
    }\
  else if (err != 0)\
    {\
      [NSException raise: NSLockException format: @"failed to lock mutex"];\
    }\
}
2.5.2 lock(before:)

lock(before:)在指定时间前尝试加锁,到达指定时间后立即返回。如果加锁成功,返回 true,否则,返回 false。

GNUstep 中实现如下:

- (BOOL) lockBeforeDate: (NSDate*)limit
{
    do {
        int err = pthread_mutex_trylock(&_mutex);
        if (0 == err) {
            CHK(Hold)
            return YES;
        }
        if (EDEADLK == err) {
            (*_NSLock_error_handler)(self, _cmd, NO, @"deadlock");
        }
        sched_yield();
    } while ([limit timeIntervalSinceNow] > 0);
    return NO;
}
2.5.3 try()

try()尝试加锁。如果锁已经锁定,立即返回 false。

GNUstep 中实现如下:

#define MTRYLOCK \
- (BOOL) tryLock\
{\
  int err = pthread_mutex_trylock(&_mutex);\
  if (0 == err) \
    { \
      CHK(Hold) \
      return YES; \
    } \
  else \
    { \
      return NO;\
    } \
}

3. 解锁unlock()

必须在加锁的线程调用unlock()解锁,在其他线程解锁会产生无法预期的错误。解锁未加锁的 lock 属于语法错误。触发此类错误时,NSLock会将错误输出到控制台。

        moneyLock.unlock()

GNUstep 中实现如下:

#define MUNLOCK \
- (void) unlock\
{\
  if (0 != pthread_mutex_unlock(&_mutex))\
    {\
      [NSException raise: NSLockException\
        format: @"failed to unlock mutex"];\
    }\
  CHK(Drop) \
}

Demo名称:Synchronization
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Synchronization

上一篇:线程同步之os_unfair_lock

下一篇:线程同步之递归锁

参考资料:

  1. pthread_mutex_init() pthread_mutex_destroy()
  2. lock and unlock a mutex
  3. NSLock

欢迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/线程同步之互斥锁.md

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容