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