|
|
6/23/2008 谁说我不更新,谁说我不来看?刚刚不知道怎么发神经把握自己的spaces打开了,发现最近的一篇日志正好是去年的今天写的!是不是有点太巧了,整整一年没有写来看过,没有看写过,看了一些留言,感谢扬帆、rwy、弱猪等人还来我这里捧场,非常感谢!
明年我还回来写的(估计)。这都不叫日志,都改叫年中总结了~
6/23/2007 一年没有来这里了...有点点荒凉
没有想过一年后的我会是现在这个样子.我也不能知道再过一年我是什么样子
反正一切都在变好,虽然路有坎坷.但心已经满足.毕竟我,现在的我还是好好的.
8/19/2006 不知道怎样的心情.因为我有同学现在已经没有假期了,走入社会,开始工作了.而我还可以享受假期的休闲时光.对,是休闲时光.
总体感觉在家比较懒惰.自从去年底搬到这里来,觉得安逸了很多.感觉更适合于居住吧...也许是刚搬家后的生活习惯改变,没有培养好.放了家总有种放松自己的感觉.自然浪费的时间也不少了.假期一共回过三趟黄村.第一趟是直接回本校到D301找冯老大帮我看IDE配置问题.第二次和第三次都是周末.一般是待3~4天,在学校的时候还是比较好的.因为身边都是老同学,感觉踏实.学习有劲头.但回了家却开始放松对自己的要求了~自从第三次回来后,总是有事情缠身.
/豆豆准备出国了.没有几天了,来我家只能让他好好玩玩/
/寥媛第一次接团.不过之前还没有去过北大,我陪她先去逛去一圈,这没啥意思,毕竟是大学,不是公园.后来才知道,好伦哥原来是匹萨自助...我原来一直以为是自助烤肉...晕/
/陪波子从村里配了台电脑/
/去刘岩家,还住了一晚,和几个上次认识的朋友一块聚/
/谦儿该走了,于是去张南家也住了一晚,很高兴他还穿的是那件T恤/
/高中一个朋友,寒假刚从英国回来,一起聚了聚/
/昨天和波子,斌,方,姜,蒋,宣,头儿,他们聚了聚,看来姜旭要发达了,最近老和剧组出去跑.毕竟家里有关系就是好啊.开来他应该是我们里面最早发达的了,呵呵/
/今天和李之广去村里,我买了个DC,顺便帮他姐配了台电脑,中午大吃一顿.晚上跟他家里又外面大吃一顿,撑死我了,丫可真能吃啊!还有尼康的L3不错,低端入门绝对就是他了!/
明儿收拾东西滚回学校学习去了...
8/13/2006 自从从康庄回来后好像就没怎么待着,而且也没有忙关于网站的事情.回来后的第一天是补觉,好像睡了整整一天吧~~反正睡的不短...前天被高中的哥们叫他到家里去玩,因为比较远,就住了一宿,昨天回家,今天早上被我弟弟叫出去了.晚上是去奶奶家.回来后又先去弟弟家拿东西,然后在他家迷瞪一会儿,呵呵.十点多一点刚刚到家~眼看马上就要开学了,我的抓紧点了...
8/8/2006 又入康庄3日,两夜中,一整夜失眠,一后半夜失眠.不知何缘.
网站问题,又有了进一步得解决,框架设计基本完成了,不过标签还用不好,今天一下午光设计页面了就...
唉,啥算是从新学起哈~~
上传问题还有有待解决,想到一个不力的因素,就是用jspsarmtupload包的功能上传的速度如果,如果速度过慢还是要重新考虑一下的问题,比较还是ftp协议传送文件是最快的吧~~
回家又该享受享受了~~浑身难受死了 7/31/2006 ftp的信息资源遍历到数据库里面的工作已经完成,本地开启的服务测验已经成功.
下面的工作基本上就是针对网站的功能进行实现,不过还是要用到数据库对每个文件都加以注解这个情况是个问题`~`~方法多种,做到的时候再说吧
今天见到了几位牛人,唉`~`~努了 7/27/2006 今天我把电脑里面的游戏全都删了,游戏害人啊,耽误了我多少时间~全都给我一边去吧! 7/24/2006 今天和0000先生交流了一下,发现了一个我现在的误区.就是把事情复杂化了~
每件事刚刚接触总是以为很复杂很复杂,但真的等到了解了,就会觉得越发的简单.
这么看来,给学校ftp做的这个网站的最初版本0.01阿尔法版已经初有眉目了,前一阵子比较懒惰加上放假回家,杂事太多......进度比较慢,是该抓紧了....
7/16/2006 重写一下计划表:
应岳从远老师的提议,要为校内ftp添加新功能,所以准备搞一个新东西,老岳一开始说能不能到网上找找类似功能的源代码,我想倒不如应用我现有知识和正在学习的东西来完成这样任务.目的是做到以ftp为资源库,用户可以通过网页进行浏览,下载ftp资源.并可以实现排行,更新,附加简介,公告等功能.使校内ftp能更方便的为学生服务~
可能应用到jsp+mysql,IDE:eclipes,service:tomcat.ftp服务器方面应该还是学校的rhel3操作系统,ftp使用的是vsftpd服务
初步的核心问题是lucene的应用,关于全文索引和搜索工具.但是网上关于数据库搜索的资料倒是很少...
初期构想和计划已经基本完成,不过毕竟小弟是初学,边学边做,还是十分生疏...希望多些朋友给出建议和意见
定期还是会做一些开发进程报告的,目的主要是起到自我督促的作用.还请希望大家多多支持...
ps:之前写的有些匆忙,实在是有点语无伦次,条例不清.这回应该好点了~~最近eclipes和tomcat老是有问题~郁闷万分...
后续信息我会发布在 wanxian.blog.wait4c.com 上面,敬请关注 7/8/2006 晚上和爸妈出去遛弯散步,吹这外面的风真的非常舒服,建议大家如果饭后有时间都应该出去到有树有绿地的地方走走,非常爽的说 不知夏天是什么时候来的,真的不希望这个夏天这么快就过去啊,真的希望能有另一个人一起陪我享受这个夏天啊......  6/9/2006 昨天让我认识到一个无耻的老师到底是有多么无耻,很郁闷.接着晚上去食堂吃饭,要了一个茄子,晚上回来就出问题了~觉得有点飘.浑身无力.书也没看好.夜里睡觉,起夜又折腾了一个多小时才在自己假设的梦里睡着...早上更别提了,倒是没有怎么拉肚肚.不过考试前的最后一个上午就这么晃过去了,下午考汇编语言...只好凭借昨天复习的内容死扛了.俩小时终于扛过去了.我真不知道我是怎么回的家唉.回家吃点健胃消食的,还好一些了嗯.希望别再找事烦我了.让我冷静冷静......
对了,是不是该来啥好事了?呵呵 6/8/2006 据最新研究数据表明:北京印刷学院出版学院某一个男性大个头儿动物是一个大傻逼!!绝对无耻大煞笔!!我老万咒丫的!操!!让丫车毁人亡!妻离子散!猪狗不如!一个日本猪!!!!!!!!!!!!!!!!!!!!丫也配他妈的当老师!我就操他大爷了!!这jb连人都不配当!!!纯一个傻逼!! 5/31/2006 真的很快就感觉五月过去了.很喜欢五月,觉得五月能给我带来惊喜,带来希望.突然想起初中的时候,上外教课时,老师问你都喜欢哪个月份的时候,那个抢着喊"五月份""五月份"的那个小孩...但我还是随大家附喝说喜欢八月~因为当时喜欢放假.可现在好像真的不是喜欢放暑假的到来...因为现在的我们,过一个暑假,就少一个暑假...就又老了一岁,快到这个季节枯萎的尽头.
不过这个五月还可以吧.我的好朋友被招进Mr.杨的ERP实习团队,虽然我一再想去,前后找了他们三次都未果,还是因为考虑课程年级的原因吧.不过还有机会的,这个机会实在很好,不过说实话我的水平可能还差很多,就趁这段时间继续努力吧.说真的,现在真的不是很想浪费时间,但是利用起来的时间效率也不是很高...自己要检讨一下自己.压力是有的,困难也是有的,前途自己放眼望去了,接下来就是要扎扎实实走好每一步了~这只是一个起点,五月的终结是又一个起点 5/13/2006 真的没有多大感觉,我的21已经来了,收到的第一份祝福是她给我的,不知是怎样的心情,反正自己懒得去想是什么样的心情.至少现在没有什么过生日的心情,也说不好现在是个什么样的心情.普普通通的一天过去我就又加上新的一岁了,祝自己生日快乐吧.为我的21写出更精彩的篇章来! 5/7/2006 一直没感觉,前一阵子办公交月票卡,脑子一晕,办成了成人月票卡.因为我也不准备坐公交月票车,所以就自当亏了45元钱,但突然有种莫名的焦虑,害怕拿出这张"成人"月票卡.突然想极力证明我还是学生,我有学生证,我也可以办学生卡的!一开始没有感觉到,今天才突然察觉,好像以前总是听人说等真正离开校园,走入社会的时候是最想回到校园的时候...也许现在我人在校园,心已...... 3/31/2006 哈哈,05年的今天,我把爱妻迎进家门,整整一年了啊~~为辛劳的老婆说声!老婆辛苦了!!!
辛苦一年,老婆身体还是很好,毕竟新媳妇嘛~~大病没有,小病几乎全无,就连病毒木马都没染上过(ps:真佩服我rp!)最近少有欠佳的就是老婆的脑子~记忆力当然没有问题,毕竟是镭射5年质保的sata接口,但是总感觉有噪音,可能是老婆辛勤劳动,脑子有点慢了吧,和刚进家门时,那效率绝对是有差别的!一开始我还真挺担心,后来从新清理碎片,删垃圾文件,备份,现在感觉总算是好多了~~哈哈,一周年纪念,我也真是的啥都没准备,那就给老婆三天的假期吧,让老婆好好休息休息!!没有您我的世界将是多么的冰冷灰暗啊!!老婆大人您辛苦了!!!  3/23/2006 一个不明白的问题,也许永远不会明白,也许永远不用去明白.第一次遇到这样的事情,发现了自己 3/18/2006 前天周四,去把头发剪短了,很久很久以前就想,把自己弄成一个秃子,可是没这勇气.直到这学期rwy弄了个秃头才又给了我这个念头~~不过没秃,只是比较短罢了~回到家,我妈一看吓了一大跳.说现在怎么都快认不出我来了~~我去照了照镜子,是啊,真的变了,马小刀说:"人的多换几个面貌的,要不很乏味的".呵呵,真不知道该怎么说自己了~~ 3/13/2006 男,姓薛,名煜峰,身高1.71,山西人,01话剧团团长,面相清秀,戴眼镜,名字很好听,05年夏天时他的锁骨最好看。当我进入大学时,他还是大三的学生。如今已经毕业,步入社会,投入工作。在北京工作一年,06年春节,因家里有事情,外加在本地,被介绍给了一个很不错了单位工作,春节后来北京,就不是回北京,这么简单的意义了.而是为离开做最后的准备吧.说起来,在团里我跟他说话并不是非常非常多,不过他一说要离开北京我总有些紧张.也说不上为什么.两年过去了,这两年里我不相信人能没有变化,但每次见到他,都总觉得他没有变,喜欢这种状态,喜欢这种心情,喜欢这种时光吧?真的很想回到03年的冬天,回到04年的夏天.回到那个时候的我... 老薛,一路走好,祝你工作顺利,家里人身体健康,你也健康. 3/8/2006
转自http://www.anqn.com
引言
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。
为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
下面我们先来尝试编写一个简单的多线程程序。
简单的多线程编程
Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序example1.c。
/* example.c*/ #include #include void thread(void) { int i; for(i=0;i<3;i++) printf("This is a pthread.n"); }
int main(void) { pthread_t id; int i,ret; ret=pthread_create(&id,NULL,(void *) thread,NULL); if(ret!=0){ printf ("Create pthread error!n"); exit (1); } for(i=0;i<3;i++) printf("This is the main process.n"); pthread_join(id,NULL); return (0); } 我们编译此程序:
gcc example1.c -lpthread -o example1
运行example1,我们得到如下结果:
This is the main process. This is a pthread. This is the main process. This is the main process. This is a pthread. This is a pthread.
再次运行,我们可能得到如下结果:
This is a pthread. This is the main process. This is a pthread. This is the main process. This is a pthread. This is the main process.
前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数,pthread_create和pthread_join,并声明了一个pthread_t型的变量。
pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
typedef unsigned long int pthread_t;
它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,void *(*__start_routine) (void *), void *__arg));
第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
函数pthread_join用来等待一个线程的结束。函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。
修改线程的属性
在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。
属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。
#include pthread_attr_t attr; pthread_t tid;
/*初始化属性值,均设为默认值*/ pthread_attr_init(&attr); pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#include #include pthread_attr_t attr; pthread_t tid; sched_param param; int newprio=20;
pthread_attr_init(&attr); pthread_attr_getschedparam(&attr, ¶m); param.sched_priority=newprio; pthread_attr_setschedparam(&attr, ¶m); pthread_create(&tid, &attr, (void *)myfunction, myarg);
线程的数据处理
和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。
1、线程数据
在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
创建键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *))); 第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。
在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。
/* 声明一个键*/ pthread_key_t myWinKey; /* 函数 createWindow */ void createWindow ( void ) { Fl_Window * win; static pthread_once_t once= PTHREAD_ONCE_INIT; /* 调用函数createMyKey,创建键*/ pthread_once ( & once, createMyKey) ; /*win指向一个新建立的窗口*/ win=new Fl_Window( 0, 0, 100, 100, "MyWindow"); /* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/ setWindow(win); /* 将窗口指针值绑定在键myWinKey上*/ pthread_setpecific ( myWinKey, win); }
/* 函数 createMyKey,创建一个键,并指定了destructor */ void createMyKey ( void ) { pthread_keycreate(&myWinKey, freeWinKey); }
/* 函数 freeWinKey,释放空间*/ void freeWinKey ( Fl_Window * win){ delete win; } 这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer)); extern void *pthread_getspecific __P ((pthread_key_t __key)); 这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。
2、互斥锁
互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。
void reader_function ( void ); void writer_function ( void );
char buffer; int buffer_has_item=0; pthread_mutex_t mutex; struct timespec delay; void main ( void ){ pthread_t reader; /* 定义延迟时间*/ delay.tv_sec = 2; delay.tv_nec = 0; /* 用默认属性初始化一个互斥锁对象*/ pthread_mutex_init (&mutex,NULL); pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL); writer_function( ); }
void writer_function (void){ while(1){ /* 锁定互斥锁*/ pthread_mutex_lock (&mutex); if (buffer_has_item==0){ buffer=make_new_item( ); buffer_has_item=1; } /* 打开互斥锁*/ pthread_mutex_unlock(&mutex); pthread_delay_np(&delay); } }
void reader_function(void){ while(1){ pthread_mutex_lock(&mutex); if(buffer_has_item==1){ consume_item(buffer); buffer_has_item=0; } pthread_mutex_unlock(&mutex); pthread_delay_np(&delay); } } 这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。
pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。
3、条件变量
前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。
函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex));
线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。
另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为: extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
函数pthread_cond_signal()的原型为:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。
pthread_mutex_t count_lock; pthread_cond_t count_nonzero; unsigned count; decrement_count () { pthread_mutex_lock (&count_lock); while(count==0) pthread_cond_wait( &count_nonzero, &count_lock); count=count -1; pthread_mutex_unlock (&count_lock); }
increment_count(){ pthread_mutex_lock(&count_lock); if(count==0) pthread_cond_signal(&count_nonzero); count=count+1; pthread_mutex_unlock(&count_lock); }
count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。读者可以试着让两个线程分别运行这两个函数,看看会出现什么样的结果。
函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。
4、信号量
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
/* File sem.c */ #include #include #include #define MAXSTACK 100 int stack[MAXSTACK][2]; int size=0; sem_t sem; /* 从文件1.dat读取数据,每读一次,信号量加一*/ void ReadData1(void){ FILE *fp=fopen("1.dat","r"); while(!feof(fp)){ fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]); sem_post(&sem); ++size; } fclose(fp); } /*从文件2.dat读取数据*/ void ReadData2(void){ FILE *fp=fopen("2.dat","r"); while(!feof(fp)){ fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]); sem_post(&sem); ++size; } fclose(fp); } /*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/ void HandleData1(void){ while(1){ sem_wait(&sem); printf("Plus:%d+%d=%dn",stack[size][0],stack[size][1], stack[size][0]+stack[size][1]); --size; } }
void HandleData2(void){ while(1){ sem_wait(&sem); printf("Multiply:%d*%d=%dn",stack[size][0],stack[size][1], stack[size][0]*stack[size][1]); --size; } } int main(void){ pthread_t t1,t2,t3,t4; sem_init(&sem,0,0); pthread_create(&t1,NULL,(void *)HandleData1,NULL); pthread_create(&t2,NULL,(void *)HandleData2,NULL); pthread_create(&t3,NULL,(void *)ReadData1,NULL); pthread_create(&t4,NULL,(void *)ReadData2,NULL); /* 防止程序过早退出,让它在此无限期等待*/ pthread_join(t1,NULL); }
在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:
Multiply:-1*-2=2 Plus:-1+-2=-3 Multiply:9*10=90 Plus:-9+-10=-19 Multiply:-7*-8=56 Plus:-5+-6=-11 Multiply:-3*-4=12 Plus:9+10=19 Plus:7+8=15 Plus:5+6=11
从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。
小结
多线程编程是一个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子还有很多。希望大家能用多线程技术写出高效实用的好程序来。
|