线程同步问题(一)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言


对于单线程来说它不需要处理线程的同步问题,所以线程同步是在多线程环境下需要注意的问题。线程的主要优势在于资源的共享性。譬如通过全局变量来实现信息共享。
但是这也会带来一个问题,多线程并发共享数据,会带来数据不一致的问题。这时候就需要使用线程同步来避免这个问题。

一、线程同步

1、线程同步是为了对共享的资源进行保护;
共享资源指的是,多个线程都会进行访问的资源;
2、保护的目的是为了解决数据的一致性问题;
当一个线程修改的变量是其他线程可以读取或者修改的时候,就存在数据一致性问题。需要对这些线程进行同步操作,以确保他们在访问变量的时候,取到的都是有效值。
3、出现数据一致性问题的本质在于进程中的多个线程对共享资源的并发访问(同时访问)。
4、如何解决对共享资源的并发访问出现的数据不一致的问题。
需要线程同步技术,来实现同一时间,只允许一个线程访问变脸,防止出现并发访问的情况,来解决数据不一致性的问题。
线程的主要优势就是资源共享,但是必须要确保不会出现对共享资源并发访问。所以要采用相应的方法实现线程同步。常见的方法有:
互斥锁,条件变量,自旋锁,以及读写锁

二、互斥锁

在访问共享资源之前对互斥锁进行上锁,在访问结束后释放互斥锁。
互斥锁锁上之后,任何其他视图再次对互斥锁进行加锁的线程都会被阻塞,直至当前线程释放互斥锁。在释放互斥锁的时候,阻塞的线程会被唤醒。多个阻塞的线程都会尝试去上锁,当有一个线程上锁之后,其余的会再次陷入阻塞,等待再一次解锁。
(简单的例子就是卫生间,上卫生间一次只能进去一个人,进去后锁上,外面的人排队)
需要注意的是,只有将所有的线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。加入有一个线程在没有得到锁的情况下也能访问共享资源,那么即使其他的线程在使用共享资源前都申请了互斥锁,也有可能会出现数据不一致。

互斥锁使用pthread_mutex_t数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。

互斥锁初始化

1、使用PTHREAD_MUTEX_INITIALIZER宏初始化互斥锁
互斥锁使用pthread_mutex_t数据类型表示,pthread_mutex_t是一个结构体类型

# define PTHREAD_MUTEX_INITIALIZER \ 
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

使用宏初始化互斥锁的操作为:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

PTHREAD_MUTEX_INITIALIZER 宏中携带了互斥锁的默认属性

2、使用pthread_mutex_init()函数初始化互斥锁
宏只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式,譬如先定义互斥锁,后再进行初始化,或者在堆中动态分配的互斥锁,譬如使用malloc()函数申请分配的互斥锁对象,那么在这些情况下,可以使用pthread_mutex_init()函数对互斥锁进行初始化,其函数原型如下所示:

#include <pthread.h> 
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)

函数参数和返回值含义如下:
mutex:参数mutex是一个pthread_mutex_t类型指针,指向需要进行初始化操作的互斥锁对象;
attr:参数attr是一个pthread_mutexattr_t类型指针,指向一个pthread_mutexattr_t类型对象,该对象用于定义互斥锁的属性;若将参数attr设置为NULL,则表示将互斥锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_MUTEX_INITIALIZER这种方式初始化,而不同之处在于,使用宏不进行错误检查。
使用pthread_mutex_init()函数对互斥锁进行初始化示例:

pthread_mutex_t mutex; 
pthread_mutex_init(&mutex, NULL);

或者:

pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t)); 
pthread_mutex_init(mutex, NULL);

互斥锁加锁和解锁

互斥锁初始化之后,处于一个未锁定状态,调用函数pthread_mutex_lock()可以对互斥锁加锁、获取互斥锁,而调用函数pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。其函数原型如下所示:

#include <pthread.h> 
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

使用这些函数需要包含头文件<pthread.h>,参数mutex指向互斥锁对象;pthread_mutex_lock()和pthread_mutex_unlock()在调用成功时返回0;失败将返回一个非0值的错误码。
调用pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。

调用pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:

  • 对处于未锁定状态的互斥锁进行解锁操作;
  • 解锁由其它线程锁定的互斥锁
    如果有多个线程处于阻塞状态等待互斥锁被解锁,当互斥锁被当前锁定它的线程调用pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会成功!

pthread_mutex_trylock()函数

当互斥锁已经被其它线程锁住时,调用pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用pthread_mutex_trylock()函数;调用pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码EBUSY。

#include <pthread.h> 
int pthread_mutex_trylock(pthread_mutex_t *mutex);

参数mutex指向目标互斥锁,成功返回0,失败返回一个非0值的错误码,如果目标互斥锁已经被其它线程锁住,则调用失败返回EBUSY。

销毁互斥锁

当不再需要互斥锁时,应该将其销毁,通过调用pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:

#include <pthread.h> 
int pthread_mutex_destroy(pthread_mutex_t *mutex);

使用该函数需要包含头文件<pthread.h>,参数mutex指向目标互斥锁;同样在调用成功情况下返回0,失败返回一个非0值的错误码。

  • 不能销毁还没有解锁的互斥锁,否则将会出现错误;
  • 没有初始化的互斥锁也不能销毁。

被pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用。

互斥锁死锁

当一个线程试图对同一个互斥锁加锁两次,会出现什么情况?情况就是该线程会陷入死锁状态,一直被阻塞永远出不来;这就是出现死锁的一种情况,除此之外,使用互斥锁还有其它很多种方式也能产生死锁。
比如一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁。
(简言之就是,两个线程各有一个互斥锁,但是有彼此再向对方的线程请求上锁,这样两个线程都被阻塞住了。就产生了死锁)——(这就好比是C语言中两个头文件相互包含的关系,那肯定编译报错!)
为了防止死锁,就需要对互斥锁的层级逻辑关系进行明确,当多个线程对一组互斥锁进行操作的时候总是按照相同的顺序进行锁定,就能够避免死锁。
再就是尝试使用pthread_mutex_trylock()以不阻塞的方式对互斥锁进行上锁。

互斥锁属性

属性太多,仅对互斥锁的类型进行介绍:

  • PTHREAD_MUTEX_NORMAL:标准互斥锁类型,不做任何的错误检查或者死锁检测。。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。
  • PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
  • PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
  • PTHREAD_MUTEX_DEFAULT:此类互斥锁提供默认的行为和特性。使用宏PTHREAD_MUTEX_INITIALIZER初始化的互斥锁,或者调用参数arg为NULL的pthread_mutexattr_init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性,Linux上,PTHREAD_MUTEX_DEFAULT类型互斥锁的行为与PTHREAD_MUTEX_NORMAL类型相仿。
    可以使用pthread_mutexattr_gettype()函数得到互斥锁的类型属性,使用pthread_mutexattr_settype()修改/设置互斥锁类型属性,其函数原型如下所示:
#include <pthread.h> 
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type); 
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

总结

待补充,补充内容为:条件变量,自旋锁,读写锁

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值