THREADS: THREAD SYNCHRONIZATION


Protecting Accesses to Shared Variables: Mutexes

The term critical section is used to refer to a section of code that accesses a shared resource and whose execution should be atomic; that is, its execution should not be interrupted by another thread that simultaneously accesses the same shared resource.

Statically Allocated Mutexes

动态创建mutex较为复杂。

A mutex is a variable of the type pthread_mutex_t。在它可以使用之前,mutex必须被初始化。对于一个静态分配的mutex,我们可以通过给它赋值PTHREAD_MUTEX_INITIALIZER来实现:

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

Locking and Unlocking a Mutex

初始化结束之后,mutex是未被加锁的。为了对一个mutex加锁以及解锁,我们使用pthread_mutex_lock()pthread_mutex_unlock()函数。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Both return 0 on success, or a positive error number on error

To lock a mutex, we specify the mutex in a call to pthread_mutex_lock(). If the mutex is currently unlocked, this call locks the mutex and returns immediately. If the mutex is currently locked by another thread, then pthread_mutex_lock() blocks until the mutex is unlocked, at which point it locks the mutex and returns.

如果线程尝试将自己已经加锁的mutex加锁,then, for the default type of mutex, one of two implementationdefined possibilities may result: the thread deadlocks, blocked trying to lock a mutex that it already owns, or the call fails, returning the error EDEADLK.在Linux上,线程会默认死锁。

如果有多个线程在等待一个已经被加锁的mutex,那么当mutex被解锁之后,哪个线程获得该锁是不确定的。

#include <pthread.h>
#include <tlpi_hdr.h>

static int glob = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void *threadFunc(void *arg) {
  int loops = *((int *)arg);
  int loc, j, s;

  for (j = 0; j < loops; j++) {
    s = pthread_mutex_lock(&mtx);
    if (s != 0) errExitEN(s, "pthread_mutex_lock");
    loc = glob;
    loc++;
    glob = loc;

    s = pthread_mutex_unlock(&mtx);
    if (s != 0) errExitEN(s, "pthread_mutex_unlock");
  }

  return NULL;
}

int main(int argc, char *argv[]) {
  pthread_t t1, t2;
  int loops, s;

  loops = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-loops") : 10000000;

  s = pthread_create(&t1, NULL, threadFunc, &loops);
  if (s != 0) errExitEN(s, "pthread_create");
  s = pthread_create(&t2, NULL, threadFunc, &loops);
  if (s != 0) errExitEN(s, "pthread_create");

  s = pthread_join(t1, NULL);
  if (s != 0) errExitEN(s, "pthread_join");
  s = pthread_join(t2, NULL);
  if (s != 0) errExitEN(s, "pthread_join");

  printf("glob = %d\n", glob);
  exit(EXIT_SUCCESS);
}

pthread_mutex_lock函数有两个变种:pthread_mutex_trylock()pthread_mutex_timelock()

The pthread_mutex_trylock() function is the same as pthread_mutex_lock(), except that if the mutex is currently locked, pthread_mutex_trylock() fails, returning the error EBUSY.

pthread_mutex_timelock()函数需要一个额外的参数,abstime,该参数规定了线程为了等待mutex将会sleep多久。如果在asbtime时间内,线程没能获得mutex,pthread_mutex_timelock()将会返回错误ETIMEOUT

Performance of Mutexes

Mutex Deadlocks

有时,线程需要同步地获取两个或者更多地不同的共享资源,每种资源都由一个mutex管理。这时可能会发生死锁。

避免死锁的最简单的方式是定义一种mutex层次结构。当不同线程可以对同样一组mutex加锁时,他们必须以相同的顺序对每个mutex加锁。比如,在Figure 30-3中,如果两个线程总是以 先对 mutex1 加锁再对 mutex2 加锁的顺序获得mutex,那么就可以避免死锁。然而,有时mutexes之间的层次结构并没有这么清晰的逻辑结构。

