第11章 线程同步

第11章 线程同步

1、引言

  • 当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。
  • 如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。
  • 同样,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。

但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。

1.1 举个栗子

在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。

在这里插入图片描述

图 11-7 描述了两个线程读写相同变量的假设例子。

  • 在这个例子中,线程 A读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。
  • 当线程B在这两个存储器写周期中间读取这个变量时,它就会得到不一致的值。

1.2 再举个栗子

为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。

在这里插入图片描述

图11-8描述了这种同步。

  • 如果线程B希望读取变量,它首先要获取锁。
  • 同样,当线程A更新变量时,也需要获取同样的这把锁。
  • 这样,线程B在线程A释放锁以前就不能读取变量。

1.3 再再举个栗子

两个或多个线程试图在同一时间修改同一变量时,也需要进行同步。考虑
变量增量操作的情况(图11-9)

增量操作通常分解为以下3步:

  1. 从内存单元读入寄存器。
  2. 在寄存器中对变量做增量操作。
  3. 把新的值写回内存单元。

在这里插入图片描述

如果两个线程试图几乎在同一时间对同一个变量做增量操作而不进行同步的话,结果就可能出现不一致。

  • 变量可能比原来增加了1,也有可能比原来增加了2,具体增加了1还是2要取决于第二个线程开始操作时获取的数值。
  • 如果第二个线程执行第1步要比第一个线程执行第3步要早,第二个线程读到的值与第一个线程一样,为变量加1,然后写回去,事实上没有实际的效果,总的来说变量只增加了1。

如果修改操作是原子操作,那么就不存在竞争。

在图 11-9 例子中,如果增加1只需要一个存储器周期,那么就没有竞争存在。

  • 如果数据总是以顺序一致出现的,就不需要额外的同步。
  • 当多个线程观察不到数据的不一致时,那么操作就是顺序一致的。

但是,在现代计算机系统中,存储访问需要多个总线周期,多处理器的总线周期通常在多个处理器上是交叉的,所以我们并不能保证数据是顺序一致的。

  • 在顺序一致环境中,可以把数据修改操作解释为运行线程的顺序操作步骤。
  • 可以把这样的操作描述为“线程A对变量增加了1,然后线程B对变量增加了1,所以变量的值就比原来的大2”,或者描述为“线程B对变量增加了1,然后线程A对变量增加了1,所以变量的值就比原来的大2”。
  • 这两个线程的任何操作顺序都不可能让变量出现除了上述值以外的其他值。

1.4 其他情况

程序使用变量的方式也会引起竞争,也会导致不一致的情况发生。

例如,我们可能对某个变量加 1,然后基于这个值做出某种决定。因为这个增量操作步骤和这个决定步骤的组合并非原子操作,所以就给不一致情况的出现提供了可能。

2、线程同步方法

3、互斥量

3.1 理解

  • 互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。
  • 对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。
  • 如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。

3.2 函数

互斥量是用pthread_mutex_t数据类型表示的。使用前必须先初始化

初始化

两种初始化方式:

  1. 设置为常量:PTHREAD_MUTEX_INITIALIZER(只适用于静态分配)
  2. 调用函数初始化:调用pthread_mutex_init函数初始化,如果动态分配互斥量(例如:通过调用malloc函数)在释放内存前需要调用pthread_mutex_destroy。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexarrt_t *restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 返回值:成功返回0,失败返回错误编号。
  • 若使用默认属性初始化互斥量,attr设为NULL即可。

加锁、解锁

#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将锁住互斥量,不会出现阻塞直接返回0。
    • 互斥量处于锁住状态,那么pthread_mutex_trylock将会失败,不能锁住互斥量,返回ebusy。

3.3 来个栗子

目的:使用互斥量保护数据结构

