Mjpeg-streamer源码学习笔记-Main-线程(四)

这一篇的主要难点是main()中条件变量,互斥锁引出的线程问题。

新手写,有不对的请大神指正,鼓励。

本人参考书籍:

Apue第二版

这一篇会介绍Apue的11章线程来更好的理解源码的线程问题,为之后的V4L2多模块多线程编程做准备。

一:线程

1.1引言

本章将进一步深入考察进程,了解如何使用多个控制线程(简称为进程)在单进程环境中执行多个任务。一个进程中的所有线程都可以访问该进程的组成部件,如文件描述符和内存。

1.2线程概念

有了多个控制线程以后,在程序设计时可以把进程设计成在同一时刻能够做不止一件事情,每个线程处理各自独立的任务。好处:

~1 采用同步编程模式,同步比异步简单的多

~2 有了多个控制线程,相互独立的任务的处理就可以交叉进行,只需要为每个任务分配一个单独的线程。

~3 多个线程自动地可以访问相同的存储地址空间和文件描述符

~4 多线程可以把程序中处理用户输入输出的部分与其他部分分开。

线程包含的信息:线程ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的。

1.3 线程标识

就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只在它所属的进程环境中有效。

1.3.1线程引入 pthread_self 和 pthread_equal 原因

解决不用平台的问题。

先介绍2个函数

pthread_equal

-----------------------------------------------------------

#include<pthread.h>

int pthread_equal(pthread_t tid1,pthread_t tid2)

返回值若相等则返回非0值,否则返回0

-----------------------------------------------------------

pthread_self

-----------------------------------------------------------

#include<pthread.h>

pthread_t pthread_self(void)

返回值:调用线程的线程ID

-----------------------------------------------------------

1.引入pthread_equal的原因:

在线程中,线程ID的类型是pthread_t类型,由于在Linux下线程采用POSIX标准,所以,在不同的系统下,pthread_t的类型是不同的,比如在ubuntn下,是unsigned long类型,而在solaris系统中,是unsigned int类型。而在FreeBSD上才用的是结构题指针。 所以不能直接使用==判读,而应该使用pthread_equal来判断。

2.引入pthread_self的原因:

在使用pthread_create(pthread_t *thread_id,NULL,void* (*fun) (void *),void * args);虽然第一个参数中已经保存了线程ID,但是,前提是主线程首先执行时,才能实现的,而如果不是,那么thread指向一个未出划的变量。那么子线程想使用时,应该使用pthread_self();

1.4线程创建

在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。新增的线程可以通过调用pthread_create函数创建

pthread_create

-----------------------------------------------------------

#include<pthread.h>

int pthread_create(pthread_t *restrict tidp,,

const pthread_attr_t *restrict attr,

void *(*start_rtn) (void *),void *restrict arg);

返回值:若成功则返回0,否则返回错误编号

当成功返回时:

tidp:指向的内存单元被设置为新创建线程的线程ID

attr:参数用于定制各种不同的线程属性

start_rtn:新创建的线程从start_rtn函数的地址开始运行

arg:如果需要向start函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入

-----------------------------------------------------------

书中例程11-1解析:打印线程ID

-----------------------------------------------------------

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

pthread_t ntid;

void printids(const char *s)
{
pid_t pid;
pthread_t tid;

pid = getpid();//得到当前进程ID
tid = pthread_self();//得到当前线程ID
printf("%s pid %u tid %u (0x%x)\n",s,(unsigned int)pid,
(unsigned int)tid,(unsigned int)tid);
}

void *thr_fn(void *arg)//作为CREATE函数的start_rtn入口函数
{
printids("new thread:");
return ((void *)0);
}

int main(void)
{
int err;

err = pthread_create(&ntid,NULL,thr_fn,NULL);
if(err !=0)
{
perror("can't create thread:");
return -1;
}
printids("main thread:");
sleep(1);//休眠处理是为了处理主线程和新线程之间的竞争
exit(0);
}

在编译过程中出现以下问题:我们用gcc 11-1.c编译,出现

出错原因:pthread 库不是 linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。

解决方案:加上 -lpthread

编译成功,问题解决。

查看结果:



最后Linux下的结果说明:

果然,两个线程的进程ID是一样的,线程ID不同。而且是main先运行,说明thr_fn的运行机会是在主函数sleep之后获得的。

1.5线程终止

单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流。

(1)线程只是从启动例程(即线程函数)中返回,返回值是线程的退出码(return)

