线程之线程同步

风流意气都作罢 提交于 2020-03-01 12:51:01

本文来自个人博客:https://dunkwan.cn

线程同步

互斥量

互斥量从本质上来说是一把锁,在访问共享资源前对互斥量进行设置,在访问完成后释放互斥量。互斥量使用pthread_mutex_t数据类型表示的。在使用互斥量以前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如通过调用malloc函数),在释放内存前需要调用pthread_mutex_detroy

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
两个函数的返回值:若成功,返回0;否则,返回错误编号。

要用默认的属性初始化互斥量,只需把attr设为NULL

对互斥量进行加锁,需要调用pthread_mutex_lock。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量加锁,需要调用pthread_mutex_unlock

#include <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;否则,返回错误编码。

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞直接返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY

函数pthread_mutex_timedlock

当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock互斥量原语允许绑定线程阻塞时间。pthread_mutex_timedlockpthread_mutex_lock是基本等价的。但是在到达超时时间值时,pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEDOUT

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
返回值:若成功,返回0;否则,返回错误编号。

指定愿意等待的绝对时间。这个超时时间使用timespec结构来表示的,它用秒和纳秒来描述时间。

测试示例:

如何用pthread_mutex_timedout避免永久阻塞。

#include "../../include/apue.h"
#include <pthread.h>

int main(void)
{
    int err;
    struct timespec tout;
    struct tm *tmp;
    char buf[64];
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&lock);
    printf("mutex is locked\n");
    clock_gettime(CLOCK_REALTIME, &tout);
    tmp = localtime(&tout.tv_sec);
    strftime(buf, sizeof(buf), "%r", tmp);
    printf("current time is %s\n", buf);
    tout.tv_sec += 10;

    err = pthread_mutex_timedlock(&lock, &tout);
    clock_gettime(CLOCK_REALTIME, &tout);
    tmp = localtime(&tout.tv_sec);
    strftime(buf, sizeof(buf), "%r", tmp);
    printf("the time is now %s\n", buf);
    if(err == 0)
        printf("mutex locked again!\n");
    else
        printf("can't lock mutex again: %s\n", strerror(err));
    return 0;
}

结果如下:

读写锁

读写锁与互斥量类似,不过读写锁允许更高的并发性。互斥量要么是锁住状态,要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

读写锁又被称为共享互斥锁。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它是写模式锁住时,就可以说是以互斥模式锁住的。与互斥量一样,读写锁在使用前必须初始化,在释放它们底层的内存之前必须销毁。

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict relock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
两个函数的返回值:若成功,返回0;否则,返回错误编码。

attr如果为null指针,则读写锁有默认的属性。

pthread_rwlock_rdlock在读模式下锁定读写锁,pthread_rwlock_wrlock在写模式下锁定读写锁,pthread_rwlock_unlock进行解锁。

#include <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);
所有函数的返回值:若成功,返回0;否则,返回错误编号。

读写锁的条件版本。

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
两个函数返回值:若成功,返回0;否则,返回错误编号。

带有超时的读写锁

与互斥量一样,读写锁也拥有自己的超时的读写锁加锁函数,该函数可以避免引用程序在获取读写锁时陷入永久的阻塞状态。

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);
两个函数的返回值:若成功,返回0;否则,返回错误编码。

pthread_mutex_timedlock类似,超时指定的是绝对时间。

条件变量

条件变量是线程可用的一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量和互斥量一起使用时,允许线程以无竞争方式等待特定的条件发生。

条件本身是由互斥量保护,线程在改变状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

在使用条件变量之前必须进行初始化。然而条件变量的初始化又分为两种方式,第一种是静态分配的方式,即使用常量PHTREAD_COND_INITIALIZER赋给静态分配的条件变量;第二种是动态分配的方式,这种方式需要使用pthread_cond_init函数对它进行初始化。在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化。

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
两个函数返回值:若成功,返回0;否则,返回错误编码。

使用pthread_cond_waitpthread_cond_timedwait函数等待条件变量为真。

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
两个函数返回值:若成功,返回0;否则,返回错误编码。

pthread_cond_wait的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。

pthread_cond_timedwait函数只是多了一个超时,超时值指定了我们愿意等待多长时间。这个等待的时间值需是绝对值。要等到超时值的绝对时间,可以使用下面的函数。

#include <sys/time.h>
#include <stdlib.h>
void maketimeout(struct timespec *tsp, long minutes)
{
    struct timeval now;
    /* get the current time */
    gettimeofday(&now, NULL);
    tsp->tv_sec = now.tv_sec;
    tsp->tv_nsec = now.tv_usec * 1000;
    /* add the offset to get timeout value */
    tsp->tv_sec += minutes * 60;
}

如果超时到期时条件还未出现,pthread_cond_timewait将重新获取互斥量,然后返回错误ETIMEDOUT,从pthread_cond_wait或者pthread_cond_timedwait调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经运行并改变了条件。

pthread_cond_signalpthread_cond_broadcast函数用于通知条件已经满足。pthread_cond_signal函数至少唤醒一个以上的等待该条件的线程,而pthread_cond_broadcast函数唤醒所有等待该条件的线程。

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
两个函数的返回值:若成功,返回0;否则,返回错误编码。

注意:一定是在条件状态发生改变以后才给线程发送信号。

自旋锁

自旋锁可用于以下几种情形:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。

pthread_spin_init函数用于自旋锁的初始化,pthread_spin_destroy用于进行自旋锁的反初始化。

