C语言多线程编程-死锁和线程同步方式介绍(一)

来源:互联网 发布:c语言图形输出 编辑:程序博客网 时间:2024/06/07 18:41

序言

实验室项目采用多线程实现,然而暂时只涉及到几个基本的线程操作函数,线程和进程的区别、线程的同步和异步机制以及线程通信等暂时都没有涉及,打算在这里做些总结,以备后用。本文打算学习死锁和线程同步。

1. 死锁

死锁是指多个线程因竞争资源而造成的一种互相等待的僵局

  • 举例说明: 资源S1,S2; 进程P1,P2
    资源S1,S2都是不可剥夺资源(内存是可剥夺资源):一个进程申请了之后,不能强制收回,只能进程结束之后自动释放;
    进程P1申请了资源S1,进程P2申请了资源S2;
    接下来P1的操作用到资源S2,P2的资源用到资源S1。但是P1,P2都得不到接下来的资源,那么就引发了死锁。

  • 举例说明:线程t1,t2,t3
    如果三个线程t1,t2,t3要实现同步,在某种情况下,t1在等t2,t2要等t3,而此时t3却在等t1… 那么问题来了,很显然,t1,t2,t3都不会运行,这种现象叫死锁。若无外力作用,这些线程都将无法向前推进。这种情况在我们的程序中是不允许出现的,这种无限的等待没有意义。

1.1 死锁产生的主要原因

  • 系统资源竞争:资源分配不当,以及系统资源的竞争导致系统资源不足,导致死锁;

  • 进程运行推进顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,导致死锁。

1.2 产生死锁的四个必要条件

  • 互斥条件:一个资源一次只能被一个进程使用。

  • 请求与保持条件: 进程已经保持了至少一个资源,又提出了新的资源请求,而该资源已经被其他进程占有。此时请求进程被阻塞,而对自己已获得的资源保持不放。

  • 不可剥夺条件:进程所获得的资源在未使用完之前,不能被其他进程强行夺走,即只能由进程自己主动释放。

  • 循环等待条件:若干进程间形成首尾相接循环等待资源的关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而”只要上述条件之一不满足,就不会发生死锁“。

1.3 死锁的避免与预防

  • 死锁避免
    系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
    在系统设计、进程调度等方面注意如何让这四个必要条件不成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

  • 死锁预防
    死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。

死锁避免不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁,死锁避免是在系统运行过程中注意避免死锁的最终发生。

2. 同步

文中打算介绍三种线程同步机制:互斥锁 + 条件变量 + 读写锁

  • 互斥锁:适用于线程可用的资源只有一个,需要互斥访问的情况
  • 条件变量:适用线程之间构成条件等待关系的情况
  • 读写锁:提高互斥锁在数据库系统数据访问(大量读,较少写)等应用领域的效率

2.1 互斥锁

2.1.1 互斥锁原理

互斥锁以排他方式防止共享数据被并发访问。互斥锁是一个二元变量,只有锁定(禁止1)和解锁(允许0)两种状态,互斥锁可以看作是特殊意义的全局变量,因为在同一时刻只有一个线程能够对互斥锁进行操作。
将某个共享资源与某个特定互斥锁在逻辑上绑定,即要申请该资源必须先获取锁。对该共享资源的访问操作如下:

(1) 首先申请互斥锁,如果该互斥锁处于锁定状态,默认阻塞当前线程;如果处于解锁状态,则申请到该锁并立即占有该锁,使锁处于锁定状态防止其他线程访问该资源。
(2) 只有锁定该互斥锁的线程才能释放该互斥锁,其他线程试图释放操作无效。

2.1.2 互斥锁基本操作函数
功能 函数 初始化互斥锁 pthread_mutex_init 阻塞申请互斥锁 pthread_mutex_lock 非阻塞申请互斥锁 pthread_mutex_trylock 释放互斥锁 pthread_mutex_unlock 销毁互斥锁 pthread_mutex_destroy

使用互斥锁前先定义该互斥锁(全局变量)

pthread_mutex_t lock;

在使用互斥锁以前,必须首先对它进行初始化

静态分配的互斥锁:置为常量PTHREAD_MUTEX_INITIALIZER,属性为NULL,也可以调用pthread_mutex_init函数动态分配的互斥锁:例如通过调用malloc函数分配的互斥锁,只能调用pthread_mutex_init,且在释放内存前需要调用pthread_mutex_destroy

(1) 初始化互斥锁
int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