当一个以上的线程需要访问动态分配的对象时,我们可以在对象中嵌入引用计数,确保在所有使用该对象的线程完成数据访问之前,该对象内存空间不会被释放。

  • 在对引用计数加 1、减 1、检查引用计数是否到达 0 这些操作之前需要锁住互斥量。
  • 在foo_alloc 函数中将引用计数初始化为 1 时没必要加锁,因为在这个操作之前分配线程是唯一引用该对象的线程。
  • 但是在这之后如果要将该对象放到一个列表中,那么它就有可能被别的线程发现,这时候需要首先对它加锁。
  • 在使用该对象前,线程需要调用foo_hold对这个对象的引用计数加1。
  • 当对象使用完毕时,必须调用foo_rele释放引用。最后一个引用被释放时,对象所占的内存空间就被释放。
  • 在这个例子中,我们忽略了线程在调用foo_hold之前是如何找到对象的。如果有另一个线程在调用foo_hold时阻塞等待互斥锁,这时即使该对象引用计数为0,foo_rele释放该对象的内存仍然是不对的。可以通过确保对象在释放内存前不会被找到这种方式来避免上述问题。可以通过下面的例子来看看如何做到这一点。
#include <stdlib.h>        // 包含标准库,用于内存管理函数。
#include <pthread.h>       // 包含pthread库,用于多线程支持。

struct foo {
    int 			f_count;  // 一个整数字段,用于存储结构体的引用计数。
    pthread_mutex_t f_lock;   // 用于控制对结构体的访问的互斥锁。
    int 			f_id;     // 一个整数字段,用于存储结构体的标识符。
};
// 分配并初始化“foo”结构体的函数
struct foo* foo_alloc(int id) { 
    struct foo* fp;           	// 声明一个指向“foo”结构体的指针。
    if((fp = malloc(sizeof(struct foo))) != NULL) { // 为结构体分配内存。
        fp->f_count = 1;       						// 将引用计数初始化为1。
        fp->f_id = id;         						// 使用提供的ID初始化结构体的ID。
        if(pthread_mutex_init(&fp->f_lock, NULL) != 0) { // 初始化互斥锁。
            free(fp);           					// 如果互斥锁初始化失败,释放内存。
            return(NULL);       					// 返回NULL以指示失败。
        }
    }
    return(fp);                // 返回初始化的“foo”结构体。
}
// 增加“foo”结构体的引用计数的函数
void foo_hold(struct foo* fp) {  
    pthread_mutex_lock(&fp->f_lock); 	// 获取互斥锁以确保独占访问。
    fp->f_count++;            			// 增加引用计数。
    pthread_mutex_unlock(&fp->f_lock); 	// 释放互斥锁。
}
// 释放“foo”结构体并可能释放其内存的函数
void foo_rele(struct foo* fp) {  
    pthread_mutex_lock(&fp->f_lock); 		// 获取互斥锁以确保独占访问。
    if (--fp->f_count == 0) { 				// 如果引用计数减为0,则可以安全地释放结构体。
        pthread_mutex_unlock(&fp->f_lock); 	// 释放互斥锁。
        pthread_mutex_destory(&fp->f_lock); // 销毁互斥锁。
        free(fp);           				// 释放与“foo”结构体关联的内存。
    } else {
        pthread_mutex_unlock(&fp->f_lock);  // 如果引用计数不为零,释放互斥锁。
    }
}

4、 条件变量

4.1 理解

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

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

4.2 函数

初始化

由pthread_cond_t数据类型表示

两种初始化方式:

  • 静态分配:把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量
  • 动态分配:需要使用pthread_cond_init函数对它进行初始化

在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化(deinitialize)

#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_init函数的attr参数可以设置为NULL。

我们使用pthread_cond_wait等待条件变量变为真。如果在给定的时间内条件不能满足,那么会生成一个返回错误码的变量。

#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 timespaec *restrict tsptr);

两个函数的返回值:若成功,返回0;否则,返回错误编号

注意事项

  • 传递给pthread_cond_wait的互斥量对条件进行保护。
  • 调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。
  • pthread_cond_wait返回时,互斥量再次被锁住。
  • pthread_cond_timedwait函数的功能与pthread_cond_wait函数相似,只是多了一个超时(tsptr)。超时值指定了我们愿意等待多长时间,它是通过timespec结构指定的。这个时间值是一个绝对数而不是相对数。
  • 例如,假设愿意等待3分钟。那么,并不是把3分钟转换成timespec结构,而是需要把当前时间加上3分钟再转换成timespec结构。
  • 可以使用clock_gettime函数(见6.10节)获取timespec结构表示的当前时间。但是目前并不是所有的平台都支持这个函数,因此,也可以用另一个函数gettimeofday 获取timeval结构表示的当前时间,然后把这个时间转换成timespec结构。
