线程是一种轻量级的进程。
进程的通信机制主要包括
无名管道、有名管道、消息队列、信号量、共享内存以及信号等。这些机制都是由linux内核来维护的,实现起来都比较复杂,而且占用大量的系统资源。
线程间的通信机制实现起来则相对简单,主要包括
互斥锁、条件变量、读写锁和线程信号等。
本文会对以上所涉及的线程间的通信机制一一展开。
互斥锁通信机制:
1、
互斥锁基本原理:互斥锁以排他的方式防止数据被并发修改。当多个线程共享相同的内存时,需要确保每个线程看到的数据是一样的。如果是只读,那么一定是一样的。如果是可读可写,在一个线程操作一个内存区域时,包含三个步骤,即读出数据,修改数据,写回数据。如果该线程在没有写回数据前,另一个线程来访问同一个区域,如果是读,得不到最新的数据状态,如果是写,则会造成数据覆盖等问题。
互斥锁就两个状态:开锁(0),上锁(1)。将某个共享资源和互斥锁绑定后,对该共享资源的访问操作如下:
A】在访问资源前,首先申请该互斥锁,如果在开锁状态,则申请到该锁对象,并立即占有该锁(锁定)。以防其他线程访问修改此资源。如果该锁处于锁定状态,默认阻塞等待。
B】原则上只有锁定该互斥锁的进程才能释放此互斥锁。但实际上,非锁定的线程去解锁也能成功。这个与锁的条件有关,本文后续内容会详细介绍。
互斥锁基本操作函数如下:
功能
函数
初始化互斥锁
pthread_mutex_init()
阻塞申请互斥锁
pthread_mutex_lock()
释放互斥锁
pthread_mutex_unlock()
尝试加锁(非阻塞方式)
pthread_mutex_trylock()
销毁互斥锁
pthread_mutex_destroy()
2、
互斥锁的初始化和销毁:pthread_mutex_init()、pthread_mutex_destroy()
B】函数原型:extern int pthread_mutex_init(pthread_mutex_t *__mutex,__const pthread_mutexattr_t *__mutexattr);
extern int pthread_mutex_destroy(
pthread_mutex_t *__mutex);
C】返回:操作成功返回0,不成功则返回非零值
D】参数:
a、第一个参数为指向要初始化/销毁的互斥锁的指针。pthread_mutex_t即互斥量类型。在使用互斥锁时,需在函数内定义一个这种类型的变量。其值可通过pthread_mutex_init()函数来以初始化,也可以通过使用pthread.h中定义的宏
PTHREAD_MUTEX_INITIALIZER
(只对静态分配的互斥量)来初始化。
如果是动态分配的互斥量,那么释放内存前需要用pthread_mutex_destroy,初始化用pthread_mutex_init()。
pthread.h中宏定义如下:
#define PTHREAD_MUTEX_INITIALIZER { {0,} }
初始化方式如下:
pthread_mutex_t p = PTHREAD_MUTEX_INITIALIZER;
b、第二个参数mutexattr是指向属性对象的指针,该属性对象定义要初始化锁的属性。如果该指针为NULL,则表示使用默认属性。锁的属性在本文后续部分有详细的介绍。
3、
互斥锁的申请、释放和尝试解锁:pthread_mutex_lock()、pthread_mutex_unlock()、pthread_mutex_trylock()
A】函数原型:extern int pthread_mutex_lock(pthread_mutex_t *__mutex);
extern int pthread_mutex_trylock(pthread_mutex_t *__mutex);
extern int pthread_mutex_unlock(pthread_mutex_t *__mutex);
B】返回:成功返回0,失败返回一个错误编号,以指明错误。(pthread_mutex_unlock()未设置errno变量)
条件变量通信机制:
1、条件变量基本原理:条件变量的出现,可以弥补互斥锁的缺陷,有些问题仅仅靠互斥锁无法解决。但是条件变量不能单独使用,必须配合互斥锁一起实现对资源的互斥访问。
例:互斥锁无法解决的问题。
int i = 3;int j = 7;
pthread A
pthread_B
pthread_mutex_lock();
pthread_mutex_lock()
{
{
i++;
if(i==j)
j--;
do_something();
}
}
pthread_mutex_unlock();
pthread_mutex_unlock();
——————————————————————————————————————
上例中:两个线程抢占互斥锁,可能会导致pthread B中的do_something()函数永远无法执行的情况。这是程序员不想看到的。仔细分析后,可得到线程B其实不需要一直的申请释放锁,其运行仅仅需要一种情况而已。在A线程满足i == j时,通知B线程执行do_something()即可。
条件变量基本操作:
功能
函数
初始化条件变量
pthread_cond_init()
阻塞等待条件变量
pthread_cond_wait()
通知等待该条件变量的第一个线程
pthread_cond_signal()
在指定的时间之内等待条件变量
pthread_cond_timedwait()
销毁条件变量状态
pthread_cond_destroy()
2、
条件变量的初始化和销毁:pthread_cond_init()、pthread_cond_destroy()
A】函数原型:extern int pthread_cond_init(pthread_cond_t *__restrict __cond,__const pthread_condattr_t *__restrict __cond_attr);
extern int pthread_cond_destroy(
pthread_cond_t *__cond);
B】返回:成功返回0,失败返回错误编号以指明错误。
C】参数:第一个参数指向要初始化或损坏的条件变量的指针,条件变量的类型为pthread_cond_t。第二个参数指向条件属性对象的指针。该属性对象定义要初始化的条件变量的特性,如果此变量初始化为NULL,则为默认属性。关于条件属性,本文后续会有详细介绍。
3、
通知等待条件变量的线程:pthread_cond_signal()、pthread_cond_broadcast()
A】函数原型:extern int pthread_cond_signal
(
pthread_cond_t *__cond
);
extern int pthread_cond_broadcast
(
pthread_cond_t *__cond
);
B】说明:
a、pthread_cond_signal()函数用于唤醒等待出现与条件变量cond相关联的条件的第一个线程。如果cond上没有阻塞任何线程,则此函数不起作用。如果cond阻塞了多个线程,则调度策略将确定要取消阻塞的线程。显然,在此函数中,隐含的释放了当前线程占用的信号量(备注:信号和信号量不是一个东西,在进程和进程通信中会详细说明信号和信号量)。
b、pthread_cond_broadcast()函数用于唤醒等待出现与条件变量cond关联的条件的所有线程。如果cond上没有阻塞任何线程,则此函数不起作用。
4、
等待条件变量:pthread_cond_wait()、pthread_cond_timedwait()
A】函数原型:extern int pthread_cond_wait(pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex);
extern int pthread_cond_timedwait(
pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex ,
__const struct timespec
*__restrict __abstime);
B】参数说明:cond是指向要等待的条件变量的指针,mutex指向与条件变量cond关联的互斥锁的指针。pthread_cond_wait()、
pthread_cond_timedwait()函数的实现是一个
先对互斥锁进行解锁,再加锁的一个过程。pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)函数传入的参数mutex用于保护条件,因为我们在调用pthread_cond_wait时,如果条件不成立我们就进入阻塞,但是进入阻塞这个期间,如果条件变量改变了的话,那我们就漏掉了这个条件。因为这个线程还没有放到等待队列上,所以调用pthread_cond_wait前要先锁互斥量,即调用pthread_mutex_lock(),pthread_cond_wait在把线程放进阻塞队列后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。这样其它线程才能对临界资源进行访问并在适当的时候唤醒这个阻塞的进程。当pthread_cond_wait返回的时候又自动给mutex加锁。
Thread A:当满足条件的时候发送一个信号。
Thread B:先给一个mutex加锁,以便互斥访问count的值。在一个while循环里等待count值达到MAX_COUNT。因为当某个条件满足时,可能会有多个线程被唤醒。所以需要判断条件是否还满足。pthread_cond_wait首先把调用线程放入条件变量的等待队列,然后再释放mutex。当函数返回时,mutex又会被加上锁。最后对mutex解锁,让其他线程使用count变量。(加了写锁的等待就是占着茅坑不拉屎,有数据更新此操作域又执行不了写操作,只能先解锁咯~~~~)
pthread_cond_timedwait()多了一个参数,即abstime,abstime是从1970年1月1日00:00:00以来的秒数,是一个绝对时间。等待时间到,则不阻塞,往下执行程序。timespec结构体声明如下:
struct timespec
{
long tv_sec;
long tv_nsec;
};
C】返回:如果成功,返回0,失败则返回一个错误编号。
读写锁通信机制:
1、
读写锁基本原理:在对数据的读写操作时,往往是读占主要部分。为了满足当前能够允许多个读出,但只允许一个写入的需求。线程提供了读写锁来实现。读写锁基本原则如下:
A】如果有其他线程读数据,则允许其他线程执行读操作,但是不允许写操作。
B】如果有其他线程申请了写锁,则其他线程既不能申请读锁,也不能申请写锁
读写锁基本操作函数如下:
功能
函数
初始化读写锁
pthread_rwlock_init()
阻塞申请读锁
pthread_rwlock_rdlock()
非阻塞申请读锁
pthread_rwlock_tryrdlock()
阻塞申请写锁
pthread_rwlock_wrlock()
非阻塞申请写锁
pthread_rwlock_trywrlock()
释放锁(读锁和写锁)
pthread_rwlock_unlock()
毁坏读写锁
pthread_rwlock_destroy()
2、
初始化/毁坏读写锁:pthread_rwlock_init()、pthread_rwlock_destroy()
A】函数原型:extern int pthread_rwlock_init(pthread_rwlock_t *__restrict __rwlock,__const pthread_rwlockattr_t *__restrict __attr);
extern int pthread_rwlock_destroy(pthread_rwlock_t *__rwlock);
B】返回:如果成功则返回0,否则返回一个错误编号,以指明错误。
C】参数说明:第一个参数指向要初始化的读写锁的指针。类型为pthread_rwlock_t。第二个参数为读写锁的属性。在本文后续部分会详细说明。
3、
申请读锁,写锁和解除读写锁:pthread_rwlock_rdlock()、pthread_rwlock_tryrdlock()、pthread_rwlock_wrlock()、pthread_rwlock_trywrlock()、pthread_rwlock_unlock()
A】函数原型:extern int pthread_rwlock_rdlock(pthread_rwlock_t *__rwlock);
extern int pthread_rwlock_tryrdlock(
pthread_rwlock_t *__rwlock);
extern int pthread_rwlock_wrlock(
pthread_rwlock_t *__rwlock);
extern int pthread_rwlock_trywrlock(
pthread_rwlock_t *__rwlock);
extern int pthread_rwlock_unlock(
pthread_rwlock_t *__rwlock);
B】说明:成功返回0,失败则返回一个错误编号以表明错误。
调用pthread_rwlock_unlock时需注意:
a、如果调用此函数来释放读写锁定rwlock上的读锁定,但当前在此读写锁定上还保持着其他的读锁定,则读锁定将保持线程读锁定状态。只不过当前线程已经不是其所有者之一。
b、如果此函数释放读写锁的最后一个读锁,则对象将处于没有所有者的解锁状态。
c、如果调用此函数释放读写锁的写锁,则读写锁定将处于没有所有者的解锁状态。
线程信号:
线程是一种轻量级的进程,因此进程的信号同样适用于线程。不过相对于进程信号,线程拥有与信号相关的私有数据——线程信号掩码,则就决定了线程在信号操作时具有以下特性:
A】每个线程可以先别的线程发送信号,pthread_kill()函数用来完成这一操作。
B】每个线程都可以设置自己的阻塞集合。pthread_sigmask()用来完成这一操作。类似于进程中的sigprocmask()函数。主进程创建出来的线程继承主进程的掩码。
C】每个线程需要设置针对某信号的处理方式,但同一个进程中对某信号的处理方式只能有一个有效,即最后一次设置的处理方式。即在所有的线程里,同一信号在任何线程里的对该信号的处理一定相同
D】如果别的进程向当前进程发来一个信号,具体由哪个形成去处理,是未知的。
1、
向指定的线程发送信号:pthread_kill()
A】函数原型:extern int pthread_kill(pthread_t __threadid,int __signo)
B】参数说明:threadid是目标线程,sigo是要发送的信号。pthread_kill()函数用于请求将信号传送给线程,调用进程中,信号将被异步定向到线程。如果signo为0,就会进行错误检查而不发送信号。成功完成后,pthread_kill()将返回0。否则会返回一个错误编号,用于指明错误(未设置errno变量)。
2、
调用线程的信号掩码:pthread_sigmask()
A】函数原型:extern int pthread_sigmask(int __how,__const __sigset_t *restrict __newmask,
__sigset_t *restrict __oldmask);
B】说明:函数类似于进程信号中的sigprocmask()函数。pthread_sigmask()用来检查或更改线程的信号掩码。(此部分可参考进程和进程通信 信号部分内容)。
C】参数:第一个参数how定义如何更改调用线程的信号掩码。其合法值有以下三个(此小点内容与sigprocmask()重复)
#define SIG_BLOCK
0
#define SIG_UNBLOCK 1
#define SIG_SETMASK
2
宏说明:
SIG_BLOCK:将第2个参数所描述的集合添加到当前进程阻塞的信号集中。
SIG_UNBLOCK:将第2个参数所描述的集合从当前进程阻塞信号集中删除。
SIG_SETMASK:不管之前的阻塞信号,仅设置当前的进程阻塞的集合为第2个参数描述的对象。
如果newmask值是个空指针,则参数how没有意义,且不会更改线程的阻塞信号集,因此该调用可以用于查询当前受阻塞的信号。
D】返回:执行成功后,返回0,失败后返回错误编号来指明错误(未设置errno变量),另外,如果由于某种原因pthread_sigmask()失败,那么线程的信号掩码将不会变化。
【附言】
上面提到的互斥锁的属性,条件变量的属性和读写锁的属性三块内容,在下一篇文章中整理。放置于线程和线程通信目录下,请关注。 http://blog.163.com/jimking_2010/blog/static/1716015352013102510748824/