Linux多线程(三)(同步互斥)

时间:2023-03-09 09:51:40
Linux多线程(三)(同步互斥)

1. 线程的同步与互斥

1.1. 线程的互斥

在Posix Thread中定义了一套专门用于线程互斥的mutex函数。mutex是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁),可以把互斥锁看作某种意义上的全局变量。为什么需要加锁,就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。

1. 创建和销毁锁

有两种方法创建互斥锁,静态方式和动态方式。

·静态方式:

POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

在Linux Threads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个宏常量。

·动态方式:

动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)

其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。通常为NULL

pthread_mutex_destroy()用于注销一个互斥锁,API定义如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此Linux Threads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

2. 互斥锁属性

互斥锁属性结构体的定义为:

typedef struct

{

int __mutexkind; //注意这里是两个下划线

} pthread_mutexattr_t;

互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性__mutexkind,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。有三个值可供选择:

·PTHREAD_MUTEX_TIMED_NP,这是缺省值(直接写NULL就是表示这个缺省值),也就是普通锁(或快速锁)。当一个线程加锁以后,其余请求锁的线程将形成一个阻塞等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性

示例:初始化一个快速锁。

pthread_mutex_t lock;

pthread_mutex_init(&lock, NULL);

·PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

示例:初始化一个嵌套锁。

pthread_mutex_t lock;

pthread_mutexattr_t mutexattr;

mutexattr.__mutexkind = PTHREAD_MUTEX_RECURSIVE_NP;

pthread_mutex_init(&lock, &mutexattr);

·PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。如果锁的类型是快速锁,一个线程加锁之后,又加锁,则此时就是死锁。

示例:初始化一个嵌套锁。

pthread_mutex_t lock;

pthread_mutexattr_t mutexattr;

mutexattr.__mutexkind = PTHREAD_MUTEX_ERRORCHECK_NP;

pthread_mutex_init(&lock, &mutexattr);

3.锁操作

锁操作主要包括

加锁 int pthread_mutex_lock(pthread_mutex_t *mutex)

解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex)

测试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex)

·pthread_mutex_lock:加锁,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

·pthread_mutex_unlock:根据不同的锁类型,实现不同的行为:

对于快速锁,pthread_mutex_unlock解除锁定;

对于递规锁,pthread_mutex_unlock使锁上的引用计数减1;

对于检错锁,如果锁是当前线程锁定的,则解除锁定,否则什么也不做。

·pthread_mutex_trylock:语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

Example:比较pthread_mutex_trylock()与pthread_mutex_lock()

#include <stdio.h>

#include <pthread.h>

pthread_mutex_t lock;

void* pthfunc(void *args)

{

pthread_mutex_lock(&lock); //先加一次锁

pthread_mutex_lock(&lock); //再用lock加锁,会挂起阻塞

//pthread_mutex_trylock(&lock); //用trylock加锁,则不会挂起阻塞

printf("hello\n");

sleep(1);

pthread_exit(NULL);

}

main()

{

pthread_t pthid = 0;

pthread_mutex_init(&lock,NULL);

pthread_create(&pthid,NULL,pthfunc,NULL);

pthread_join(pthid,NULL);

pthread_mutex_destroy(&lock);

}

4. 加锁注意事项

如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,则必须在退出回调函数pthread_cleanup_push/pthread_cleanup_pop中解锁。同时不应该在信号处理函数中使用互斥锁,否则容易造成死锁。

5. 互斥锁实例

Example:火车站售票(此处不加锁,则会出现卖出负数票的情况)

#include <stdio.h>

#include <pthread.h>

int ticketcount = 20; //火车票,公共资源(全局)

void* salewinds1(void* args) //售票口1

{

while(ticketcount > 0) //如果有票,则卖票

{

printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);

sleep(3); //卖一张票需要3秒的操作时间

ticketcount --; //出票

printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);

}

}

void* salewinds2(void* args) //售票口2

{

while(ticketcount > 0) //如果有票,则卖票

{

printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);

sleep(3); //卖一张票需要3秒的操作时间

ticketcount --; //出票

printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);

}

}

int main()

{

pthread_t pthid1 = 0;

pthread_t pthid2 = 0;

pthread_create(&pthid1,NULL,salewinds1,NULL); //线程1

pthread_create(&pthid2,NULL,salewinds2,NULL); //线程2

pthread_join(pthid1,NULL);

pthread_join(pthid2,NULL);

return 0;

}

