5. Linux线程同步

目录

5.1 互斥锁

5.1.1 初始化和销毁

5.1.2 Lock和Unlock

5.1.3 互斥量属性

5.1.4 例程

5.2 读写锁

5.2.1 读写锁初始化

5.2.2 读写锁的lock与unlock

5.2.3 超时读写锁

5.2.4 读写锁属性

5.3 条件变量

5.3.1 初始化

5.3.2 Wait

5.3.3 Signal

5.3.4 条件变量属性

5.4 自旋锁

5.5 屏障

5.1 Barrier的使用

5.2 Barrier属性


Linux 线程间共享进程的地址空间和资源,于是线程间就可能因为竞争这些资源而产生问题。Linux线程由多种同步方式来解决这类竞争问题,下面我们将介绍最常用的几种:互斥锁,读写锁,条件变量,自旋锁,屏障。Linux 线程间共享进程的地址空间和资源,于是线程间就可能因为竞争这些资源而产生问题。Linux线程由多种同步方式来解决这类竞争问题,下面我们将介绍最常用的几种:互斥锁,读写锁,条件变量,自旋锁,屏障。

5.1 互斥锁

互斥锁类似于进程中的二值信号量,用来保证同一时间内某个资源只能被一个线程占用。使用方式就是:线程在使用某个资源前对其加锁,后续企图使用这个资源的线程阻塞;当前线程使用完后释放资源,第一个开始阻塞的线程占用该资源,以此类推。Pthread线程库中提供了一组API来完成互斥锁的操作。

5.1.1 初始化和销毁

互斥锁的初始化有静态初始化和动态初始化两种。使用动态初始化时,在内存释放前需要先销毁该互斥锁。互斥变量用pthread_mutex_t表示,静态初始化就是将其设置成PTHREAD_MUTEX_INITIALIZER。动态初始化和销毁可以由下面的函数完成:

#inlcude<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
//执行成功返回0,失败返回错误编号

5.1.2 Lock和Unlock

#inlcude<pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_trylock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
//执行成功返回0,失败返回错误编号

Trylock和 lock的区别在于lock函数调用lock函数的线程会被阻塞,但是调用trylock的函数如果申请不到资源会直接返回EBASY。

5.1.3 互斥量属性

在初始化互斥锁前,我们还可以指定互斥锁的属性,主要有三种,实际上这三种属性是也是线程同步属性的共性,它们分别是线程共享属性,健壮性属性,类型属性。因为Linux系统只支持第三种类型属性,所以这里我们只详细介绍下类型属性。

在设置互斥锁属性之前,我们先要对一个属性对象进行初始化,使用完之后需要反初始化:

#inlcude<pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t * mutex);
//执行成功返回0,失败返回错误编号
  1. 进程共享属性

前面有介绍过进程可以通过共享内存的方式进行通信。在共享段中的数据,我们也可以通过互斥量来保证同步。

  • PTHREAD_PROCESS_PRIVATE:只能实现多一个进程中多个线程之间的同步;
  • PTHREAD_PROCESS_SHARED:通过在共享段中创建互斥量,可以实现多个进程中的线程实现同步。

共享属性可以通过下面的一组函数去设置和获取:

#inlcude<pthread.h>
int pthread_mutexattr_getpshared(cosnt pthread_mutexattr_t *attr, int *restrict pshared);
int pthread_mutexattr_setpshared(cosnt pthread_mutexattr_t *attr, int restrict pshared);
//执行成功返回0,失败返回错误编号

2. 健壮属性

线程健壮属性也与多进程间同步有关。当持有互斥量的进程突然终止时,其它进程中等待该互斥量而阻塞的线程会是什么行为呢?健壮属性就是用来处理这种情况:

  • PTHREAD_MUTEX_STALLED: 持有互斥量进程终止时,不采取任何行为,这种情况下再去使用互斥量的行为是未定义的,等待该互斥量的线程可能会被有效拖住;
  • PTHREAD_MUTEX_ROBUST: 持有互斥量进程终止时,其它进程再试图去lock时会直接返回EOWNERREAD,这样我们就可以针对这种情况去对这个互斥量做恢复状态的操作。

我们可以用下面的这组函数来设置健壮属性:

#inlcude<pthread.h>
int pthread_mutexattr_getpRobust(cosnt pthread_mutexattr_t *attr, int *restrict robust);
int pthread_mutexattr_setpRobust(cosnt pthread_mutexattr_t *attr, int restrict robust);
//执行成功返回0,失败返回错误编号

设置可Robust属性,当线程lock时接收到了EOWNERREAD返回值,我们就可以知道是由于之前上锁的进程异常终止而没有unlocl了。此时我们可以在本线程的unlock前 调用以下的函数,此后这个互斥量就又可以正常使用了。