形参:    mutex      要初始化的互斥锁的指针    mutexattr  要初始化的互斥锁的属性;NULL表示使用默认属性其他:也可使用宏初始化静态分配的互斥锁    #define PTHREAD_MUTEX_INITIALIZER {{0,}}    pthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER;返回值:成功返回0,否则返回一个错误编号

(2) 销毁互斥锁
调用pthread_mutex_init初始化的互斥锁,在释放内存前需要调用pthread_mutex_destroy
int pthread_mutex_destroy (pthread_mutex_t *mutex);

形参:    mutex    指向要初始化的互斥锁的指针返回值:成功返回0,否则返回一个错误编号

(3) 阻塞方式申请互斥锁
int pthread_mutex_lock (pthread_mutex_t *mutex);

说明:如果一个线程要占用一个共享资源,必须先申请一个对应的互斥锁返回值:成功返回0,否则返回一个错误编号

(4) 非阻塞方式申请互斥锁
int pthread_mutex_trylock (pthread_mutex_t *mutex);

返回值:成功返回0,否则返回一个错误编号,以指明错误

(5) 释放互斥锁
int pthread_mutex_unlock (pthread_mutex_t *mutex);

说明:释放操作只能有占有该互斥锁的线程完成返回值:成功返回0,否则返回一个错误编号


2.2 条件变量

2.2.1 条件变量原理

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量不能单独使用,必须配合互斥锁一起实现对资源的互斥访问

条件变量分为两部分:条件变量。条件本身是由互斥量保护的,线程在改变条件状态前先要锁住互斥量。
条件变量使线程睡眠等待某种条件出现。条件变量主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

条件变量原理说明尤其是pthread_cond_wait()函数的使用,可参考这篇文章:http://blog.csdn.net/ithomer/article/details/6031723

2.2.2 条件变量基本操作函数
功能 函数 初始化条件变量 pthread_cond_init 阻塞等待条件变量 pthread_cond_wait 在指定的时间内阻塞等待条件变量 pthread_cond_timedwait 通知等待该条件变量的第一个线程 pthread_cond_signal 通知等待该条件变量的所有线程 pthread_cond_broadcast 销毁条件变量状态 pthread_cond_destroy

使用条件变量前,先定义该条件变量(全局变量)

pthread_cond_t condition;

pthread_cond_t数据类型的条件变量可以用两种方式进行初始化

静态分配的条件变量:把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,属性为NULL动态分配的条件变量:使用pthread_cond_init函数进行初始化

(1) 初始化条件变量
int pthread_cond_init (pthread_cond_t *restrict cond, pthread_condattr_t *restrict cond_attr);

说明:使用属性attr来初始化条件变量cond形参:    cond      指向要初始化的条件变量指针    cond_attr 指向属性对象的指针,该属性对象定义要初始化的条件变量的特性;NULL表示使用默认属性返回值:成功返回0,否则返回错误编号以指明错误
pthread_cond_t cond;pthread_condattr_t cattr;int ret;    //返回值ret = pthread_cond_init(&cond, NULL);   //默认属性初始化条件变量ret = pthread_cond_init(&cond, &cattr); //特定属性初始化条件变量

(2) 通知等待条件变量的线程
int pthread_cond_signal (pthread_cond_t *cond);

说明:通知等待条件变量的第一个线程其他:如果cond没有阻塞任何线程,则此函数不起作用    如果cond阻塞了多个线程,则调度策略将确定要取消阻塞的线程    显然在此函数被调用时隐含了释放当前线程占用的信号量的操作返回值:成功返回0,否则返回错误编号以指明错误

int pthread_cond_broadcast (pthread_cond_t *cond);

说明:唤醒等待与条件变量cond关联的条件的所有线程其他:如果cond上没有阻塞任何线程,则此函数不起作用返回值:成功返回0,否则返回错误编号以指明错误

(3) 等待条件变量
int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

说明:阻塞等待某个条件变量形参:    cond   指向要等待的条件变量的指针    mutex  指向与条件变量cond关联的互斥锁的指针返回值:成功返回0,否则返回一个错误编号

int pthread_cond_timedwait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