Example:加锁之后的火车售票

#include <stdio.h>

#include <pthread.h>

int ticketcount = 20;

pthread_mutex_t lock;

void* salewinds1(void* args)

{

while(1)

{

pthread_mutex_lock(&lock); //因为要访问全局的共享变量,所以就要加锁

if(ticketcount > 0) //如果有票

{

printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);

sleep(3); //卖一张票需要3秒的操作时间

ticketcount --; //出票

printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);

}

else //如果没有票

{

pthread_mutex_unlock(&lock); //解锁

pthread_exit(NULL); //退出线程

}

pthread_mutex_unlock(&lock); //解锁

sleep(1); //要放到锁的外面,让另一个有时间锁

}

}

void* salewinds2(void* args)

{

while(1)

{

pthread_mutex_lock(&lock); //因为要访问全局的共享变量,所以就要加锁

if(ticketcount>0) //如果有票

{

printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);

sleep(3); //卖一张票需要3秒的操作时间

ticketcount --; //出票

printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);

}

else //如果没有票

{

pthread_mutex_unlock(&lock); //解锁

pthread_exit(NULL); //退出线程

}

pthread_mutex_unlock(&lock); //解锁

sleep(1); //要放到锁的外面,让另一个有时间锁

}

}

int main()

{

pthread_t pthid1 = 0;

pthread_t pthid2 = 0;

pthread_mutex_init(&lock,NULL); //初始化锁

pthread_create(&pthid1,NULL,salewinds1,NULL); //线程1

pthread_create(&pthid2,NULL,salewinds2,NULL); //线程2

pthread_join(pthid1,NULL);

pthread_join(pthid2,NULL);

pthread_mutex_destroy(&lock); //销毁锁

return 0;

}

总结:线程互斥mutex:加锁步骤如下:

1. 定义一个全局的pthread_mutex_t lock; 或者用

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //则main函数中不用init

2. 在main中调用 pthread_mutex_init函数进行初始化

3. 在子线程函数中调用pthread_mutex_lock加锁

4. 在子线程函数中调用pthread_mutex_unlock解锁

5. 最后在main中调用 pthread_mutex_destroy函数进行销毁

1.2. 线程的同步

6 条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

1、 创建和注销

条件变量和互斥锁一样,都有静态、动态两种创建方式:

静态方式使PTHREAD_COND_INITIALIZER常量,如下:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态方式调用pthread_cond_init()函数,API定义如下:

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

尽管POSIX标准中为条件变量定义了属性,但在Linux Threads中没有实现,因此cond_attr值通常为NULL,且被忽略。

注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:

int pthread_cond_destroy(pthread_cond_t *cond);

2、 等待和激发

等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait():

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

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

线程解开mutex指向的锁并被条件变量cond阻塞。其中计时等待方式表示经历abstime段时间后,即使条件变量不满足,阻塞也被解除。无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。(也就是说在做pthread_cond_wait之前,往往要用pthread_mutex_lock进行加锁,而调用pthread_cond_wait函数会将锁解开,然后将线程挂起阻塞。直到条件被pthread_cond_signal激发,再将锁状态恢复为锁定状态,最后再用pthread_mutex_unlock进行解锁)。

激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程

3、 其他

pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,也就是说如果pthread_cond_wait()被取消,则退出阻塞,然后将锁状态恢复,则此时mutex是保持锁定状态的,而当前线程已经被取消掉,那么解锁的操作就会得不到执行,此时锁得不到释放,就会造成死锁,因而需要定义退出回调函数来为其解锁。

以下示例集中演示了互斥锁和条件变量的结合使用,以及取消对于条件等待动作的影响。在例子中,有两个线程被启动,并等待同一个条件变量,如果不使用退出回调函数(见范例中的注释部分),则tid2将在pthread_mutex_lock()处永久等待。如果使用回调函数,则tid2的条件等待及主线程的条件激发都能正常工作。

实例:

#include <stdio.h>

#include <pthread.h>

#include <unistd.h>

pthread_mutex_t mutex;

pthread_cond_t cond;

void ThreadClean(void *arg)

{

pthread_mutex_unlock(&mutex);

}

void * child1(void *arg)