#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
两个函数的返回值:若成功,返回0;否则,返回错误编码。

只有一个属性是自旋锁特有的,pshared这个属性只在支持线程进程同步的平台上才用得到。它表示进程共享属性,表明自旋锁如何获得。如果它设为PTHREAD_PROCESS_SHARED,则自旋锁能被可以访问所锁底层内存的线程获取,即便那些线程属于不同的进程,情况也如此。否则pshared参数设为PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁进程内部的线程所访问。

pthread_spin_lockpthread_spin_trylock对自旋锁加锁,前者在获取锁之前一直自旋,后者如果不能获取锁,就立即返回EBUSY错误。pthread_spin_unlock用于解锁。

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
所有函数的返回值:若成功,返回0;否则,返回错误编码。

注意:不要调用在持有自旋锁情况下可能进入休眠状态的函数。如果调用了这些函数,会浪费CPU资源,因为其他线程需要获取自旋锁需要等待的时间就延长了。

屏障

屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。

pthread_barrier_initpthread_barrier_destroy分别用于屏障的初始化和反初始化。

#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count);
int pthread_barrier+destroy(pthread_barrier_t *restrict barrier)l
两个函数的返回值:若成功,返回0;否则,返回错误编码。

count参数指定允许所有进程继续运行之前,必须到达屏障的线程数目。attr参数指定屏障对象的属性,默认属性设置为NULL即可。

pthread_barrier_wait函数用于线程已完成工作,准备等所有其他线程赶上来。

#include <pthread.h>
int pthread_barrier_eait(pthread_barrier_t *barrier);
返回值:若成功,返回0或PTHREAD_BARRIER_SERIAL_THREAD;否则,返回错误编码。

调用pthread_barrier_wait的线程在屏障计数未满足条件时,会进入休眠状态。如果该线程是最后调用pthread_barrier_wait的线程,就满足了屏障计数,所有的线程都被唤醒。

一旦达到屏障计数值,而且线程处于非阻塞状态,屏障可被重用。但是除非在调用了pthread_barrier_destroy函数之后,有调用了pthread_barrier_init函数对计数用另外的数进行初始化,否则屏障计数不会改变。

测试示例:

多个线程如何使用屏障进行同步。

#include "apue.h"
#include <pthread.h>
#include <limits.h>
#include <sys/time.h>

#define NTHR   8				/* number of threads */
#define NUMNUM 8000000L			/* number of numbers to sort */
#define TNUM   (NUMNUM/NTHR)	/* number to sort per thread */

long nums[NUMNUM];
long snums[NUMNUM];

pthread_barrier_t b;

#ifdef SOLARIS
#define heapsort qsort
#else
extern int heapsort(void *, size_t, size_t,
                    int (*)(const void *, const void *));
#endif

/*
 * Compare two long integers (helper function for heapsort)
 */
int
complong(const void *arg1, const void *arg2)
{
	long l1 = *(long *)arg1;
	long l2 = *(long *)arg2;

	if (l1 == l2)
		return 0;
	else if (l1 < l2)
		return -1;
	else
		return 1;
}

/*
 * Worker thread to sort a portion of the set of numbers.
 */
void *
thr_fn(void *arg)
{
	long	idx = (long)arg;

	heapsort(&nums[idx], TNUM, sizeof(long), complong);
	pthread_barrier_wait(&b);

	/*
	 * Go off and perform more work ...
	 */
	return((void *)0);
}

/*
 * Merge the results of the individual sorted ranges.
 */
void
merge()
{
	long	idx[NTHR];
	long	i, minidx, sidx, num;

	for (i = 0; i < NTHR; i++)
		idx[i] = i * TNUM;
	for (sidx = 0; sidx < NUMNUM; sidx++) {
		num = LONG_MAX;
		for (i = 0; i < NTHR; i++) {
			if ((idx[i] < (i+1)*TNUM) && (nums[idx[i]] < num)) {
				num = nums[idx[i]];
				minidx = i;
			}
		}
		snums[sidx] = nums[idx[minidx]];
		idx[minidx]++;
	}
}

int
main()
{
	unsigned long	i;
	struct timeval	start, end;
	long long		startusec, endusec;
	double			elapsed;
	int				err;
	pthread_t		tid;

	/*
	 * Create the initial set of numbers to sort.
	 */
	srandom(1);
	for (i = 0; i < NUMNUM; i++)
		nums[i] = random();

	/*
	 * Create 8 threads to sort the numbers.
	 */
	gettimeofday(&start, NULL);
	pthread_barrier_init(&b, NULL, NTHR+1);
	for (i = 0; i < NTHR; i++) {
		err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM));
		if (err != 0)
			err_exit(err, "can't create thread");
	}
	pthread_barrier_wait(&b);
	merge();
	gettimeofday(&end, NULL);

	/*
	 * Print the sorted list.
	 */
	startusec = start.tv_sec * 1000000 + start.tv_usec;
	endusec = end.tv_sec * 1000000 + end.tv_usec;
	elapsed = (double)(endusec - startusec) / 1000000.0;
	for (i = 0; i < NUMNUM; i++)
		printf("%ld\n", snums[i]);
	printf("sort took %.4f seconds\n", elapsed);
	exit(0);
}

结果如下:

在上述编译过程中会出现如下错误:

该问题的解决方案可参考https://www.cnblogs.com/thrillerz/articles/4368746.html注意需要在编译选项中加入-pthread-lbsd才可编译通过。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!