Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步

什么是线程的同步

当有多个线程要同时对一个共享的内存空间进行读写时,我们要保证这个内存空间对于多个线程来说是一致的。当多个线程同时读/写这个内存空间时,就需要对线程进行同步,以确保任何时刻只有一个线程能修改该内存空间,这样才能保证线程不会访问到无效的数据。
我通过下面这幅图解释下线程同步的重要性:

线程同步的重要性

在这个例子中,两个线程A和B都要按顺序做以下3件事:

  1. 将变量 i 写入寄存器
  2. 寄存器加1
  3. 将寄存器内容重新写回变量 i

线程A先运行,线程B在线程A运行到第2步时开始运行,我们期待的结果是最终变量 i 的值会加2,但由于这两个线程没有进行同步,最后变量 i 的值只加了1。因此,对于多线程程序来说,线程的同步是很重要的。

线程的同步既然这么重要,那我们能通过什么办法来对其进行同步呢?我这里介绍三种基本的线程同步方法:

  1. 互斥量(mutex)
  2. 读写锁(rwlock)
  3. 条件变量(cond)

互斥量

简单来说,互斥量就是一把锁住共享内存空间的锁,有了它,同一时刻只有一个线程可以访问该内存空间。当一个线程锁住内存空间的互斥量后,其他线程就不能访问这个内存空间,直到锁住该互斥量的线程解开这个锁。

互斥量的初始化

对于一个互斥量,我们首先需要对它进行初始化,然后才能将其锁住和解锁。我们可以使用动态分配和静态分配两种方式初始化互斥量。

分配方式 说明
动态分配 调用pthread_mutex_init()函数,在释放互斥量内存空间前要调用pthread_mutex_destroy()函数
静态分配 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

下面是 pthread_mutex_init()pthread_mutex_lock() 函数的原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                       const pthread_mutexattr_t *restrict attr);
                       
args:
    pthread_mutex_t *restrict mutex         : 指向需要被初始化的互斥量的指针
    const pthread_mutexattr_t *restrict attr: 指向需要被初始化的互斥量的属性的指针

return:
    互斥量初始化的状态,0是成功,非0是失败
    