#include <sys/time.h>
#include <stdlib.h>
void maketimeout(struct timespec *tsp, long minutes) {
    struct timeval now;
    gettimeofday(&now,NULL);
    tsp->tv_sec = now.tv_sec;
    tsp->tv_nsec = now.tv_usec*1000;
    tsp->tv_sec += minutes*60;
}

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

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

POSIX 规范为了简化 pthread_cond_signal 的实现,允许它在实现的时候唤醒一个以上的线程。

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

两个函数的返回值:若成功,返回0;否则,返回错误编号

在调用pthread_cond_signal或者pthread_cond_broadcast时,我们说这是在给线程或者条件发信号。必须注意,一定要在改变条件状态以后再给线程发信号。

4.3 举个例子

以下例子给出了如何结合使用条件变量和互斥量对线程进行同步。

#include <pthread.h>

// 定义消息结构体
struct msg {
    struct msg* m_next;
};

struct msg* workq; // 全局消息队列指针

pthread_cond_t qready = PTHREAD_COND_INITIALIZER; // 条件变量,用于通知等待中的线程
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER; // 互斥锁,用于保护消息队列

void process_msg(void) {
    struct msg* mp;
    for (;;) {
        pthread_mutex_lock(&qlock); // 获取互斥锁,保护对消息队列的访问
        // 如果消息队列为空,等待条件变量
        while (workq == NULL) {
            pthread_cond_wait(&qready, &qlock);
        }
        mp = workq; // 获取队列头部的消息
        workq = mp->m_next; // 更新队列头部指针,将头部消息出队
        pthread_mutex_unlock(&qlock); // 释放互斥锁,允许其他线程访问消息队列
    }
}

void enqueue_msg(struct msg* mp) {
    pthread_mutex_lock(&qlock); // 获取互斥锁,保护对消息队列的访问
    mp->m_next = workq; // 将新消息插入到队列头部
    workq = mp; // 更新队列头部指针,将新消息设置为队列头部
    pthread_mutex_unlock(&qlock); // 释放互斥锁,允许其他线程访问消息队列
    pthread_cond_signal(&qready); // 发送信号,通知等待中的消费者线程有新消息可处理
}
  • 条件是工作队列的状态。我们用互斥量保护条件,在 while 循环中判断条件。
  • 把消息放到工作队列时,需要占有互斥量,但在给等待线程发信号时,不需要占有互斥量。
  • 只要线程在调用pthread_cond_signal之前把消息从队列中拖出了,就可以在释放互斥量以后完成这部分工作。
  • 因为我们是在 while 循环中检查条件,所以不存在这样的问题:线程醒来,发现队列仍为空,然后返回继续等待。如果代码不能容忍这种竞争,就需要在给线程发信号的时候占有互斥量。

5、 读写锁

5.1 引言

读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性。

互斥锁只有两个状态:(一次只能由一个线程可以对其加锁)

  • 锁住状态
  • 不加锁状态

读写锁可以有3中状态:(一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁)

  • 读模式的加锁状态
  • 写模式的加锁状态
  • 不加锁状态

注意:

  1. 写加锁状态:解锁前,所有试图加锁的线程都会被阻塞。
  2. 读加锁状态:所有试图以读模式加锁的线程都可以得到访问权,但是,试图以写模式加锁的线程都会被阻塞,直到所有的线程释放它们的读锁为止。
  3. 读写锁非常适合于对数据结构读的次数远大于写的情况。

读写锁也叫做共享互斥锁(shared-exclusive lock)。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它是写模式锁住的时候,就可以说成是以互斥模式锁住的。

与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。

5.2 函数

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
  • 返回值:成功为0,失败为错误编号
  • 若使用读写锁默认属性,传一个NULL指针给attr

初始化

两种初始化:

  • 设置为常量:如果默认属性够用,可用静态分配的读写锁进行初始化。PTHREAD_RWLOCK_INITIALIZER常量
  • 调用函数:pthread_rwlock_init

如果pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。

在释放读写锁占用的内存之前,需要调用pthread_rwlock_destroy做清理工作。如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。

加锁、解锁

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthradd_rwlock_t *rwlock);
  • 返回值:成功为0,失败为错误编号
  • 各种实现可能会对共享模式下可获取的读写锁的次数进行限制,所以需要检查pthread_rwlock_rdlock的返回值

