[单刷APUE系列]第十一章——线程[2]

线程同步

线程因为共享同一个进程的内存空间,因此资源的访问也应当如同操做系统同样受到限制,一个线程在读取的时候其余线程不能写入,这种限制被称为同步措施。
在学习操做系统原理的时候应当都听过锁的使用。一个资源,若是想要被多个进程访问,最好使用锁机制来确保一致性,不会出现访问冲突。线程也是同样,对于这个状况最简单的想法就是简单的加上一个锁,同一时刻只容许一个线程访问资源。
资源的访问其实是竞争的访问,若是说全部的操做都是原子性的,那么线程就不存在资源冲突,可是很惋惜,目前作不到这点,因此咱们仍是不得不忍受着线程同步的繁琐。
顺便一提,目前现有的现代化操做系统,几乎都是按照线程分配CPU的,而不是按照进程分配的,若是CPU大于线程数,甚至可让一个线程占据一个CPU资源,不过这种状况不多见罢了。多线程

互斥量

pthread模型提供了互斥的锁访问,也就是上面讲过的同一时刻只有一个线程访问资源。当设置了互斥量之后,任何视图对互斥量加锁的线程都会被阻塞直到当前线程释放互斥锁。
互斥变量是用pthread_mutex_t数据类型声明。而且在使用前,必须进行初始化,咱们也能够将其设置为常量PTHREAD_MUTEX_INITIALIZER,也可使用pthread_mutex_init函数初始化。若是是动态的分配变量,则在释放内存前须要使用pthread_mutex_destroy函数

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_init函数建立一个新的互斥量,而且使用attr参数初始化,若是attr为null,则使用默认的属性参数。pthread_mutex_destroy释放分配给mutex参数的资源。
下面是加锁函数性能

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

这三个函数都很简单,lock函数会阻塞调用进程,trylock函数则尝试锁住资源,若是失败则不会阻塞。
下面讲述原著中一个例子,关于C语言引用计数的线程同步。学习

#include <stdlib.h>
#include <pthread.h>

struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
    /* more stuff here */
}

struct foo *foo_alloc(int id);
{
    struct foo *fp;
    
    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
        fp->f_id = id;
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
            free(fp);
            return(NULL);
        }
        /* continue initialization */
    }
    return(fp);
}

void foo_hold(struct foo *fp)
{
    pthread_mutex_lock(&fp->f_lock);
    ++fp->f_count;
    pthread_mutex_unlock(&fp->f_lock);
}

void foo_rele(struct foo*fp)
{
    pthread_mutex_lock(&fp->f_lock);
    if (--fp->f_count == 0) {
        pthread_mutex_lock(&fp->f_lock);
        pthread_mutex_destroy(&fp->lock);
        free(fp);
    } else {
        pthread_mutex_unlock(&fp->f_lock);
    }
}

引用计数是一种古老的内存管理计数,很简单,可是很是有效,性能也很高,上面就是一种C语言下的引用计数,固然,咱们能够将函数以函数指针的形式放置在结构体中,这里就不弄了。
能够看到,结构体很是简单,就一个引用计数成员、互斥量和数据成员,使用foo_alloc函数分配空间,而且在其中初始化互斥量,在foo_alloc函数中咱们并无使用互斥量,由于初始化完毕前分配线程是惟一的能使用的线程。可是在这里例子中,若是一个线程调用foo_rele释放引用,可是在此期间另外一个线程使用foo_hold阻塞了,等第一个线程调用完毕,引用变为0,而且内存被回收,而后就会致使崩溃。操作系统

避免死锁

死锁也是个很常见的状况,一个线程试图对同一个互斥量加锁两次,那么它自身会陷入死锁状态,再好比两个线程都在等待对方已经占有的资源,也会致使死锁,死锁一直是新手的大忌,因此应当仔细控制加锁来避免。pthread_mutex_trylock就是一个很好的方法用来防止死锁产生。线程

#include <stdlib.h>
#include <pthread.h>

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo *fh[NHASH];

pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
    struct foo *f_next;
    /* more stuff here */
}

struct foo *foo_alloc(int id);
{
    struct foo *fp;
    int idx;
    
    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
        fp->f_id = id;
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
            free(fp);
            return(NULL);
        }
        idx = HASH(id);
        pthread_mutex_lock(&hashlock);
        fp->f_next = fh[idx];
        fh[idx] = fp;
        pthread_mutex_lock(&fp->f_lock);
        pthread_mutex_unlock(&hashlock);
        /* continue initialization */
        pthread_mutex_unlock(&fp->f_lock);
    }
    return(fp);
}

void foo_hold(struct foo *fp)
{
    pthread_mutex_lock(&fp->f_lock);
    ++fp->f_count;
    pthread_mutex_unlock(&fp->f_lock);
}

struct foo *foo_find(int id)
{
    struct foo *fp;
    
    pthread_mutex_lock(&hashlock);
    for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
        if (fp->f_id == id) {
            foo_hold(fp);
            break;
        }
    }
    pthread_mutex_unlock(&hashlock);
    return(fp);
}