#inlcude<pthread.h>
int pthread_mutex_consistent(pthread_mutex_t * mutex);
//执行成功返回0,失败返回错误编号

3. 类型属性

互斥量的类型属性主要是针对以下几种情况:同一线程对同一互斥锁没有解锁前重新加锁;线程对另一个线程加锁的互斥量进行解锁(不占用解锁);线程对已经解锁的互斥量进行解锁。我们先来看下有几种类型属性:

  • PTHREAD_MUTEX_NORMAL:标准类型,不做任何特殊的错误检查或这死锁检测;
  • PTHREAD_MUTEX_ERRORCHECK: 提供错误检查
  • PTHREAD_MUTEXT_RECURSIVE:允许同一线程对同一互斥量递归加锁,当然也需要相应次数的解锁后,该互斥锁才算释放;
  • PTHREAD_MUTEX_DEFAULT:不同的操作系统可以将其映射成上述三种之一,linux下等同于NORMAL类型。

同样的,Pthread线程库对设置和获取类型属性也定义了一组函数:

#inlcude<pthread.h>
int pthread_mutexattr_getptype(cosnt pthread_mutexattr_t *attr, int *restrict type);
int pthread_mutexattr_settype(cosnt pthread_mutexattr_t *attr, int restrict type);
//执行成功返回0,失败返回错误编号

有些讲解中有提到健壮性和类型信息,在这些宏定义后都含有_np的字样,那是glibc2.4引入的,现在已经基本上废弃不用了。

5.1.4 例程

本节中我们再来简单改编下APUE书中图12-8的程序示例,该示例使用递归类型的互斥量保证了线程间数据的同步。这个例程是在main函数中分别设置两个10s 和 5s 的定时器,让两个线程分别等待10s和5s后执行入口函数。

需要注意的是:在mian函数中执行完创建线程的函数后,一定要有等待线程执行完之后的操作,否则,进程一旦执行完先退出,线程会自动退出,无法再执行。

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include<unistd.h>
#define err_exit(err,string) printf("%d %s\n",err,string);

int makethread(void *(*fn)(void *), void *arg)
{
	int err;
	pthread_t tid;
	pthread_attr_t attr;
	
	err = pthread_attr_init(&attr);
	if(err != 0)
	{
		printf("make thread init attr fail\n");
		return err;
	}
	
	err = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
	if(err != 0)
	{
		printf("make thread init set detach fail\n");
		return err;
	}
	
	err = pthread_create(&tid, &attr, fn, arg);
	if(err != 0)
	{
		printf("make thread creat thread fail\n");
		return err;
	}
	
	pthread_attr_destroy(&attr);

	return err;
}

struct to_info {
	void	      (*to_fn)(void *);	/* function */
	void           *to_arg;			/* argument */
	struct timespec to_wait;		/* time to wait */
};

#define SECTONSEC  1000000000	/* seconds to nanoseconds */

#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM)	nanosleep((REQ), (REM))
#endif

#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000		/* microseconds to nanoseconds */

