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

序言

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

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

你可能感兴趣的:(多线程编程)