说明:在指定的时间范围内等待条件变量形参:    cond    指向要等待的条件变量的指针    mutex   指向与条件变量cond关联的互斥锁的指针    abstime 等待过期时的绝对时间,如果在此时间范围内取到该条件变量函数将返回            使用UTC时钟,即为一个绝对时间,该数据结构声明如下            struct timespec            {                long ts_sec;   //秒部分                long ts_nsec;  //纳秒部分            }            使用这个结构时,需要指定愿意等待多长时间,时间值是一个绝对数而不是相对数。            例如,如果能等待3分钟,就需要把当前时间加上3分钟再转换到timespec结构,而不是把3分钟转换成timespec结构。其他:wait和timedwait函数都包含了一个互斥锁。    如果线程因等待条件变量而进入等待状态,将隐含释放其申请的互斥锁;    同样,在返回时,银行申请到该互斥锁对象操作

(4) 销毁条件变量
在释放底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行去除初始化(deinitialize)。
int pthread_cond_destroy (pthread_cond_t *cond);

返回值:成功返回0,否则返回错误编号以指明错误

互斥锁是为了上锁而设计的,条件变量是为了等待而设计的


2.3 读写锁

2.3.1 读写锁原理

读写锁与互斥锁类似,不过读写锁允许更高的并行性。互斥锁要么是锁定状态要么是解锁状态,而且一次只有一个线程可以对其加锁。

  • 读写锁可以有三种状态:
    • 读模式下加锁状态
    • 写模式下加锁状态
    • 不加锁状态

读写锁非常适合于对数据结构读的次数远大于写的情况,例如对数据库系统数据的访问

  • 写模式:当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。

  • 读模式:当读写锁在读模式下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。

读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。

读写锁分为读锁和写锁,具体如下:

(1) 如果某线程申请了读锁,其他线程可以再申请读锁,但不能申请写锁(2) 如果某线程申请了写锁,则其他线程不能在申请读锁也不能申请写锁
2.3.2 读写锁的基本操作函数
功能 函数 初始化读写锁 pthread_rwlock_init 阻塞申请读锁 pthread_rwlock_rdlock 非阻塞申请读锁 pthread_rwlock_tryrdlock 阻塞申请写锁 pthread_rwlock_wrlock 非阻塞申请写锁 pthread_rwlock_trywrlock 释放锁(读锁和写锁) pthread_rwlock_unlock 销毁读写锁) pthread_rwlock_destroy

与互斥锁一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁

pthread_rwlock_t rwlock;     //全局变量

pthread_rwlock_t数据类型的读写锁可以用两种方式进行初始化

静态分配的读写锁:把常量PTHREAD_COND_INITIALIZER赋给静态分配的读写锁,属性为NULL动态分配的读写锁:使用pthread_cond_init函数进行初始化区别在于:静态初始化不执行错误检查,使用默认属性初始化读写锁

(1) 初始化读写锁
int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

说明:使用属性attr来初始化读写锁,如果attr为NULL则使用默认的读写锁属性形参:    rwlock   指向要初始化的读写锁的指针    attr     指向属性对象的指针,该属性对象定义要初始化的读写锁的特性返回值:成功返回0,否则返回错误编号以指明错误

(2) 申请读锁
int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);

说明:以阻塞方式申请读锁返回值:成功返回0,否则返回错误编号以指明错误其他:如果不能申请到该读锁,pthread_rwlock_rdlock将阻塞当前进程

int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);

说明:以非阻塞方式申请读锁返回值:成功返回0,否则返回错误编号以指明错误其他:如果不能申请到该读锁,pthread_rwlock_tryrdlock将返回错误

(3) 申请写锁
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);

说明:以阻塞方式申请写锁返回值:成功返回0,否则返回错误编号以指明错误其他:如果不能申请到该写锁,pthread_rwlock_wrlock将阻塞当前进程

int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);

说明:以非阻塞方式申请写锁返回值:成功返回0,否则返回错误编号以指明错误其他:如果不能申请到该写锁,pthread_rwlock_trywrlock将返回错误

注:申请读锁和写锁的形参都是全局变量rwlock

(4) 解锁
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);

说明:如果无论是读锁还是写锁,都使用该函数来释放锁返回值:成功返回0,否则返回错误编号以指明错误其他:    (1) 如果调用该函数来释放读锁,但当前还有其他读锁定,则保持读锁定状态,只不过当前线程已不再是其所有者之一    如果释放最后一个读锁,则读写锁将处于解锁状态    (2) 如果调用此函数释放写锁,则置读写锁为解锁状态

(5) 销毁读写锁
在释放读写锁占用的内存之前,需要调用pthread_rwlock_destroy做清理工作。
int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);

返回值:成功返回0,否则返回一个错误编号以指明错误

如果pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就丢失了。



Acknowledgements:
http://blog.csdn.net/jhonz/article/details/52786280
http://blog.csdn.net/u014588619/article/details/44684575
http://www.cnblogs.com/feisky/archive/2010/03/08/1680950.html
http://www.cnblogs.com/nufangrensheng/p/3521654.html
《高级程序设计-第三版》,人民邮电出版社,杨宗德、吕光宏等著

2017.04.17

0 0
原创粉丝点击