{

//pthread_cleanup_push(ThreadClean,NULL); //1

while(1){

printf("thread 1 get running \n");

printf("thread 1 pthread_mutex_lock returns %d\n", pthread_mutex_lock(&mutex));

pthread_cond_wait(&cond,&mutex); //等待父进程发送信号

printf("thread 1 condition applied\n");

pthread_mutex_unlock(&mutex);

sleep(5);

}

//pthread_cleanup_pop(0); //2

return 0;

}

void *child2(void *arg)

{

while(1){

sleep(3); //3

printf("thread 2 get running.\n");

printf("thread 2 pthread_mutex_lock returns %d\n", pthread_mutex_lock(&mutex));

pthread_cond_wait(&cond,&mutex);

printf("thread 2 condition applied\n");

pthread_mutex_unlock(&mutex);

sleep(1);

}

}

int main(void)

{

pthread_t tid1,tid2;

printf("hello, condition variable test\n");

pthread_mutex_init(&mutex,NULL);

pthread_cond_init(&cond,NULL);

pthread_create(&tid1,NULL,child1,NULL);

pthread_create(&tid2,NULL,child2,NULL);

while(1){ //父线程

sleep(2); //4

pthread_cancel(tid1); //5

sleep(2); //6

pthread_cond_signal(&cond);

}

sleep(10);

return 0;

}

不做注释1,2则导致child1中的unlock得不到执行,锁一直没有关闭,而child2中的锁不能执行lock,则会一直在pthread_mutex_lock()处永久等待。如果不做注释5的pthread_cancel()动作,即使没有那些sleep()延时操作,child1和child2都能正常工作。注释3和注释4的延迟使得child1有时间完成取消动作,从而使child2能在child1退出之后进入请求锁操作。如果没有注释1和注释2的回调函数定义,系统将挂起在child2请求锁的地方,因为child1没有释放锁;而如果同时也不做注释3和注释4的延时,child2能在child1完成取消动作以前得到控制,从而顺利执行申请锁的操作,但却可能挂起在pthread_cond_wait()中,因为其中也有申请mutex的操作。child1函数给出的是标准的条件变量的使用方式:回调函数保护,等待条件前锁定,pthread_cond_wait()返回后解锁。

条件变量机制和互斥锁一样,不能用于信号处理中,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。

Example:火车售票,利用条件变量,当火车票卖完的时候,再重新设置票数为10;

#include<pthread.h>

#include<stdio.h>

int ticketcount = 10;

pthread_mutex_t lock; //互斥锁

pthread_cond_t cond; //条件变量

void* salewinds1(void* args)

{

while(1)

{

pthread_mutex_lock(&lock); //因为要访问全局的共享变量ticketcount,所以就要加锁

if(ticketcount > 0) //如果有票

{

printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);

ticketcount --;//则卖出一张票

if(ticketcount == 0)

pthread_cond_signal(&cond); //通知没有票了

printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);

}

else //如果没有票了,就解锁退出

{

pthread_mutex_unlock(&lock);

break;

}

pthread_mutex_unlock(&lock);

sleep(1); //要放到锁的外面

}

}

void* salewinds2(void* args)

{

while(1)

{

pthread_mutex_lock(&lock);

if(ticketcount > 0)

{

printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);

ticketcount --;

if(ticketcount == 0)

pthread_cond_signal(&cond); //发送信号

printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);

}

else

{

pthread_mutex_unlock(&lock);

break;

}

pthread_mutex_unlock(&lock);

sleep(1);

}

}

void *setticket(void *args) //重新设置票数

{

pthread_mutex_lock(&lock); //因为要访问全局变量ticketcount,所以要加锁

if(ticketcount > 0)

pthread_cond_wait(&cond,&lock); //如果有票就解锁并阻塞,直到没有票就执行下面的

ticketcount = 10; //重新设置票数为10

pthread_mutex_unlock(&lock); //解锁

sleep(1);

pthread_exit(NULL);

}

main()

{

pthread_t pthid1,pthid2,pthid3;

pthread_mutex_init(&lock,NULL); //初始化锁

pthread_cond_init(&cond,NULL); //初始化条件变量

pthread_create(&pthid1,NULL, salewinds1,NULL); //创建线程

pthread_create(&pthid2,NULL, salewinds2,NULL);

pthread_create(&pthid3,NULL, setticket,NULL);

pthread_join(pthid1,NULL); //等待子线程执行完毕

pthread_join(pthid2,NULL);

pthread_join(pthid3,NULL);

pthread_mutex_destroy(&lock); //销毁锁

pthread_cond_destroy(&cond); //销毁条件变量

}