APUE 4 - 线程<2> : 线程同步

当控件的多个线程共享统一内存时,我们需要确定各个线程访问到的数据的一致性。在cpu结构中,修改操作由多个内存读写周期(memory cycle),而在这些内存周期之间, 有可能会发生其他线程的内存读操作,这样就会产生多线程之间的数据一致性问题。

互斥锁 mutex

我们可以通过线程互斥锁接口(pthreads mutual-exclusion interfaces)来保证同一时间只有一个线程访问我们的数据。一个mutex变量使用pthread_mutex_t数据类型来表示。在我们使用这个变量前,我们必须使用常量PTHREAD_MUTEX_INITIALIZER或调用pthread_mutex_init对其进行初始化。如果我们动态的分配mutex(通过调用pthread_mutex_init),我们需要在释放内存前调用pthread_mutex_destroy。

 #include <pthread.h>

 /* Both return 0 if OK, error number on failure */
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr); int pthread_mutex_destroy(pthread_mutex_t* mutex);

可以通过 pthread_mutex_lock对mutex加锁,如果mutex已经被锁住,那么其他对mutex进行加锁的线程会被阻塞,直到mutex被解锁。可以通过pthread_mutex_unlock来解锁mutex。

 #include <pthread.h>

 /* return 0 if OK, error number on failure */
int pthread_mutex_lock(pthread_mutex_t* mutex); /*
如果mutex处于unlocked状态,此方法会对mutex加锁并返回0,
否则返回EBUSY,不会对mutex加锁
*/
int pthread_mutex_trylock(pthread_mutex_t* mutex); /* return 0 if OK, error number on failure */
int pthread_mutex_unlock(pthread_mutex_t* mutex);

避免死锁

一个线程如果试图对同一个mutex进行连续两次加锁,那么它将处于死锁状态。而确实有那么几种明显的使用mutex会产生死锁。举例来说,当我们程序中有多个mutex的时候,如果我们让一个线程锁住第一个mutex,然后对第二个mutex进行加锁,而与此同时如果另一个线程锁住第二个mutex并试图对第一个mutex进行加锁,那么此时程序就会进入死锁状态。

我们可以通过消息的控制对mutex的加锁状态来避免死锁。如果我们所有的线程对所有的mutex都保持同一种加锁顺序,那么程序中永远不会出现加锁现象。然后有些时候,由于程序的架构问题我们无法保证加锁顺序,此时我们就必须采取一些其他的措施。在这种情况下我们应该先释放掉以加锁的mutex,然后重新尝试加锁。这种情况下我们可以使用pthread_mutex_trylock,如果pthread_mutex_trylock成功我们可以继续进行其他操作,否则我们应该释放掉以加锁的mutex,清理程序,然后在重试加锁。

可以使用pthread_mutex_timedlock方法绑定加锁阻塞时间,如果规定时间内没有成功对mutex加锁,它返回ETIMEOUT。

 #include <pthread.h>
#include <time.h> /* tspr is absolute time, Return 0 if OK, error number on failure. */
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec* restrict tsptr);

读写锁

读写锁也称为共享锁,它与mutex相似,但他们提供了对并行机制更高精度的控制。对于mutex来讲,他只有两种状态:locked和unlocked,并且,同一时间内只有一个线程可以锁住它。而对于读写锁来说,他们有三种状态:读锁状态(locked in read mode),写锁状态(locked in write mode)和解锁状态(unlocked)。同一时间只有一个线程可以让读写锁处于写锁状态,而同一时间可以有多个线程让读写锁处于读锁状态。

当一个读写锁处于写锁状态时,所有尝试对其加锁的线程都会处于阻塞状态,知道读写锁的写锁状态被解除。当读写锁处于读锁状态时,所有对其进行写锁操作的线程都会被阻塞,但是对其进行读锁操作的线程不会被阻塞。通常,当有对处于读写锁进行写锁操作的线程被阻塞时,其他队此读写锁进行读操作的线程也会被阻塞。

读写锁在使用前必须初始化,同样的,在释放内存钱必须销毁。也可以使用常量PTHREAD_RWLOCK_INITIALIZER初始化rwlock。

 #include <pthread.h>

 /* All return 0 if OK, error number on failure */
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock); int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
/* unlock any type of rwlock */
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_truwrlock(pthread_rwlock_t* rwlock);

