线程最大的特点就是资源的共享性,然而资源共享中的同步问题是多线程编程的难点.Linux 系统提供了多种方式处理线程间同步问题,下面介绍几个.
一.互斥锁
1.怎样实现线程间的同步?
通过锁机制.在同一时刻,它通常只允许一个线程执行一个关键部分的代码.这样可以保证数据的正确性和每个操作的完整性.
2.互斥锁函数
A. pthread_mutex_init函数 //初始化一个互斥锁
两种方式来进行初始化,一种方式是静态赋值法,方法如下:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITLALIZER; 其中 PTHREAD_MUTEX_INITLALIZER 是一个结构常量.
另一种方式通过此函数来进行初始化,函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
函数中的参数mutexattr表示互斥锁的属性,如果为NULL表示默认属性.下面在介绍互斥锁的属性.
B.加锁函数. pthread_mutex_lock函数和pthread_mutex_trylock函数
初始化以后,就可以给互斥锁加锁了.加锁函数原型如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
它们俩个加锁函数区别如下:
用pthread_mutex_lock()加锁时,如果mutex已经被锁住,当前尝试加锁的进程就会阻塞,直到互斥锁被其他线程释放.当pthread_mutex_lock()函数返回时,说明当前线程已经成功将该互斥锁加锁.
用pthread_mutex_trylock()函数进行加锁时,如果mutex已经被锁住,它不回阻塞等待,而是立即返回,返回的错误代码为EBUSY(资源正在使用,不能共享).
注意: 加锁时,无论哪种类型的锁,都不可能被两个不同的线程同时得到,其中一个必须等待解锁.同一进程中的线程如果加锁后没有解锁,其他线程将无法获得改锁.即会造成死锁(当一个线程终止时,如果不释放其占有的临界资源, 则该资源会被认为还被已经退出的线程所使用,因而永远不回得到释放.如果一个线程在等待使用这个资源,它就可能无限的等待下去,这就形成了死锁).为了防止死锁,linux系统提供了一对函数:pthread_cleanup_push()和 pthread_cleanup_pop()用于自动释放资源.从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序 段中的终止动作都将执行pthread_cleanup_push()指定的清理函数.这俩个函数是以宏形式提供的:
#define pthread_cleanup_push(routine,arg)
{ struct _pthread_cleanup_buffer buffer;
_pthread_cleanup_push(&buffer,(routine),(srg));
#define pthread_cleanup_pop _pthread_cleanup_pop(&buffer,(exeute));
}
这两个函数必须成对出现,且必须位于程序的同一代码段中才能通过编译.
C.解锁函数 pthread_mutex_unlock 函数.
函数原型如下: int pthread_mutex_unlock(pthread_mutex_t *mutex);
用pthread_mutex_unlock()解锁需满足一下两个条件:
>>互斥锁必须处于加锁状态
>>调用本函数的线程必须是给此互斥锁加锁的线程.
D.清除互斥锁函数 pthread_mutex_destroy函数
当一个互斥锁使用完毕后,必须进行清除操作. 该函数原型如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
清除一个互斥锁意味着释放它所占用的资源.清除锁时要求当前处于开放状态.若锁处于锁定状态,则返回 EBUSY.成功时返回0.由于在linux中,互斥锁并不占用内存,因此此函数除了解除到斥锁的状态以外没有其他操作.
3.互斥锁的属性
PTHREAD_MUTEX_TIMED_NP
普通锁,当一个线程加锁后,其余请求锁的线程形成等待队列,解锁后按优先级获得锁.
PTHREAD_MUTEX_RECURSIVE_NP
嵌套锁,允许同一个线程对同一个锁多次加锁,并多次通过unlock解锁,如果是不同线程请求,则在 解锁时重新争.
PTHREAD_MUTEX_ERRORCHECK_NP
检错锁,在同一个线程请求同一个锁的情况下,返回EDEADLK,否则执行的动作与普通锁相同.
PTHREAD_MUTEX_ADAPTIVE_NP
适应锁.解锁后,重新竞争.
下面举个类子说明下:
实现功能,对一个数据的读写操作.
- #include<stdio.h>
- #include<pthread.h>
- #include<stdlib.h>
- pthread_mutex_t number_mutex;
- int globalnumber=0;
- void write_globalnumber()
- {
- pthread_cleanup_push(pthread_mutex_unlock,&number_mutex); //note 1
- while(1)
- {
- sleep(2);
- pthread_mutex_lock(&number_mutex);
- globalnumber++;
- printf("in function write_globalnumber the globalnumber is :%d\n",globalnumber);
- pthread_mutex_unlock(&number_mutex);
- }
- pthread_cleanup_pop(0); //note 2
- }
- void read_globalnumber()
- {
-
- while(1)
- {
- sleep(5);
- int temp;
- pthread_mutex_lock(&number_mutex);
- temp=globalnumber;
- pthread_mutex_unlock(&number_mutex);
- printf("In function read_globalnumber the globalnumber is : %d\n",temp);
- }
- }
- int main(void)
- {
- int temp;
- pthread_t thid1,thid2;
- printf("mutex variable study!\n");
- pthread_mutex_init(&number_mutex,NULL);
- pthread_create(&thid1,NULL,(void *)write_globalnumber,NULL);
- pthread_create(&thid2,NULL,(void *)read_globalnumber,NULL);
- sleep(50); //note 3
- do
- {
- sleep(10);
- pthread_cancel(thid1); //note 4
- }while(1);
- return 0;
- }
上面代码中实现了如何通过互斥锁来保护全局变量.两个函数对共享全局变量globalnumber进行读写操作,write_globalnumber函数使用互斥锁保证在修改变量的时候操作一次执行完毕.而read_globalnumber函数使用互斥锁保证在读数据的时候,全局变量globalnumber不会被修改,确保读到数据的正确性.
note1和note2是回调函数保护.防止死锁.虽然你把它注释掉没有太大影响,但是我认为,这是一种好的习惯吧. note3让主线程睡上50妙,以使thid1和thid2 线程有时间执行它们的操作.
二.条件变量
1.条件变量是通过什么操作来保证线程间的同步?
条件变量是利用线程间共享的全局变量进行同步的一种机制.它其实类似于if语句,符合条件就执行某段程序,否则等待条件成立.
它主要包涵两个动作
>>一个等待使用资源的线程等待"条件变量被设置为真"
>>一个线程使用资源后"设置条件变量为真"
存在问题: 要保证条件变量能被正确的修改,所以他要受到特殊保护,所以条件变量一般都是和互斥锁共同使用的.
2.条件变量操作的函数
A.初始化函数
pthread_cond_init 函数 初始化条件变量.
函数原型: int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
其中cond_attr参数是条件变量的属性,其并没有得到实现,所以它的值通常为NULL.
还有一种静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER 赋予互斥锁.
B.等待条件成立函数.
>>pthread_cond_wait 函数.函数原型如下:
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
pthread_cond_wait 函数释放由mutex指向的互斥锁,同时使当前线程关于条件变量阻塞,直到条件被信号唤醒.
>>pthread_cond_timedwait函数,原型如下:
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime)
pthread_cond_timedwait函数和pthread_cond_wait 函数用法类似.区别在于:pthread_cond_timedwait函数将阻塞 直到条件变量获得信号换成经过有abstime指定的时间.
C.解除阻塞函数
线程被条件变量阻塞后,可通过以下两个解除阻塞函数来激活.
>>pthread_cond_signal函数.原型如下
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal()激活一个等待条件成立的线程,如果有多个等待线程,则按入队顺序激活其中一个.
>>pthread_cond_broadcast 函数 ,原型如下
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_broadcast()激活所有等待线程.
D.清除条件变量函数
pthread_cond_destroy函数,原型如下:
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy函数清除由cond指向的条件变量.
注意:只有再没有线程等待该条件变量的时候才能清除这个条件变量,否则返回EBUSY.
下面举个类子说明下:
- #include<stdio.h>
- #include<unistd.h>
- #include<pthread.h>
- pthread_mutex_t mutex;
- pthread_cond_t cond;
- void * thread1(void *arg)
- {
- pthread_cleanup_push(pthread_mutex_unlock,&mutex); //*******note 1****
- while(1)
- {
- printf("thread1 is running!\n");
- pthread_mutex_lock(&mutex);
- pthread_cond_wait(&cond,&mutex);
- printf("thread1 applied the condition!\n");
- pthread_mutex_unlock(&mutex);
- sleep(4);
- }
- pthread_cleanup_pop(0); //***********note 2******
- }
- void *thread2(void *arg)
- {
- while(1)
- {
- sleep(5);
- printf("thread2 is running!\n");
- pthread_mutex_lock(&mutex);
- pthread_cond_wait(&cond,&mutex);
- printf("thread2 applied the condition!\n");
- pthread_mutex_unlock(&mutex);
- sleep(2);
- }
- }
- int main(void)
- {
- pthread_t tid1,tid2;
- printf("**************condition variable study !*********************\n");
- pthread_mutex_init(&mutex,NULL);
- pthread_create(&tid1,NULL,(void *)thread1,NULL);
- pthread_create(&tid2,NULL,(void *)thread2,NULL);
-
- do
- {
- sleep(5);
- pthread_cancel(tid1); //************note 3*******
- pthread_cond_signal(&cond); //*************note 4*******
- }while(1);
- sleep(20);
- pthread_exit(0);
- return 0;
- }
上面这个类子中,有两个线程被启动,并等待同一个条件变量.这个如果注释掉回调函数,就会产生死锁.(即注释掉note1 和note2) ,原因在于,程序执行到note3的时候 ,会忽略掉线程tid1,而此时线程tid1以对此条件变量加锁,并且调用pthread_cond_wait()函数处于阻塞状态,所以如果不调用回调函数,就不会释放共享资源(即条件变量值不会被改变),所以会产生死锁. note5是激活等待条件成立的线程.
除了上述讨论的同步方式以外,还有异步信号.其他很多进程间通信手段对于LinuxThreads也是可用的,比如基于文件系统的IPC(管道、Unix域Socket等)、消息队列(Sys.V或者Posix的)、System V的信号灯等。只有一点需要注意,LinuxThreads在核内是作为共享存储区、共享文件系统属性、共享信号处理、共享文件描述符的独立进程看待的。这些大家下去自己研究,我就介绍这么多.