Single UNIX Specification还定义了读写锁原语的条件版本。

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

返回值:成功为0,失败返回错误编号EBUSY

5.3 举个栗子

目的:解释读写锁的使用。作业请求队列由单个读写锁保护。这个例子给出了多个工作线程获取单个主线程分配给它们的作业。

#include <stdlib.h>
#include <pthread.h>
// 定义任务结构体
struct job { 
    struct job *j_next; 	// 指向下一个任务
    struct job *j_prev; 	// 指向前一个任务
    pthread_t 	j_id; 		// 任务的线程ID
};
// 定义队列结构体
struct queue { 
    struct job *q_head; 		// 指向队列头部的任务
    struct job *q_tail; 		// 指向队列尾部的任务
    pthread_rwlock_t q_lock;	// 读写锁
};
// 初始化队列
int queue_init(struct queue *qp) {
    int err; // 错误码
    qp->q_head = NULL;  // 队列头部置空
    qp->q_tail = NULL;  // 队列尾部置空
    err = pthread_rwlock_init(&qp->q_lock,NULL); // 初始化读写锁
    if(err != 0) 		// 如果出现错误
        return (err); 	// 返回错误码
    return(0); // 返回成功
}
// 向队列头部插入任务(头插法)
void job_insert(struct queue *qp,struct job *jp) {
    pthread_rwlock_wrlock(&qp->q_lock); // 写锁定
    jp->j_next = qp->q_head; // 新任务的下一个指向当前头部任务
    jp->j_prev = NULL;		 // 新任务的前一个置空
    if(qp->q_head != NULL) 	 // 如果队列不为空
        qp->q_head->j_prev = jp; // 当前头部任务的前一个指向新任务
    else 					 // 如果队列为空
        qp->q_tail = jp; 	 // 队列尾部指向新任务
    qp->q_head = jp; 		 // 队列头部指向新任务
    pthread_rwlock_unlock(&qp->q_lock); // 解锁
}
// 向队列尾部追加任务
void job_append(struct queue *qp, struct job *jp) {
    pthread_rwlock_wrlock(&qp->q_lock); // 写锁定
    jp->j_next = NULL; 					// 新任务的下一个置空
    jp->j_prev = qp->q_tail; 			// 新任务的前一个指向当前尾部任务
    if(qp->q_tail != NULL) 				// 如果队列尾部不为空
        qp->q_tail->j_next = jp; 		// 当前尾部任务的下一个指向新任务
    else 								// 如果队列尾部为空
        qp->q_head = jp; 				// 队列头部指向新任务
    qp->q_tail = jp; 					// 队列尾部指向新任务
    pthread_rwlock_unlock(&qp->q_lock); // 解锁
}
// 从队列中移除任务
void job_remove(struct queue *qp, struct job *jp) {
    pthread_rwlock_wrlock(&qp->q_lock); // 写锁定
    if(jp == qp->q_head) { 				// 如果要移除的任务是队列头部任务
        qp->q_head = jp->j_next; 		// 队列头部指向下一个任务
        if(qp->q_tail == jp) 			// 如果队列尾部也是要移除的任务
            qp->q_tail = NULL; 			// 队列尾部置空
        else 							// 如果队列尾部不是要移除的任务
            jp->j_next->j_prev = jp->j_prev; // 下一个任务的前一个指向要移除任务的前一个
    } else if(jp == qp->q_tail) { 		// 如果要移除的任务是队列尾部任务
        qp->q_tail = jp->j_prev; 		// 队列尾部指向前一个任务
        jp->j_prev->j_next = jp->j_next;// 前一个任务的下一个指向要移除任务的下一个
    } else { // 如果要移除的任务在队列中间
        jp->j_prev->j_next = jp->j_next;// 前一个任务的下一个指向要移除任务的下一个
        jp->j_next->j_prev = jp->j_prev;// 下一个任务的前一个指向要移除任务的前一个
    }
    pthread_rwlock_unlock(&qp->q_lock); // 解锁
}
// 查找队列中的任务
struct job *job_find(struct queue *qp, pthread_t id) {
	struct job* jp; 							// 任务指针
    if(pthread_rwlock_rdlock(&qp->q_lock) != 0) // 读锁定
        return(NULL); 							// 如果出现错误,返回空
    for(jp = qp->q_head; jp != NULL; jp = jp->j_next) // 遍历队列中的任务
        if(pthread_equal(jp->j_id, id)) 		// 如果找到了目标任务
            break; 								// 退出循环
    pthread_rwlock_unlock(&qp->q_lock); 		// 解锁
    return(jp); 								// 返回找到的任务
}
  • 在这个例子中,凡是需要向队列中增加作业或者从队列中删除作业的时候,都采用了写模式来锁住队列的读写锁。
  • 不管何时搜索队列,都需要获取读模式下的锁,允许所有的工作线程并发地搜索队列。
  • 在这种情况下,只有在线程搜索作业的频率远远高于增加或删除作业时,使用读写锁才可能改善性能。
  • 工作线程只能从队列中读取与它们的线程 ID 匹配的作业。由于作业结构同一时间只能由一个线程使用,所以不需要额外的加锁。