(2)线程可以被同一进程中的其他线程取消

(3)线程调用pthread_exit

pthread_exit

-----------------------------------------------------------

#include<pthread_h>

void pthread_exit(void *rval_ptr)

rval_ptr是一个无参数指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

-----------------------------------------------------------

pthread_join

-----------------------------------------------------------

#include<pthread.h>

int pthread_join(pthread_t thread,void **rval_ptr)

调用它的线程将一直阻塞,直到指定的线程调用pthread_exit,或从启动例程中返回,或被取消。

如果线程只是从它的启动例程中返回,rval_ptr将包含返回码

如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。

如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL

如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL,在这种情况下,调用join函数将等待指定的线程终止,但并不获取线程的终止状态。

-----------------------------------------------------------

书中例程11-2解析:如何获取已终止的线程的退出码

-----------------------------------------------------------

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


void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return ((void *) 1);
}


void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *) 2);
}


int main(void)
{
int err;
pthread_t tid1,tid2;
void *tret;


err = pthread_create(&tid1,NULL,thr_fn1,NULL);
if(err != 0)
{
perror("can't create thread 1\n");
return -1;
}
err = pthread_create(&tid2,NULL,thr_fn2,NULL);
if(err != 0)
{
perror("can't create thread 2\n");
return -1;
}
err = pthread_join(tid1,&tret);
if(err != 0)
{
perror("can't join with thread 1");
return -1;
}
printf("thread 1 exit code %d\n",(int)tret);
err = pthread_join(tid2,&tret);
if(err != 0)
{
perror("can't join with thread 2");
return -1;
}
printf("thread 2 exit code %d\n",(int)tret);
exit(0);
}

-----------------------------------------------------------

运行结果:


而APUE上的结果是thread 1 returning 先运行,thread 2 exiting后运行。在GDB调试中,也是1先运行的,而这里的顺序为什么是这样。我暂时也不清楚,知道的朋友和我说一下。

注:pthread_create和pthread_exit函数的无类型参数指针参数能传递的数值可能不止一个,该指针可以传递包含更复杂信息的结构的地址,但是注意这个结构所使用的内存在调用者完成调用以后必须仍然是有效的,否则就会出现无效或非法内存访问。

-----------------------------------------------------------

书中例程11-3解析:用自动变量(分配在栈上)作为pthread_exit的参数时出现的问题

-----------------------------------------------------------

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
struct foo
{
int a,b,c,d;
};


void printfoo(const char *s,const struct foo *fp)
{
printf(s);
printf(" structure at 0x%x\n",(unsigned)fp);
printf(" foo.a = %d\n",fp->a);
printf(" foo.b = %d\n",fp->b);
printf(" foo.c = %d\n",fp->c);
printf(" foo.d = %d\n",fp->d);
}


void *thr_fn1(void *arg)
{
struct foo foo = {1,2,3,4};
printfoo("thread 1:\n",&foo);
//用自动变量传入pthread_exit中
pthread_exit((void *) &foo);
}


void *thr_fn2(void *arg)
{
printf("thread 2: ID is %d\n",pthread_self());
pthread_exit((void *) 0);
}


int main(void)
{
int err;
pthread_t tid1,tid2;
struct foo *fp;


err = pthread_create(&tid1,NULL,thr_fn1,NULL);
if(err != 0)
{
perror("can't create thread 1");
return -1;
}
err = pthread_join(tid1,(void *)&fp);
if(err != 0)
{
perror("can't join with thread 1");
return -1;
}
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2,NULL,thr_fn2,NULL);
if(err != 0)
{
perror("can't create thread 2");
return -1;
}
sleep(1);
printfoo("parent:\n",fp);
exit(0);
}

-----------------------------------------------------------

运行结果:


可以看出,当主线程访问这个结构时,结构的内容(在线程tid1上的栈的分配)已经改变。注意第二个线程(tid2)的栈是如何覆盖第一个线程的栈的。为了解决这个问题,可以使用全局结构,或者用malloc函数分配结构。


线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。

pthread_cancel

-----------------------------------------------------------

#include<pthread.h>

int pthread_cancel(pthread_t tid);

返回值:成功返回0,否则返回错误编号

在默认的情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是,线程可以选择忽略取消方式或是控制取消方式。注意pthread_cancel并不等待线程终止,它仅仅是提出请求而已。

线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。这样的函数称为线程清理处理程序。线程可以建立多个清理处理程序,处理程序记录在栈中,也就是说它们的执行顺序与它们的注册时的顺序相反。