int pthread_mutex_destroy(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向需要被销毁的互斥量的指针
    
return:
    互斥量销毁的状态,0是成功,非0是失败

互斥量的操作

互斥量的基本操作有三种:

互斥量操作方式 说明
pthread_mutex_lock() 锁住互斥量,如果互斥量已经被锁住,那么会导致该线程阻塞。
pthread_mutex_trylock() 锁住互斥量,如果互斥量已经被锁住,不会导致线程阻塞。
pthread_mutex_unlock() 解锁互斥量,如果一个互斥量没有被锁住,那么解锁就会出错。

上面三个函数的原型:

int pthread_mutex_lock(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向需要被锁住的互斥量的指针
    
return:
    互斥量锁住的状态,0是成功,非0是失败
    


int pthread_mutex_trylock(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向需要被锁住的互斥量的指针
return:
    互斥量锁住的状态,0是成功,非0是失败
    


int pthread_mutex_unlock(pthread_mutex_t *mutex);

args:
    pthread_mutex_t *mutex: 指向需要被解锁的互斥量的指针
    
return:
    互斥量解锁的状态,0是成功,非0是失败

死锁

如果互斥量使用不当可能会造成死锁现象。死锁指的是两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。比如线程1锁住了资源A,线程2锁住了资源B;我们再让线程1去锁住资源B,线程2去锁住资源A。因为资源A和B已经被线程1和2锁住了,所以线程1和2都会被阻塞,他们会永远在等待对方资源的释放。

为了避免死锁的发生,我们应该注意以下几点;

  1. 访问共享资源时需要加锁
  2. 互斥量使用完之后需要销毁
  3. 加锁之后一定要解锁
  4. 互斥量加锁的范围要小
  5. 互斥量的数量应该少

读写锁

读写锁和互斥量相似,不过具有更高的并行性。互斥量只有锁住和解锁两种状态,而读写锁可以设置读加锁,写加锁和不加锁三种状态。对于写加锁状态而言,任何时刻只能有一个线程占有写加锁状态的读写锁;而对于读加锁状态而言,任何时刻可以有多个线程拥有读加锁状态的读写锁。下面是一些读写锁的特性:

特性 说明
1 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
2 当读写锁是读加锁状态时,所有处于读加锁状态的线程都可以对其进行加锁。
3 当读写锁是读加锁状态时,所有处于写加锁状态的线程都必须阻塞直到所有的线程释放该锁。
4 当读写锁是读加锁状态时,如果有线程试图以写模式对其加锁,那么读写锁会阻塞随后的读模式锁请求。

读写锁的初始化

同互斥量类似,我们需要先初始化读写锁,然后才能将其锁住和解锁。要初始化读写锁,我们使用 pthread_rwlock_init() 函数,同互斥量类似,在释放读写锁内存空间前,我们需要调用 pthread_rwlock_destroy() 函数来销毁读写锁。

下面是 pthread_rwlock_init()pthread_rwlock_destroy() 函数的原型:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
                        
args:
    pthread_rwlock_t *restrict rwlock:          指向需要初始化的读写锁的指针
    const pthread_rwlockattr_t *restrict attr: 指向需要初始化的读写锁属性的指针 

return:
    读写锁初始化的状态,0是成功,非0是失败

    

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向需要被销毁的读写锁的指针

return:
    读写锁销毁的状态,0是成功,非0是失败

读写锁的操作

同互斥量类似,读写锁的操作也分为阻塞和非阻塞,我们先来看看读写锁有哪些基本操作:

读写锁操作方式 说明
int pthread_rwlock_rdlock() 读写锁读加锁,会阻塞其他线程
int pthread_rwlock_tryrdlock() 读写锁读加锁,不阻塞其他线程
int pthread_rwlock_wrlock() 读写锁写加锁,会阻塞其他线程
int pthread_rwlock_trywrlock() 读写锁写加锁,不阻塞其他线程
int pthread_rwlock_unlock() 读写锁解锁

下面是这几个函数的原型:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向需要加锁的读写锁的指针

return:
    读写锁加锁的状态,0是成功,非0是失败



int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向需要加锁的读写锁的指针

return:
    读写锁加锁的状态,0是成功,非0是失败



int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向需要加锁的读写锁的指针 

return:
    读写锁加锁的状态,0是成功,非0是失败



int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向需要加锁的读写锁的指针 

return:
    读写锁加锁的状态,0是成功,非0是失败
    


int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

args:
    pthread_rwlock_t *rwlock: 指向需要解锁的读写锁的指针 

return:
    读写锁解锁的状态,0是成功,非0是失败

条件变量

条件变量是和互斥量一起使用的。如果一个线程被互斥量锁住,但这个线程却不能做任何事情时,我们应该释放互斥量,让其他线程工作,在这种情况下,我们可以使用条件变量;如果某个线程需要等待系统处于某种状态才能运行,此时,我们也可以使用条件变量。

条件变量的初始化

同互斥量一样,条件变量可以使用动态分配和静态分配的方式进行初始化:

分配方式 说明
动态分配 调用pthread_cond_init()函数,在释放条件变量内存空间前需要调用pthread_cond_destroy()函数
静态分配 pthread_cond_t cond = PTHREAD_COND_INITIALIZER

下面是 pthread_cond_init()pthread_cond_destroy() 函数的原型:

int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);
                      
args:
    pthread_cond_t *restrict cond          : 指向需要初始化的条件变量的指针
    const pthread_condattr_t *restrict attr: 指向需要初始化的条件变量属性的指针
    
return:
    条件变量初始化的状态,0是成功,非0是失败



int pthread_cond_destroy(pthread_cond_t *cond);

args:
    pthread_cond_t *cond: 指向需要被销毁的条件变量的指针

return:
    条件变量销毁的状态,0是成功,非0是失败

条件变量的操作

条件变量的操作分为等待和唤醒,等待操作的函数有 pthread_cond_wait()pthread_cond_timedwait();唤醒操作的函数有 pthread_cond_signal()pthread_cond_broadcast()

我们来看看 pthread_cond_wait() 是怎么使用的,下面是函数原型:

int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);
                      
args:
    pthread_cond_t *restrict cond  : 指向需要等待的条件变量的指针
    pthread_mutex_t *restrict mutex: 指向传入互斥量的指针

return:
    0是成功,非0是失败

当一个线程调用 pthread_cond_wait() 时,需要传入条件变量和互斥量,这个互斥量必须要是被锁住的。当传入这两个参数后,

  1. 该线程将被放到等待条件的线程列表中
  2. 互斥量被解锁

这两个操作都是原子操作。当这两个操作结束后,其他线程就可以工作了。当条件变量为真时,系统切换回这个线程,函数返回,互斥量重新被加锁。

当我们需要唤醒等待的线程时,我们需要调用线程的唤醒函数,下面是函数的原型:

int pthread_cond_signal(pthread_cond_t *cond);

args:
    pthread_cond_t *cond: 指向需要唤醒的条件变量的指针
    
return:
    0是成功,非0是失败


    
int pthread_cond_broadcast(pthread_cond_t *cond);
 
args:
    pthread_cond_t *cond: 指向需要唤醒的条件变量的指针 
    
return:
    0是成功,非0是失败

pthread_cond_signal()pthread_cond_broadcast() 的区别在于前者用于唤醒一个等待条件的线程,而后者用于唤醒所有等待条件的线程。

总结

这篇文章主要介绍了多线程中同步的重要性和线程同步的三种方法。在下篇文章中我将通过程序实例来演示如何在代码中使用多线程。

如果觉得本文对你有帮助,请多多点赞支持,谢谢!

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

推荐阅读更多精彩内容