6、自旋锁

6.1 理解

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前
一直处于忙等(自旋)阻塞状态。所以当线程自旋等待锁变为可用时,CPU不能做其他的事情。这也是自旋锁只能够被持有一小段时间的原因。

可用于以下情况:

  • 锁被持有时间短,线程不希望再重新调度上花费太多的成本。
  • 自旋锁用在非抢占式内核中时是非常有用的:除了提供互斥机制以外,它们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自选锁(把中断想成是另一种抢占)。在这种类型的内核中,中断处理程序不能休眠,因此它们能用的同步原语只能是自旋锁。

6.2 函数

初始化

#include <pthread.h>
//对自旋锁进行初始化
int pthread_spin_init(pthread_spinlock_t* lock, int pshared);
//销毁自旋锁
int pthread_spin_destory(pthread_spinlock_t *lock);

返回值:成功为0,失败为错误编号

参数:

  • pshared参数表示进程共享属性,表明自旋锁是如何获取的。
    • PTHREAD_PROCESS_SHARED,则自旋锁能被可用访问锁底层内存的线程所获取,即便那些线程属于不同的进程。
    • 如果设为PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁的进程内部的线程所访问。

加锁、解锁

#include <pthread.h>
//加锁,获取锁之前一直自旋
int pthread_spin_lock(pthread_spinlock_t *lock);
//尝试加锁,不能获取锁返回EBUSY错误(不能自旋)
int pthread_spin_trylock(pthread_spinlock_t *lock);
//解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);

返回值:成功为0,失败为错误编号

注意:

  • 如果自旋锁当前在解锁状态的话,pthread_spin_lock函数不要自旋就可以对它加锁。如果线程已经对它加锁了,结果就是未定义的。
  • 调用pthread_spin_lock会返回EDEADLK错误(或其他错误),或者调用可能会永久自旋。
  • 不管是pthread_spin_lock还是pthread_spin_trylock,返回值为0的话就表示自旋锁被加锁。
  • 不要调用在持有自旋锁情况下可能会进入休眠状态的函数。如果调用了这些函数,会浪费CPU资源,因为其他线程需要获取自旋锁需要等待的时间就延长了。

7、屏障

7.1 引言

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

pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。

但是屏障对象的概念更广,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。

7.2 函数

初始化

#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t* restrict barrier, const pthread_barrierattr_t* restrict attr, unsigned int count);
int pthread_barrier_destory(pthread_barrier_t *barrier);

返回值:成功为0,失败为错误编号

参数:

  • count:在允许所有线程继续运行之前,必须到达屏障的线程数目。
  • attr:设置为NULL,用默认属性初始化屏障

可以使用pthread_barrier_wait函数表明,线程已完成工作,准备等所有其他线程赶上来。

#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);

返回值:

  • 成功为0或者PTHREAD_BARRIER_SERIAL_THREAD
  • 失败返回错误编号

调用pthread_barrier_wait的线程在屏障计数(调用pthread_barrier_init时设定)未满足条件时,会进入休眠状态。

如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了屏障计数,所有的线程都被唤醒。

对于一个任意线程,pthread_barrier_wait函数返回了PTHREAD_BARRIER_SERIAL_THREAD。剩下的线程看到的返回值是0。这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。

一旦达到屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用。

但是除非在调用了pthread_barrier_destroy函数之后,又调用了pthread_barrier_init函数对计数用另外的数进行初始化,否则屏障计数不会改变。

7.3 举个例子