另一种不常用的策略是:"try, and then back off"。在这种策略下,线程首先使用pthread_lock_lock()对第一个mutex加锁,然后对下一个mutex使用pthread_lock_trylock()。如果pthread_mutex_trylock()调用失败(返回EBUSY),那么线程就将所有已经获得mutex释放,然后可能在一段时延之后再次尝试。

Dynamically Initializing a Mutex

静态初始化常数PTHREAD_MUTEX_INITIALIZER只能用于初始化具有默认属性的静态分配的mutex。在所有其他情况下,我们必须使用pthread_mutex_init()初始化mutex。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
//Returns 0 on success, or a positive error number on error

The attr argument is a pointer to a pthread_mutexattr_t object that has previously been initialized to define the attributes for the mutex. If attr is specified as NULL, then the mutex is assigned various default attributes.

SUSv3规定下,对任何已经被初始化过的mutex再次进行初始化将会导致未知行为。

在如下情况下我们必须使用pthread_mutex_init()而不能使用静态初始化算子:

  • mutex是被动态分配到堆上的。比如,假设我们动态分配了一个结构体组成的链表,而结构体中包含有一个pthread_mutex_t字段,该字段保存了一个用来保护该结构体的mutex
  • mutex是被分配到栈上的自动变量
  • 我们想要对一个静态分配的mutex进行非默认初始化