void foo_rele(struct foo*fp)
{
    struct foo *tfp;
    int idx;
    
    pthread_mutex_lock(&fp->f_lock);
    if (fp->f_count == 1) {
        pthread_mutex_unlock(&fp->f_lock);
        pthread_mutex_lock(&hashlock);
        pthread_mutex_lock(&fp->f_lock);
        if (fp->f_count != 1) {
            --fp->f_cound;
            pthread_mutex_unlock(&fp->f_lock);
            pthread_mutex_unlock(&hashlock);
            return;
        }
        idx = HASH(fp->f_id);
        tfp = fh[idx];
        if (tfp == fp) {
            fh[idx] = fp->f_next;
        } else {
            while (tfp->f_next != fp)
                tfp = tfp->f_next;
            tfp->f_next = fp->f_next;
        }
        pthread_mutex_unlock(&hashlock);
        pthread_mutex_unlock(&fp->f_lock);
        pthread_mutex_destroy(&fp->lock);
        free(fp);
    } else {
        --fp->f_count;
        pthread_mutex_unlock(&fp->f_lock);
    }
}

这是对以前一个例程的改进,程序很容易理解,这里维护了两个互斥量,一个全局互斥量,一个结构体内部的互斥量,或者换言之,全局互斥量是哈希表自身的互斥量,每次的时候,先加锁全局,而后再加锁结构体内部,就能避免死锁。其实上面添加的那么多东西,实际上就是哈希散列公式,而后利用哈希散列公式存取内部的结构体变量。
看了那么久了,可能你们也对其中实现细节很好奇,这里就稍微贴一下代码指针

struct _opaque_pthread_mutex_t {
        long __sig;
        char __opaque[__PTHREAD_MUTEX_SIZE__];
};
typedef struct _opaque_pthread_mutex_t __darwin_pthread_mutex_t;
typedef __darwin_pthread_mutex_t pthread_mutex_t;
#define PTHREAD_MUTEX_INITIALIZER {_PTHREAD_MUTEX_SIG_init, {0}}

相信看了上面的代码,各位对互斥量实现细节应该也有本身的见解了。rest

pthread_mutex_timedlock

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);

这个函数其实没什么好说的,只是在原先的锁定阻塞函数上多了一个超时机制,可是感受没什么用,在实际使用的时候也不多出现,并且很悲伤的是,苹果系统还没有支持这个函数,虽然FreeBSD函数支持。code

读写锁

读写锁是一个更加灵活的锁机制,互斥锁只有两种状态,锁定、不锁定,而读写锁能够有三种状态,读锁定、写锁定、不加锁状态。而且在读锁定状况下,能够有不少线程锁定,而写锁定下,只有一个线程能锁定。其实很是好理解,读取能够有多个,可是写入只能有一个,当写入的时候就不能读取,读取的时候不能写入。在不少教科书上,读写锁也被称为共享互斥锁。接口

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

这两个函数跟以前互斥锁的初始化函数差很少,系统也提供了PTHREAD_RWLOCK_INITIALIZER。就像前面提到的pthread各类类型的实现同样,因为这些变量本质上是一个结构体,也是会分配内存空间的,若是咱们在调用上面函数销毁回收空间以前就使用free函数回收内存,则会致使资源丢失,也就是内存泄露。

int pthread_rwlock_rdlock(pthread_rwlock_t *lock);
int pthread_rwlock_wrlock(pthread_rwlock_t *lock);
int pthread_rwlock_unlock(pthread_rwlock_t *lock);

这三个函数实际上也就是三种读写锁的状态,并且这些函数实际上也有条件版本。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *lock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *lock);

带有超时的读写锁

就像互斥锁同样,Unix标准还规定了带有超时的读写锁,可是很是遗憾,苹果系统一样不存在这个函数。因此这里就不讲解了

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct *timespec *restrict tsptr);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct *timespec *restrict tsptr);

条件变量

条件变量是第三种同步机制。条件变量让线程挂起,直到共享数据上的某些条件获得知足才触发启动。可是在正常状况下须要和互斥量一块儿使用,来防止出现条件竞争。
就像是其余的pthread函数同样,咱们必须先对其进行初始化后才能使用,一样也存在PTHREAD_COND_INITIALIZER常量对其进行初始化。

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);

建立完成之后,须要使用函数等待条件变为真

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

使人惊讶的是,苹果系统竟然提供了超时函数。固然,就像是锁定函数同样,这两个函数都是阻塞的。pthread_cond_wait函数自动解锁mutex参数指向的互斥量,并使当前线程阻塞在cond参数指向的条件变量上,被阻塞的线程能够被pthread_cond_signal函数函数或者pthread_cond_broadcast唤醒,当函数返回的时候,互斥量将被再次锁住。

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

这两个函数很好理解,就是发送了一个信号给cond参数指向的条件。

自旋锁

自旋锁是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较相似,它们都是为了解决对某项资源的互斥使用。不管是互斥锁,仍是自旋锁,在任什么时候刻,最多只能有一个保持者,也就说,在任什么时候刻最多只能有一个执行单元得到锁。可是二者在调度机制上略有不一样。对于互斥锁,若是资源已经被占用,资源申请者只能进入睡眠状态。可是自旋锁不会引发调用者睡眠,若是自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是所以而得名。实际上自旋锁和互斥锁在接口形式和行为方面都很是类似,能够很容易的从一个到另一个,不过,苹果系统并无提供自旋锁,因此这里也就不讲述了。

屏障

屏障是用户协调多个线程并行工做,而且直到全部线程都到达一点之后继续执行,pthread_join函数就是一种屏障。Unix系统还提供了同游的函数用于开发。

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);
int pthread_barrier_wait(pthread_barrier_t *barrier)

其实这些API通过以前的学习,基本上都能看懂了,就是和互斥锁同样,只不过更加的普遍而已。可是很是遗憾的是,苹果系统仍是没有提供这些API,或者是苹果认为这些API并无想象中那么好用,而且在实际使用中也是更少见,因此就将其depreciation。

相关文章
相关标签/搜索