-----------------------------------------------------------

pthread_cleanup_push

pthread_cleanup_pop

-----------------------------------------------------------

#include<pthread.h>

void pthread_cleanup_push(void (*rtn)(void *),void *arg);

void pthread_cleanup_pop(int execute)

当线程执行以下动作时调用清理函数,调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push来安排的

(1)调用pthread_exit

(2)响应取消请求时

(3)用非零execute参数调用pthread_cleanup_pop

如果execute参数为0,清理函数将不被调用。pop都将删除上次push调用建立的清理处理程序

-----------------------------------------------------------

书中例程11-4解析:如何使用线程清理处理程序

-----------------------------------------------------------

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


void cleanup(void *arg)
{
printf("cleanup: %s\n",(char *)arg);
}


void *thr_fn1(void *arg)
{
printf("thread 1 start \n");
pthread_cleanup_push(cleanup,"thread 1 first handler");
pthread_cleanup_push(cleanup,"thread 1 second  handler");
printf("thread 1 push complete \n");
if(arg)
return ((void *) 1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void *) 1);
}


void *thr_fn2(void *arg)
{
printf("thread 2 start \n");
pthread_cleanup_push(cleanup,"thread 2 first handler");
pthread_cleanup_push(cleanup,"thread 2 second handler");
printf("thread 2 push complete \n");
if(arg)
pthread_exit((void *)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void *) 2);
}


int main(void)
{
int err;
pthread_t tid1,tid2;
void *tret;
        //这里第四个参数不为NULL,和之前不同
err = pthread_create(&tid1,NULL,thr_fn1,(void *) 1);
if(err != 0)
{
perror("can't create thread 1\n");
return -1;
}
err = pthread_create(&tid2,NULL,thr_fn2,(void *) 1);
if(err != 0)
{
perror("can't create thread 2\n");
return -1;
}
err = pthread_join(tid1,&tret);
if(err != 0)
{
perror("can't join with thread 1\n");
return -1;
}
printf("thread 1 exit code %d\n",(int)tret);
err = pthread_join(tid2,&tret);
if(err != 0)
{
perror("can't join with thread 2\n");
return -1;
}
printf("thread 2 exit code %d\n",(int)tret);
exit(0);
}

-----------------------------------------------------------

运行结果:


从输出结果可以看出,两个线程都正确地启动和退出了,但是只调用第二个线程的清理处理程序,所以~

如果线程只是通过它的启动例程中返回而终止的话,那么它的清理程序就不会被调用,还要注意清理处理程序是按照与它们安装时相反的顺序被调用的。(即先是Second再是first)


总结一下线程函数和进程函数之间的相似之处:



在默认情况下,线程的终止状态会保存到对该线程调用join,如果线程已经处于分离状态,线程的底层存储资源可以在线程终止时立即被收回。当线程被分离时,并不能用join函数等待它的终止状态。对分离状态的线程进行join函数调用可以产生失败,返回EINVAL,pthread_detach可以使线程进程分离状态。

-----------------------------------------------------------

pthread_detach

-----------------------------------------------------------

#include<pthread.h>

int pthread_detach(pthread_t tid)

在后面会讲到通过pthread_create函数把线程的属性进行修改,创建的一个已经处于分离状态的线程。

-----------------------------------------------------------


1.6线程同步


1.6.1互斥量(重点)

可以通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥锁(mutex)从本质上说是一把锁,在访问共享资源前对对互斥量进行加锁,在访问完成后释放互斥量上的锁。加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态。第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住。

互斥变量用pthread_mutex_t数据来表示。使用前必须要进行初始化,可以置为PTHREAD_MUTEX_INITIALIZER(只对静态分布的互斥量),也可以调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc),那么在释放内存前需要调用pthread_mutex_destory。


pthread_mutex_init,pthread_mutex_destory

-----------------------------------------------------------

#include<pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexxattr_t *restrict attr)

int pthread_mutex_destory(pthread_mutex_t *mutex);

成功返回0,否则返回错误编号

要用默认的属性初始化互斥量,attr设置为NULL

-----------------------------------------------------------

对互斥锁进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,调用pthread_mutex_unlock


pthread_mutex_lock,pthread_mutex_trylock,pthread_mutex_unlock

-----------------------------------------------------------

#include<pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

int pthread_mutex_unlock(pthread_mutex_t *mutex)

成功返回0,否则返回错误编号

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY

-----------------------------------------------------------

书中实例11-5解析:使用互斥量保护数据结构

-----------------------------------------------------------

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

//新建一个数据结构
struct foo
{
int f_count;//计数
pthread_mutex_t f_lock;//互斥锁
};


struct foo *foo_alloc(void)//分配对象
{
struct foo *fp;
ifi((fp =malloc(sizeof(struct foo))) != NULL)//用malloc分配 最后要用pthread_mutex_destory释放
{
fp->f_count = 1;//初始化为1
if(pthread_mutex_init(&fp->f_lock,NULL) != 0)//互斥量初始化
{
free(fp);
return(NULL);
}
}
return(fp);
}


void foo_hold(struct foo *fp)//给对象添加一个引用
{
pthread_mutex_lock(&fp->f_lock);//加锁
fp->f_count++;//加锁使计数+1
pthread_mutex_unlock(&fp->f_lock);//解锁
}


void foo_rele(struct foo *fp)//释放一个对象的引用 
{
pthread_mutex_lock(&fp->f_lock);

        //计数减1
if(--fp->f_count == 0)//当最后一个引用被释放,对象所占对象内存空间就释放
{
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destory(&fp->f_lock);
free(fp);
}
else
{
pthread_mutex_unlock(&fp->f_lock);
}
}

-----------------------------------------------------------

在对引用计数加1,减1,以及检查引用计数是否是0,这些操作之前需要锁住互斥量。在foo_alloc函数将引用计数初始化为1没有必要加锁,因为在这个操作之前分配线程是唯一引用该对象的线程。但是在这之后如果要将该对象放到一个列表中,那么它就有可能被别的线程发现,因为需要首先对他加锁。

在使用该对象前,线程需要对这个对象的引用计数加1,当对象使用完毕时,需要对引用计数减1,。当最后一个引用被释放时,对象所占的内存空间就被释放。


1.6.2避免死锁

如果线程试图对同一个互斥量加锁两次,那么他自身就会陷入死锁状态。或者,两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。

怎么解决呢,可以通过小心的控制互斥量加锁的顺序来避免死锁的发生:

例如。假如需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前锁住A,那么这两个互斥量就不会产生死锁。

-----------------------------------------------------------

书中实例11-6解析:使用两个互斥量

-----------------------------------------------------------

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


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


struct foo *fh[NHASH];//全局结构


pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;//全局结构


struct foo
{
int f_count;
pthread_mutex_t f_lock;
struct foo *f_next;/* protected by hashlock */
int f_id;
};


struct foo *foo_alloc(void)
{
struct foo *fp;
int idx;
if((fp = malloc(sizeof(struct foo))) != NULL)
{
fp->f_count = 1;
if(pthread_mutex_init(&fp->f_lock,NULL) != 0)
{
free(fp);
return(NULL);
}
idx = HASH(fp);
pthread_mutex_lock(&hashlock);//加锁散列列表锁
fp->f_next = fh[idx];//把新的结构添加到散列存储桶中
fh[idx] = fp;//两个都是foo结构
pthread_mutex_lock(&fp->f_lock);//散列列表解锁前,先锁住新结构中的互斥量
pthread_mutex_unlock(&hashlock);//散列列表锁解锁
pthread_mutex_unlock(&fp->f_lock);//新结构的互斥锁解锁
}
return(fp);
}


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


struct foo *foo_find(int id)//找到一个已存在的对象 锁住散列列表锁然后搜索被请求的结构
{
struct foo *fp;
int idx;


idx = HASH(fp);
pthread_mutex_lock(&hashlock);//散列列表锁上锁
for(fp = fh[idx];fp != NULL; fp = fp->f_next)
{
if(fp->f_id == id)
{
foo_hold(fp);//锁住foo结构的f_lock互斥量
break;
}
}
pthread_mutex_unlock(&hashlock);//解锁
return(fp);
}