当自动分配或者动态分配的mutex不再被使用的时候,需要使用pthread_mutex_destory来将其释放。(对于使用PTHREAD_MUTEX_INITIALIZER初始化的静态分配的mutex来说,不需要使用pthread_mutex_destory

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//Returns 0 on success, or a positive error number on error

如果mutex被创建在动态分配的内存区域中,则在清空那块内存之前需要首先释放mutex。动态分配的mutex应该在它的host函数返回之前被destory。

Mutex Attributes

Mutex Types

待补充

Signaling Changes of State: Condition Variables

mutex防止多个线程同时访问同一个共享变量。而条件变量(Condition Variables)允许一个线程向另一个线程通知共享变量的状态,并且让等待资源的线程阻塞。

举个例子来说明为何要使用条件变量。假设我们有许多线程为主线程生产一些供其使用的结果,我们使用一个被mutex保护的变量avail来表示生产线程产生的资源的数量。

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static int avail = 0;

在生产线程中,我们使用如下的代码:

/* Code to produce a unit omitted */
s = pthread_mutex_lock(&mtx);
if(s != 0)
  errExitEN(s, "pthread_mutex_lock");

avail++;

s = pthread_mutex_unlock(&mtx);
if(s != 0)
  errExitEN(s, "pthread_mutex_unlock")

在主线程中,我们使用如下代码:

for(;;){
  s = pthread_mutex_lock(&mtx);
  if (s != 0)
    errExitEN(s, "pthread_mutex_lock");

  while (avail > 0){
    avail--;
  }

  s = pthread_mutex_unlock(&mtx);
  if (s != 0)
    errExitEN(s, "pthread_mutex_unlock")
}

上述代码可以工作,但是问题是,由于主线程不断地检查变量avail的状态,它会消耗CPU时间。而条件变量condition variable就是用来补救这个问题的。它让无法获得资源的线程sleep(wait),直到另一个线程提醒它状态发生了变化。(比如,条件发生了改变,而这时睡眠的线程必须做些什么事)

条件变量总是和mutex一起使用。mutex保证对共享变量的访问是互斥的,而条件变量用来提示变量状态的变化。

Statically Allocated Condition Variables

正如mutex一样,我们可以动态或者静态地分配条件变量。
条件变量类型为pthread_cond_t。对于一个静态分配的条件变量,对它的初始化是通过为其赋值PTHREAD_COND_INITIALIZER来实现的,

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

Singaling and Waiting on Condition Variables

对条件变量的基本操作是signal以及waitsignal操作目的是提醒其他正在等待的线程当前共享变量的状态已经发生了改变。The wait operation is the means of blocking until such a notification is received.

pthread_cond_signal()以及pthread_cond_broadcast() functions both signal the condition variable specified by cond. pthread_cond_wait()函数将线程阻塞,直到condition variable is signaled.

#include <pthread>

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//All return 0 on success, or a positive error number on error

The difference between pthread_cond_signal() and pthread_cond_broadcast() lies in what happens if multiple threads are blocked in pthread_cond_wait(). With pthread_cond_signal(), we are simply guaranteed that at least one of the blocked threads is woken up; with thread_cond_broadcast(), all blocked threads are woken up.

pthread_cond_wait()函数:

  • unlock the mutex specified by mutex;
  • block the calling thread until another thread signals the condition vatiable cond; and
  • relock mutex.

pthread_cond_wait()函数之所以这么设计是有原因的。通常情况下,我们对共享变量的访问应该是按照如下方式进行:

assert(pthread_mutex_lock(&mtx) == 0);

while(/* Check that shared variable is not in the state we want */)
  pthread_cond_wait(&cond, &mtx);

assert(pthread_mutex_unlock(&mtx) == 0);

互斥锁mutex和条件变量之间有一种天然的联系:

  1. 线程将mutex加锁,为的是检查共享变量的状态;
  2. 检查共享变量的状态;
  3. 如果共享变量没有处于我们想要的状态(比如buffer中没有物品),那么线程必须在 go to sleep 之前将互斥锁释放(这样一来其他线程才可以获取共享变量、改变共享变量的值)。
  4. 当条件变量状态被改变,并且发出提醒信号后,睡眠的线程重新激活,那么由于该线程立刻会访问临界区,所以互斥锁需要再次被加锁。

pthread_cond_wait()函数自动完成了上面步骤中的最后两步——mutex unlocking and locking。在第三步中,释放互斥锁以及blocking on condition variable的执行是原子操作。换句话说,其他线程,在本线程调用pthread_cond_wait()并且阻塞之前,无法获得互斥锁、并且无法访问条件变量。

现在给出利用条件变量解决生产者消费者问题的代码:

#include <assert.h>
#include <pthread.h>
#include <iostream>

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static int avail = 0;

void *producerFcn(void *) {
  while (1) {
    assert(pthread_mutex_lock(&mtx) == 0);

    avail++;
    std::cout << "Producer avail: " << avail << std::endl;

    assert(pthread_mutex_unlock(&mtx) == 0);
    assert(pthread_cond_signal(&cond) == 0);  // Wake sleeoing consumer
  }
}

void *consumerFcn(void *) {
  for (;;) {
    assert(pthread_mutex_lock(&mtx) == 0);

    while (avail == 0) {
      std::cout << "Consumer pthread_cond_wait \n";
      assert(pthread_cond_wait(&cond, &mtx) == 0);
    }

    while (avail > 0) {
      std::cout << "Comsumer avail: " << avail << std::endl;
      avail--;
    }

    assert(pthread_mutex_unlock(&mtx) == 0);
  }
}

int main() {
  pthread_t producer, consumer;
  assert(pthread_create(&producer, NULL, &producerFcn, NULL) == 0);
  assert(pthread_create(&consumer, NULL, &consumerFcn, NULL) == 0);

  while (1)
    ;
}

Testing a Condition Variable's Predicate

每个条件变量都对相关的一个或者多个共享变量具有一个相关的期望或者说断言(predicate)。比如,在上面的代码中,对共享变量avail的期望为avail==0。这段代码体现了一种设计原则:pthread_cond_wait()函数必须在一个while循环中管理,而不是一个if语句。这是因为你永远不知道,当你从pthread_cond_wait()返回后,共享变量的值是否就是你需要的值,还需要对其进行判断。

01-26 14:04