rwlock with timeouts

 #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);

条件变量(Condition Variables)

条件变量是另一种线程同步机制,他们提供了线程交互点。当与mutex一起使用时, 条件变脸可以是线下以*竞争的方式来等待任意条件的发生。这个条件本身是被一个mutex保护的。线程在改变条件状态之前必须对这个mutex加锁,其他线程在获取到这个mutex权限前不会知道这个变化,因为mutex必须处于加锁状态才能读取到条件的值。

条件变量在使用前必须进行初始化,我们可以通过常量PTHREAD_COND_INITAILIZER或者调用pthread_cond_init来初始化条件变量, 若条件变量是动态分配的,那么我么需要使用pthread_cond_destroy来释放条件变量。

 #include <pthread.h>

 /* Both return 0 if OK, error number on failure */
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr); int pthread_cond_destroy(pthread_cond_t* cond);

wait for condition to be true

 #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, constr struct timespec* restict tsptr);

mutex用于保护条件,以防止多个线程调用pthread_cond_wait的竞态条件。调用线程在调用前需要对mutex加锁,pthread_cond_wait会自动将调用线程放到此条件的等待线程列表中然后等待条件的发生并解锁mutex。当phtread_cond_wait返回时(正常情况是条件被满足时),mutex被重新置位加锁状态。在pthread_cond_wait或phread_cond_timedwait方法返回时,线程需要重新检测条件,因为其他线程在此时可能会更改了条件, 因此通常我们将等待放入循环中。

有两个函数可以通知条件已满足,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);

自旋锁(spin locks)

自旋锁与互斥锁相似,只不过它不是以休眠的方式阻塞进程,而是通过busy-waiting(spinning)知道获取到锁权限。自旋锁通常用于锁住状态时间较短和线程不愿遭受调度成本的情况。自旋锁通常用于实现其他类型锁的底层原型。依赖于系统结构,他们可以通过test-and-set指令高效的实现。尽管高效,他们会导致浪费CPU资源。

 #include <pthread.h>

 int pthread_spin_init(pthread_spinlock_t* lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t* lock); int pthread_spin_lock(pthread_spinlock_t* lock);
int pthread_spin_trylock(pthread_spinlock_t* lock);
int pthread_spin_unlock(pthread_spinlock_t* lock);

Barriers

Barriers 是一种同步机制,它可以用于并行环境下协调多线程工作。barriers允许每个线程都等待直到所有协调线程都达到某个点,然后从它阻塞的地方继续执行。pthread_jion就是一种形式的barrier——它使一个线程等待直到另一个线程退出。Barrier对象要比他更通用一些,他们允许任意数量的线程等待直到所有的线程完成执行,但线程不必退出。

 #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* barrier);

count参数用于指定在所有线程允许继续执行前必须达到barrier的线程数量。

我们使用pthread_barrier_wait函数来指示此线程以完成他的工作并准备好等待其他线程赶上进度。

#include <pthread.h>

/* Rturns 0 or PTHREAD_BARRIER_SERIAL_THREAD if OK, error number on failure */
int pthread_barrier_wait(pthread_barrier_t* barrier);

调用pthread_barrier_wait的线程会被置于休眠状态如果如果barrier数量未达到init时设置的数量。如果调用线程是最后一个线程,即barrier数量达到了初始化是设置的数量,此时所有的线程都会被唤醒。

总结

线程基本同步机制有 mutex、rwlock、condlock、spin lock、barrier。mutex同一时间内只有一个线程处于locked状态;rwlock提供了对读写更精细的控制;condlock通过条件变量为多线程同步提供了线程交互点;spin lock是高效的mutex,但是浪费cpu性能,一般用户层编程用不到此类型的锁;barrier提供了一种良好的多线程协调机制;实际项目中我们应该根据几种锁的特性来合理使用,并注意要避免死锁问题。

上一篇:Android Studio 1.0.2项目实战——从一个APP的开发过程认识Android Studio


下一篇:<转载>网页设计中的F式布局