void foo_rele(struct foo *fp)
{
struct foo *fp;
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);
/*need to recheck the condition */
if(fp->f_count != 1)//判读是否还需要释放这个结构
{
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* remove from list */
idx = HASH(fp);
tfp = fh[idx];
if(ftp == 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_destory(&fp->f_lock);
free(fp);
}
else
{
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}

-----------------------------------------------------------

hacklock互斥量保护数据结构中的fh散列表和f_next散列链字段。foo结构中的f_lock互斥量保护对foo结构中的其他字段的访问。

比较11-5和11-6,可以看出现在分配alloc函数现在锁住了散列列表锁,把新的结构添加到散列存储桶中,在对散列列表解锁之前,先锁住新结构中的互斥量中。因为新的结构是放在全局列表中的,其他线程可以找到它,所以在完成初始化前,需要阻塞其他试图访问新结构的线程。

foo_find函数锁住散列列表然后搜索被请求的结构,如果找到了,就增加引用计数,并返回该结构的指针。注意加锁的顺序是在find函数中锁定散列列表锁,然后再在hold函数中锁定foo结构中的f_lock互斥量。

现在有了两个锁之后,rele函数更加复杂。如果这是最后一个引用,因为将需要从散列列表中删除这个结构,就要先对这个结构互斥量进行解锁,才可以获取散列列表锁,然后重新获取结构互斥量。从上一次获得结构互斥量以来可能处于被阻塞状态,所以需要重新检查条件,判断是否还需要释放这个结构。如果其他线程在我们为满足锁顺序而阻塞时发现了这个结构并对引用计数加1,那么只需要简单地引用计数减1,对所有的东西解锁然后返回。

但是这样加,解锁太复杂。可以使用散列列表锁来保护结构引用计数,使事情大大简化,结构互斥量可以用于保护foo结构中的其他任何东西。

因此有了实例11-7.

-----------------------------------------------------------

书中实例11-7解析:简化的加减锁

-----------------------------------------------------------

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


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


struct foo *fh[NHASH];


pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;


struct foo
{
int f_count;/* protected by hashlock */
pthread_mutex_t f_lock;
struct foo *f_next;/* protected by hashlock */
int f_id;
};


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


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


struct foo *foo_find(int id)//找到一个已存在的对象
{
struct foo *fp;
int idx;
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
for(fp = fh[idx];fp != NULL; fp = fp->f_next)
{
if(fp->f_id == id)
{
//11-6foo_hold(fp);
fp->f_count++;
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}


void foo_rele(struct foo *fp)
{
struct foo *fp;
int idx;

//11-6  pthread_mutex_lock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
if(--fp->f_count == 0)
{
//11-6pthread_mutex_unlock(&fp->f_lock);
//11-6  pthread_mutex_lock(&hashlock);
//11-6  pthread_mutex_lock(&fp->f_lock);

/*need to recheck the conditioni*/
/*11-6if(fp->f_count != 1)
{
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
} */

/* remove from list */
idx = HASH(fp);
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);
//11-6  pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destory(&fp->f_lock);
free(fp);
}
else
{
//11-6fp->f_count--;
//11-6  pthread_mutex_unlock(&fp->f_lock);

pthread_mutex_unlock(&hashlock);
}
}

-----------------------------------------------------------

相比11-6,11-7简单的多,这样,围绕散列列表和引用计数的锁的排序问题就随之不见了。多线程的软件设计经常要考虑这类折中处理方案

如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁。

如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响。


1.6.3读写锁

读写锁和互斥量类似,前者允许更高的并行性。互斥量只有锁或者不锁两种状态,而且一次只有一个线程可以对其加锁。

读写锁有三种状态:读模式下加锁,写模式下加锁,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

写加锁状态,所有试图对此加锁的线程都会被阻塞

读加锁状态,所有以读模式对它进行加锁都会得到访问权

注意:如果线程希望以写模式对写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。

假如读已经加锁,这时候有写加锁请求,那么读写锁通常会阻塞后面的读模式请求。这样可以避免读模式的长期占用,而等待的写模式请求一直得不到满足。

读写锁也叫共享-独占锁。当它以读模式时,是以共享模式锁住的。当它以写模式时,是以独占模式锁住的。

函数介绍

pthread_rwlock_init,pthread_rwlock_destory

-----------------------------------------------------------

#include<pthread.h>

int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock,const pthread_rwlock_t *restrict attr)

int pthread_rwlock_destory (pthread_rwlock_t *rwlock)

若成功返回0,否则返回错误编号

-----------------------------------------------------------

pthread_rwlock_rdlock,pthread_rwlock_wrlock,pthread_rwlock_unlock

-----------------------------------------------------------

#include<pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)

若成功返回0,否则返回错误编号

-----------------------------------------------------------

书中实例11-8,由于大量涉及到数据结构,队列问题,就不详细解析了,对理解MJPG源码影响不大

-----------------------------------------------------------


1.6.4条件变量(重点)

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的,线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。


函数介绍

pthread_cond_init,pthread_cond_destory

-----------------------------------------------------------

#include<pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond,pthread_condattr_t *restrict attr)

int pthread_cond_destory(pthread_cond_t *cond)

若成功返回0,否则返回错误编号

-----------------------------------------------------------


使用pthread_cond_wait等待条件变为真,如果在给定时间内不能满足,那么会生成一个代表出错码的返回量


pthread_cond_wait,pthread_cond_timedwait

-----------------------------------------------------------

#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,const struct timespec *restrict timeout);

若成功则返回0,否则返回错误编号

-----------------------------------------------------------


传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。


timespec结构体介绍

struct timespec

{

time_t tv_sec;

long   tv_nsec

}

这个结构需要指定原意等待多长时间,时间值是一个绝对数而不是相对数。例如,如果要等待3分钟,就需要把当前时间加上3分钟再转换到timespec结构,而不是把3分钟转换成timespec结构


关于如何转换成timespec结构的函数,用到gettimeofday()

void maketimeout(struct timespec * tsp,long minutes)

{

struct timeval now ;

  /* get the current time */

     gettimeofday(&now);

     tsp->tv_sec = now.tv_sec;

       tsp->tv_nsec = now.tv_usec * 1000; /* usec to nsec */

     /* add the offset to get timeout value */

             tsp->tv_sec += minutes * 60;

}

如果时间到了条件还没有出现,函数将重新获取互斥量然后返回错误ETIMEDOUT。这两个wait函数成功返回时,线程需要重新计算条件,因为其他的线程可能已经在运行并改变了条件。

有两个函数可以通知线程条件已满足。pthread_cond_signal将唤醒等待该条件的某个线程,二pthread_cond_broadcast函数将唤醒等待该条件的所有线程


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)