void
clock_gettime(int id, struct timespec *tsp)
{
	struct timeval tv;

	gettimeofday(&tv, NULL);
	tsp->tv_sec = tv.tv_sec;
	tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif

void *
timeout_helper(void *arg)
{
	struct to_info	*tip;
	tip = (struct to_info *)arg;
	
	clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
	 
	(*tip->to_fn)(tip->to_arg);
	free(arg);
	return(0);
}

void
timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
	struct timespec	now;
	struct to_info	*tip;
	int				err;

	clock_gettime(CLOCK_REALTIME, &now);
	if ((when->tv_sec > now.tv_sec) ||
	  (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) {
		tip = malloc(sizeof(struct to_info));
		if (tip != NULL) {
			tip->to_fn = func;
			tip->to_arg = arg;
			tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
			if (when->tv_nsec >= now.tv_nsec) {
				tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
			} else {
				tip->to_wait.tv_sec--;
				tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec +
				  when->tv_nsec;
			}
			err = makethread(timeout_helper, (void *)tip);
			if (err == 0)
				return;
			else
				free(tip);
		}
	}

	/*
	 * We get here if (a) when <= now, or (b) malloc fails, or
	 * (c) we can't make a thread, so we just call the function now.
	 */
	 printf("Prepare to excute\n");
	(*func)(arg);
}

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

void 
func1(void *arg)
{
	pthread_mutex_lock(&mutex);

	printf("function 1 excute\n");

	pthread_mutex_unlock(&mutex);
}

void
func2(void *arg)
{
	pthread_mutex_lock(&mutex);

	printf("function 2 excute\n");

	pthread_mutex_unlock(&mutex);
}

int main(void)
{
	int		err, condition, arg;
	struct timespec	when;
	
	if ((err = pthread_mutexattr_init(&attr)) != 0)
		err_exit(err, "pthread_mutexattr_init failed");
	if ((err = pthread_mutexattr_settype(&attr,
	  PTHREAD_MUTEX_RECURSIVE)) != 0)
		err_exit(err, "can't set recursive type");
	if ((err = pthread_mutex_init(&mutex, &attr)) != 0)
		err_exit(err, "can't create recursive mutex");

	/* continue processing ... */

	pthread_mutex_lock(&mutex);

	/*
	 * Check the condition under the protection of a lock to
	 * make the check and the call to timeout atomic.
	 */
	condition = 1;
	if (condition) {
		/*
		 * Calculate the absolute time when we want to func1.
		 */
		clock_gettime(CLOCK_REALTIME, &when);
		when.tv_sec += 10;	/* 10 seconds from now */
		timeout(&when, func1, (void *)((unsigned long)arg));

		
		when.tv_sec -= 5;	/* 5 seconds from now */
		timeout(&when, func2, (void *)((unsigned long)arg));
		condition = 0;
	}
	pthread_mutex_unlock(&mutex);

	/* continue processing ... */

	sleep(20);

	exit(0);
}

 

5.2 读写锁

读写锁类似于互斥锁,但是它允许更高的并行性。读写锁允许多个线程同时拥有读锁,但是只有一个线程可以由写锁,即读和写时互斥的,但是读和读之间并不是互斥。这种锁非常适用于对数据结构读次数大于写的情况。读写锁对应的数据结构时pthread_rwlock_t

5.2.1 读写锁初始化

#inlcude<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const phread_rwlockattr_t *attr);
int pthread_ rwlock _destroy(pthread_rwlock_t *rwlock);
//执行成功返回0,失败返回错误编号

类似于互斥锁的初始化有动态和静态两种方式,读写锁的初始化也有静态和动态两种。上面的函数提供了动态初始化和反初始化的方法,我们也可以直接将其静态的设置为PTHREAD_RWLOCK_INITIALIZER.

5.2.2 读写锁的lock与unlock

#inlcude<pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//执行成功返回0,失败返回错误编号

通过以上的函数我们分别可以进行读加锁、写加锁以及解锁。当加锁失败时会阻塞中,当然我们也提供了一组trylock,当加锁失败时不会阻塞,而是返回EBUSY。

5.2.3 超时读写锁

#include<pthread.h>
#include<time.h>
int pthread_rwlock_timerdlock(pthread_rwlock_t *rwlock, const struct timespec *tpsr);
int pthread_rwlock_timewrlock(pthread_rwlock_t *rwlock, const struct timespec *tpsr);
//执行成功返回0,失败返回错误编号

超时读写锁会在加锁失败后的指定时间段内直接返回。

5.2.4 读写锁属性

读写锁只支持进程的共享属性,原理同互斥信号量相同,这里就不再过多介绍。读写锁的进程共享属性的初始化,设置分别由下面两组函数实现。

#include<pthread.h>
int pthread_rwlockattr_init(pthread_rwattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwattr_t *attr);

int pthread_rwlockattr_getpshared(cosnt pthread_ rwlockattr _t *attr, int *restrict pshared);
int pthread_ rwlockattr _setpshared(cosnt pthread_ rwlockattr _t *attr, int restrict pshared);
//执行成功返回0,失败返回错误编号

 

5.3 条件变量

条件变量是另一种线程同步机制,通常与互斥量一起使用,允许线程以无竞争的方式等待条件的发生。可以分两点进行说明:

为什么需要条件变量?我们考虑下面的情况,线程A和线程B都需要获取同一个资源,但是线程A又需要线程B执行到某一步,才能继续执行。这时候如果先执行线程A,lock住这个资源;那线程B就无法获取该资源,这样A,B两个线程就可能都会阻塞住。我们看下实现这种思想的伪代码:

pthread_mutex_lock(&mutex)
while(condition is false)
{
	pthread_mutex_unlock(&mutex)
	wait(conditon to true);
pthread_mutex_lock(&mutex)
}

这里的condition我们就可以用条件变量来实现。

     那为什么需要同互斥量一起使用呢?上面的程序中如果在unlock之后线程被切走,其它的线程执行了 conditon to true。这时此线程还没有执行到wait函数,也就永远没有机会再等待条件满足,此线程会一直阻塞。这时如果unlock wait是原子操作就可以避免这个问题。

5.3.1 初始化

#inlcude<pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const phread_cond_t *attr);
int pthread_ cond _destroy(pthread_cond_t * cond);
//执行成功返回0,失败返回错误编号

同样,条件变量也可以直接被静态初始化为PTHREAD_COND_INITIALIZER。

5.3.2 Wait

#inlcude<pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, const phread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, const phread_mutex_t *mutex,      const struct timespec * tsptr);
//执行成功返回0,失败返回错误编号

Wait函数其实就是上面介绍的unlock 和wait 的原子操作。Timedwait 多了个超时功能,到时间到了之后,wait函数就不会再等待。此时timewait函数将重新获取互斥量,并返回ETIMEOUT。需要注意的是在调用wait之前一定要先对mutex加锁。

5.3.3 Signal

#inlcude<pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//执行成功返回0,失败返回错误编号

Signal函数至少可以唤醒一个等待该条件的线程,broadcast则会唤醒所有等待该条件的线程。

5.3.4 条件变量属性

条件变量属性有共享进程属性和时钟属性两种。共享进程属性同其他同步机制一样,不做过多介绍。时钟属性控制timedwait函数采用哪个时钟。条件变量属性对应的数据结构时:pthread_condattr_t。

  1. 条件变量属性初始化
#include<pthread.h>
int pthread_condattr_init(pthread_condattr_t * attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
//执行成功返回0,失败返回错误编号

    2. 共享进程属性

#inlcude<pthread.h>
int pthread_condattr_getpshared(cosnt pthread_mutexattr_t *attr, int *restrict pshared);
int pthread_condattr_setpshared(cosnt pthread_mutexattr_t *attr, int restrict pshared);
//执行成功返回0,失败返回错误编号

     3. 时钟属性

#inlcude<pthread.h>
int pthread_condattr_getclock(cosnt pthread_condattr_t *attr,clockid_t *clock_id);
int pthread_condattr_setclock(cosnt pthreadcondattr_t *attr, int restrict clock_id);
//执行成功返回0,失败返回错误编号

可选的时钟属性有以下几种:

CLOCK_REALTIME

实时系统时间

CLOCK_MONOTONIC

不带负跳数的实时系统时间

CLOCK_PROCESS_CPUTIME_ID

调用进程的CPU时间

CLOCK_THREAD_CPUTIME_ID

调用线程的CPU时间

5.4 自旋锁

自旋锁和互斥锁类似,都是用来保证同一时刻只有一个调用者可以访问某个资源。但自旋锁和互斥锁的调度机制不一样,对已经加锁的情况,另一个调用者再企图去加锁,如果是互斥锁,该调用者会阻塞,但自旋锁不会,它会一直在那里轮训,直到其它调用者解锁。所以自旋锁其实不是用于多线程并发,而是用于多处理器并发

鉴于自旋锁的特性,它通常被用在内核中,而不是用户层。内核中常用的情景是中断处理程序,在非抢占式的内核中,中断处理程序一般不能休眠,所以自旋锁通常是其唯一可用的同步原语。

如果用在多线程编程中,必须是在非抢占式调度的系统内。如果是这种场景下,线程被切走,此时该线程进入阻塞队列,如果还有没解锁的自旋锁,那阻塞在锁上的其它线程自旋的时间可能就会超过预期。

     自旋锁只有进程共享属性,它的一些常用的操作接口如下:

#inlcude<pthread.h>
int pthread_spin_init(pthread_ spinlock _t *restrict lock,int pshared);
int pthread_ spin _destroy(pthread_ spinlock _t * lock);

int pthread_spin_lock(pthread_ spinlock _t *restrict lock);
int pthread_spin_trylock(pthread_ spinlock _t *restrict lock);
int pthread_spin_unlock(pthread_ spinlock _t *restrict lock);
//执行成功返回0,失败返回错误编号

对于自旋锁还有两点需要说明:

  1. 同一调用者递归对自旋锁加锁必定会引起死锁;
  2. 自旋时时一直占用CPU的,所以自旋锁一般只适用于占用资源时间很短的场景。

5.5 屏障

屏障的原理是让很多线程同时执行到某个点,才能继续执行下去。即每个线程都运行到了设置屏障的地方,才能继续执行。

5.1 Barrier的使用

初始化的时候需要制定count数,只有count数目的线程都执行到了屏障的地点,所有的线程才能继续执行下去。最后一个执行wait 函数的线程返回值为PTHEAD_BARRIER_SERIAL_THREAD,其它线程返回值都是0。此时count计数被reset 成0。该barrier对象可以继续使用。Barrier对应的数据结构是pthread_barrier_t。

#inlcude<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier, pthread_barrierattr_t *attr,                      
                         unsigned int count);
int pthread_ barrier_destroy(pthread_ barrier_t * barrier);

int pthread_barrier_wait(pthread_barrier_t *barrier);

5.2 Barrier属性

Barrier也只有进程共享属性,其对应的数据结构是:pt

#include<pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t * attr);
int pthread_condattr_destroy(pthread_barrierattr_t *attr);

int pthread_barrierattr_getpshared(cosnt pthread_barrierattr_t *attr, int *restrict pshared);
int pthread_barrierattr_setpshared(cosnt pthread_barrierattr_t *attr, int pshared);
//执行成功返回0,失败返回错误编号

ead_barrierattr_t。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值