线程的概念
线程:light weight process,轻量级的进程,Linux环境下本质上仍是进程。和进程的区别是,进程有独立的地址空间和PCB,而线程有PCB但是没有独立的地址空间。在Linux环境下,线程是最小的执行单位,而进程是最小分配资源单位。
线程共享资源:
- 文件描述符表。
- 每种信号的处理方式。
- 当前工作目录。
- 用户ID和组ID。
- 内存地址空间(.text/.data/.bss/heap/共享库)。
线程非共享资源:
- 线程id。
- 处理器现场和栈指针(内核栈)。
- 独立的栈空间(用户空间栈)。
- errno变量。
- 信号屏蔽字。
- 进程调度优先级。
线程的优缺点
优点:1,提高程序并发性。2,开销小。3,数据通信,共享数据方便。
缺点:1,库函数,稳定性稍低。2,调试、编写困难,gdb不支持。3,对信号支持不好。
线程的创建和回收
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
struct person{
char name[20];
int age;
};
typedef struct person* Person;
void *func(void *arg){
Person p = (Person)arg;
printf("name = %s,age = %d\n",p->name,p->age);
Person ret = (Person)malloc(sizeof(struct person));
strcpy(ret->name,p->name);
ret->age = p->age+1;
return (void *)ret;
}
int main(int argc, char const *argv[])
{
pthread_t tid;
Person p = (Person)malloc(sizeof(struct person));
strcpy(p->name,"Jack");
p->age = 34;
pthread_create(&tid,NULL,func,(void *)p);
Person ret;
pthread_join(tid,(void**)&ret);
printf("name = %s,age = %d\n",ret->name,ret->age);
free(ret);
system("pause");
return 0;
}
由于线程共享的东西比较多,如果不使用同步,线程之间是竞争的关系,所以,我们无法预测线程之间的执行顺序,并且,在线程执行的过程中,也可能切换线程。下面是一个例子:
#include<stdio.h>
#include<pthread.h>
int i = 0;
void *func(void *arg){
while(1){
i++;
i++;
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid,NULL,func,(void *)NULL);
while(1){
i++;
i++;
if(i % 2 != 0){
printf("error! i = %d is odd\n",i);
break;
}
}
return 0;
}
这个程序每次运行的结果都不一样,这就是与时间有关的错误。产生这种错误有三个条件:
- 共享数据。
- 多个对象竞争。
- 没有合理的同步机制。
要解决这种问题,需要使用同步的机制。
线程使用注意事项
- 需要主线程退出其他线程不退出,主线程应调用pthread_exit方法。
- 要避免僵尸线程,使用pthread_join显示回收,或者使用pthread_detach分离线程或者在pthread_create中指定分离属性。
- malloc和mmap申请的内存可以被其他线程释放。
- 应避免在多线程中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit。
互斥量
Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁会才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的,但通过锁,就将资源的访问变成互斥操作,而后与时间有关的错误也不会产生了。互斥锁只有一把,所以同一个时刻只有一个线程拥有这把锁。
互斥锁实质上是操作系统提供的一把”建议锁“(又称”协同锁“),建议程序中有多线程访问共享资源的时候使用该机制,但,并没有强制限定。因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。如,当A线程对某个全局变量加锁访问,B在访问之前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
mutex有以下的一些函数:
/* 获得一个初始化好的锁 */
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
/* 初始化锁 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
/* 加锁,阻塞 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 加锁,不阻塞 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/* 解锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 释放锁 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
上面的程序可以改成下面的形式:
#include<stdio.h>
#include<pthread.h>
pthread_mutex_t mutex;
int i = 0;
void *func(void *arg){
while(1){
pthread_mutex_lock(&mutex);
i++;
i++;
pthread_mutex_unlock(&mutex);
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_mutex_init(&mutex,NULL);
pthread_create(&tid,NULL,func,(void *)NULL);
while(1){
pthread_mutex_lock(&mutex);
i++;
i++;
if(i % 2 != 0){
printf("error! i = %d is odd\n",i);
break;
}
pthread_mutex_unlock(&mutex);
}
pthread_mutex_destroy(&mutex);
return 0;
}
在做这个demo的时候,犯了一个错误,把unlock写到了检查是否为奇数之前,也会出现上面那种现象,原因在于,有可能恰好在检查前失去CPU,然后执行一次i++之后又得到了CPU(此时虽然已经上锁了,但是不会去检查,会继续执行)。
死锁及其解决方案
一个线程可以通过某种形式的加锁机制来防止别的线程在互斥还没有释放的时候就访问这个资源。值得注意的是,加锁是阻塞的,所以可能会出现这种情况:某个线程在等待另一个线程,而后者也在等待别的线程,这样一直下去,直到这个链条上的线程又在等待第一个线程释放锁。这得到一个任务之间互相等待的连续循环,没有哪个线程能够继续,这被称为死锁。最简单的情况就是自己等自己,如下面的这个程序:
#include<stdio.h>
#include<pthread.h>
int main(int argc, char const *argv[])
{
pthread_mutex_t pit = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&pit);
printf("Lock again\n");
pthread_mutex_lock(&pit);
return 0;
}
再次上锁的时候,需要先释放锁,但是线程本身是无法释放锁的,所以就会进入死循环。
死锁的产生需要同时满足以下4个条件:
- 互斥条件。线程使用的资源中至少有一个是不能共享的。
- 至少有一个线程它必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
- 资源不能被抢占,只能等待其他线程释放资源。
- 必须有循环等待。
这些条件需要全部满足才会产生死锁,所以,为了避免死锁,只需要破坏其中一个条件即可。在程序中,防止死锁最容易的方法是破坏第4个条件。也就是说,我们要注意资源的获取顺序,最好是一致的。如:A获取顺序1,2,3;B顺序也是1,2,3,则不会发生死锁。而如果B的顺序是3,2,1,则容易出现死锁。因为后者会出现互相等待的情况。下面以一个哲学家吃饭的例子来说明这个问题:
#include<stdio.h>
#include<pthread.h>
pthread_mutex_t mutex[5];
void* philosopher(void *arg){
int id = (int)arg;
int left,right;
if(id<4){
left = id;
right = id + 1;
}else if(id == 4){
/* 这里会出现死锁,如果要避免死锁,交换left和right的值即可 */
left = id;
right = 0;
}
while(1){
/* 先拿左边的,再拿右边的 */
pthread_mutex_lock(&mutex[left]);
pthread_mutex_lock(&mutex[right]);
printf("philosopher %d eating\n",id);
pthread_mutex_unlock(&mutex[left]);
pthread_mutex_unlock(&mutex[right]);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t th[5];
for(i=0;i<5;i++){
mutex[i] = PTHREAD_MUTEX_INITIALIZER;
}
for(i=0;i<5;i++){
pthread_create(&th[i],NULL,philosopher,(void *)i);
}
for(i=0;i<5;i++){
pthread_join(th[i],NULL);
}
for(i=0;i<5;i++){
pthread_mutex_destroy(&mutex[i]);
}
return 0;
}
条件变量及生产者消费者模型
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用,给多线程提供一个会和的场所。
/* 获得一个初始化好的锁 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/* 初始化条件变量 */
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
/* 唤醒一个等待该条件变量的线程 */
int pthread_cond_signal(pthread_cond_t *cond);
/* 唤醒全部等待的条件变量 */
int pthread_cond_broadcast(pthread_cond_t *cond);
/* 等待一个条件变量,并且将mutex解锁,唤醒后将mutex加锁 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/* 和wait差不多,只不过到了绝对时间点会直接唤醒 */
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
/* 释放条件变量 */
int pthread_cond_destroy(pthread_cond_t *cond);
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程操作一个共享资源,生产者向其中添加产品,消费者从中消费产品。具体的代码如下:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int i = -1;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER;
void *cfunc(void *arg){
int id = (int)arg;
while(1){
/* 要访问i,必须先加锁 */
pthread_mutex_lock(&lock);
while(i == -1){
/* 消费者等待满 */
pthread_cond_wait(&cond_full,&lock);
}
printf("consumer %d consume %d\n",id,i);
i = -1;
/* 解锁之后通知生产者空了 */
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond_empty);
}
}
void *pfunc(void *arg){
int id = 0;
while(1){
/* 要访问i,必须加锁 */
pthread_mutex_lock(&lock);
while(i != -1){
/* 生产者等待空 */
pthread_cond_wait(&cond_empty,&lock);
}
printf("producer procduce %d\n",++id);
i = id;
/* 解锁后通知消费者满了 */
sleep(1);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond_full);
}
}
int main(int argc, char const *argv[])
{
pthread_t cid1,cid2,pid;
pthread_create(&cid1,NULL,cfunc,(void *)1);
pthread_create(&cid2,NULL,cfunc,(void *)2);
pthread_create(&pid,NULL,pfunc,NULL);
pthread_join(cid1,NULL);
pthread_join(cid2,NULL);
pthread_join(pid,NULL);
pthread_cond_destroy(&cond_empty);
pthread_cond_destroy(&cond_full);
pthread_mutex_destroy(&lock);
return 0;
}
相较于mutex而言,条件变量可以减少竞争。如直接使用mutex,除了生产者,消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果没有产品,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高了程序效率。在我看来,阻塞相当于while循环,而wait是sleep,唤醒之后才会继续执行。
在JAVA中有一个CountDownLatch类,可以用来同步一个或多个线程,强制它们等待由其他线程执行的一组操作,这里我用C语言简单实现了一个类似的场景,就是5个线程同时准备好了,才可以执行后面的操作。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
int n;
void *func(void *arg){
int id = (int)arg;
printf("thread %d preparing\n",id);
sleep(id);
pthread_mutex_lock(&mutex);
n--;
if(n==0){
pthread_cond_broadcast(&cond);
}else{
pthread_cond_wait(&cond,&mutex);
}
pthread_mutex_unlock(&mutex);
printf("thread %d working\n",id);
return (void *)NULL;
}
int main(int argc, char const *argv[])
{
mutex = PTHREAD_MUTEX_INITIALIZER;
cond = PTHREAD_COND_INITIALIZER;
n = 5;
pthread_t pt[5];
int i;
for(i=0;i<5;i++){
pthread_create(&pt[i],NULL,func,(void *)(i+1));
pthread_detach(pt[i]);
}
pthread_exit(NULL);
return 0;
}
信号量
信号量是进化版的互斥锁,也就是从1变成了N。由于互斥锁的粒度较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将真个数据对象锁住,这样虽然达到了多线程操作共享数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与字节使用单线程无异。信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。信号量常用的有以下一些函数:
/* 初始化信号量,值为value,可以设定共享还是非共享 */
int sem_init(sem_t *sem, int pshared, unsigned int value);
/* 释放锁 */
int sem_destroy(sem_t *sem);
/* 信号量减1,如果小于0,就阻塞等待 */
int sem_wait(sem_t *sem);
/* 和wait类似,不阻塞 */
int sem_trywait(sem_t *sem);
/* 和wait类似,到了时间自动解除阻塞 */
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/* 使信号量加1,如果有等待sem的线程,就唤醒这些线程 */
int sem_post(sem_t *sem);
下面是一个使用信号量实现生产者消费者模型的例子:
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#define MAX 100
sem_t sp,sc;
int arr[5];
void *produce(void *arg){
int i = 0;
int n = 1;
while(n<=MAX){
sem_wait(&sp);/* 拿空位置 */
arr[i] = n++;
i = (i+1)%5;
sem_post(&sc);/* 产品数目加1 */
}
return (void *)NULL;
}
void *consume(void *arg){
int i = 0;
int n = 0;
while(n<MAX){
sem_wait(&sc);
n = arr[i];
i = (i+1)%5;
printf("consume %d\n",n);
sem_post(&sp);
}
return (void *)NULL;
}
int main(int argc, char const *argv[])
{
sem_init(&sp,0,5);/* 开始有5个空位置 */
sem_init(&sc,0,0);/* 开始一个产品也没有 */
pthread_t pt[2];
pthread_create(&pt[0],NULL,produce,NULL);
pthread_create(&pt[1],NULL,consume,NULL);
pthread_join(pt[0],NULL);
pthread_join(pt[1],NULL);
sem_destroy(&sp);
sem_destroy(&sc);
return 0;
}
读写锁
与互斥量类似,但读写锁允许更高的并行性。读写锁是一把锁,只不过读写锁有两个不同的加锁方式:可以以读的方式加锁,也可以以写的方式加锁。读写锁有以下的特性:
- 读写锁是”写模式加锁“时,解锁前,所有对该锁加锁的线程都会被阻塞。
- 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果以写模式进行加锁会阻塞。
- 读写锁是“读模式加锁”时,既有视图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读模式锁。读锁、写锁并行阻塞,写锁优先级高。
总结起来就一句话:写独占,读共享,写锁优先级高。
读写锁有以下的一些常用函数:
/* 初始化读写锁 */
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_tryrdlock(pthread_rwlock_t *rwlock);
/* 以写的方式加锁,阻塞 */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/* 以写的方式加锁,不阻塞 */
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 解锁 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
下面以一个简单的例子来说明读写锁的特性:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
pthread_rwlock_t rwlock;
int n;
void *reader(void *arg){
int id = (int)arg;
while(n <= 100){
pthread_rwlock_rdlock(&rwlock);
printf("reading thread %d reading data %d\n",id,n);
sleep(1);
printf("reading thread %d finish reading\n",id);
pthread_rwlock_unlock(&rwlock);
}
return NULL;
}
void *writer(void *arg){
int id = (int)arg;
while(n <= 100){
pthread_rwlock_wrlock(&rwlock);
printf("writing thread %d writing data %d\n",id,++n);
sleep(1);
printf("writing thread %d finish reading\n",id);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
n = 0;
pthread_rwlock_init(&rwlock,NULL);
int i;
pthread_t tid[3];
for(i=0;i<2;i++){
pthread_create(&tid[i],NULL,reader,(void *)(i+1));
}
pthread_create(&tid[2],NULL,writer,(void *)3);
for(i=0;i<3;i++){
pthread_join(tid[i],NULL);
}
return 0;
}
可以看到的现象是,读模式加锁时,其他读线程可以进入,写线程加锁的时候,所有的线程都阻塞。值得注意的是,如果写线程后面不sleep一秒的话,就写线程一个人自己玩,因为解锁之后马上加锁,写线程很容易抢到这把锁。
网友评论