若成功则返回0,否则返回错误编号

调用它们也称为向线程或条件发送信号。必须注意一定要在改变条件状态以后再给线程发送信号

-----------------------------------------------------------

书中实例11-9解析:使用条件变量和互斥量进行同步

-----------------------------------------------------------

#include<pthread.h>


struct msg
{
struct msg *m_next;
};


struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;//条件
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;//互斥量


void process_msg(void)
{
struct msg *mp;
for(;;)
{
pthread_mutex_lock(&qlock);//互斥量上锁
while(workq == NULL)
pthread_cond_wait(&qready,&qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);//解锁
/* now process the message mp */
}
}


void enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;//把消息队列放到工作队列时,需要占有互斥量
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);//通知线程条件已满足,发送信号时不需要占有互斥量
}

-----------------------------------------------------------

这里的条件是工作队列的状态。用互斥量保护条件,把消息队列放到工作队列时,需要占有互斥量,但向等待线程发送信号时并不需要占有互斥量。这就解释了enqueue_msg函数中的代码,只要线程可以在调用cond_signal之前把消息队列中拖出,就可以在释放互斥量以后再完成这部分工作。

while中检查条件,所以不会存在问题:线程醒来,发现队列仍为空,然后返回继续等待。如果代码不能容忍这种竞争,就需要在向线程发送信号的时候占有互斥量。


至此,APUE内容介绍完了。

现在来看源码中的代码

定义:

pthread_mutex_t db;   /* 互斥锁,数据锁 */
pthread_cond_t  db_update;  /* 条件变量,数据更新的标志 */

Main代码:

/* this mutex and the conditional variable are used to synchronize access to the global picture buffer */
        if(pthread_mutex_init(&global.in[i].db, NULL) != 0) {
            LOG("could not initialize mutex variable\n");
            closelog();
            exit(EXIT_FAILURE);
        }
        if(pthread_cond_init(&global.in[i].db_update, NULL) != 0) {
            LOG("could not initialize condition variable\n");
            closelog();
            exit(EXIT_FAILURE);
        }

这里应用的很简单,就是一起使用了互斥量和条件变量,然后创造无条件竞争。

总结一句就是,一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

当程序进入pthread_cond_wait等待后,将会把mutex进行解锁,
当离开pthread_cond_wait之前,mutex会重新加锁。所以在main中的mutex会被加锁。


至此,Main的分析已经结束。

之前一共讲了get_long,守护进程,插件,动态库,线程,互斥量,条件变量这些主要问题来帮助自己分析mjpg-streamer.c源码。

下面一篇会着重讲input_uvc.c这个文件。

转自:http://blog.csdn.net/s419101357/article/details/11827871

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值