以下例子给出了在一个任务上合作的多个线程之间如何用屏障进行同步。

#include "apue.h"        
#include <pthread.h>     
#include <limits.h>      // 包含限制常量
#include <sys/time.h>    
#define NTHR 8           // 定义线程数
#define NUMNUM 8000000L  // 定义总元素数
#define TNUM (NUMNUM/NTHR)  // 每个线程的元素数

long nums[NUMNUM];         // 存储随机数的数组
long snums[NUMNUM];        // 存储排序后的数值的数组

pthread_barrier_t b;      // 声明一个 pthread 屏障变量

#ifdef SOLARIS
#define headpsort qsort   // 在 Solaris 平台上使用 qsort
#else
extern int heapsort(void*, size_t, size_t, int(*)(const void*, const void*)); // 在其他平台上使用 heapsort
#endif

//比较两个元素的大小
int complong(const void* arg1, const void* arg2) {
    long l1 = *(long*)arg1; // 强制转换参数类型为 long
    long l2 = *(long*)arg2;
    if (l1 == l2)
        return 0;
    else if (l1 < l2)
        return -1;
    else
        return 1;
}
//线程的执行函数,使用heapsort算法对一部分数组进行排序,并在完成后等待其他线程
void* thr_fn(void* arg) {
    long idx = (long)arg;  // 强制转换参数类型为 long
    heapsort(&nums[idx], TNUM, sizeof(long), complong); // 使用 heapsort 进行排序
    pthread_barrier_wait(&b);  // 等待其他线程到达屏障
    return ((void*)0);
}
//合并各个线程排序后的子数组,生成最终的有序数组
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;

    srandom(1);  // 初始化随机数生成器
    for (i = 0; i < NUMNUM; ++i) {
        nums[i] = random();  // 生成随机数并存储在 nums 数组中
    }
    gettimeofday(&start, NULL);  // 获取开始时间
    pthread_barrier_init(&b, NULL, NTHR + 1);  // 初始化 pthread 屏障
    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);  // 获取结束时间

    startusec = start.tv_sec * 1000000 + start.tv_usec;
    endusec = end.tv_sec * 1000000 + end.tv_usec;
    elapsed = (double)(endusec - startusec) / 1000000.0; // 计算经过的时间
    printf("sort took %.4f seconds\n", elapsed); // 打印排序花费的时间
    for (i = 0; i < NUMNUM; ++i)
        printf("%ld\n", snums[i]);  // 打印排序后的数值
    exit(0);
}
  • 这个例子给出了多个线程只执行一个任务时,使用屏障的简单情况。

  • 在更加实际的情况下,工作线程在调用pthread_barrier_wait函数返回后会接着执行其他的活动。

  • 在这个实例中,使用8个线程分解了800万个数的排序工作。每个线程用堆排序算法对100万个数进行排序。然后主线程调用一个函数对这些结果进行合并。

  • 并不需要使用 pthread_barrier_wait 函数中的返回值PTHREAD_BARRIER_SERIAL_THREAD 来决定哪个线程执行结果合并操作,因为我们使用了主线程来完成这个任务。
    gettimeofday(&end, NULL); // 获取结束时间

    startusec = start.tv_sec * 1000000 + start.tv_usec;
    endusec = end.tv_sec * 1000000 + end.tv_usec;
    elapsed = (double)(endusec - startusec) / 1000000.0; // 计算经过的时间
    printf(“sort took %.4f seconds\n”, elapsed); // 打印排序花费的时间
    for (i = 0; i < NUMNUM; ++i)
    printf(“%ld\n”, snums[i]); // 打印排序后的数值
    exit(0);
    }


- 这个例子给出了多个线程只执行一个任务时,使用屏障的简单情况。
- 在更加实际的情况下,工作线程在调用pthread_barrier_wait函数返回后会接着执行其他的活动。
- 在这个实例中,使用8个线程分解了800万个数的排序工作。每个线程用堆排序算法对100万个数进行排序。然后主线程调用一个函数对这些结果进行合并。
- 并不需要使用 pthread_barrier_wait 函数中的返回值PTHREAD_BARRIER_SERIAL_THREAD 来决定哪个线程执行结果合并操作,因为我们使用了主线程来完成这个任务。
- 这也是把屏障计数值设为工作线程数加1的原因,主线程也作为其中的一个候选线程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值