2007年7月29日日曜日

C++によるプログラミング入門1

プログラミングの基礎知識
 こんにちは。C++を使ったプログラミング入門をはじめます。

 この講座はプログラミングの初心者を対象としてC++を楽しくおぼえていくというものです。ぎちぎちした文法や細かい知識は話しません。そういうことは、 この講座をおえれば、一人でも本を読んでおぼえていけるでしょう。
 また、用語はすべてキチンと解説していくと百科事典になってしまうので、ウソにならないように気をつけながら、なるべく簡単な説明で講義を進めていくつもりです。
 C++でプログラミングをする場合、C++用の「処理系」(後述)が必要です。これについてはあらかじめどこかで入手して、簡単な使い方を、知っている近くの人にでも聞いておいてください。有名な処理系としては、例えば、Borland C++、Visual C++、gccなどがあります。もちろん、他にもいろいろ優れたものがあります。どれかを自分のマシンにインストールしておいてください。具体的な使い方はそれぞれ違いますが、これは、知っている人に聞くのが一番です。難しいことは聞かなくてよいですから、今日の講義と次の講義を読んだ後にでも、「エディタ( 処理系についている場合も多いです)を使ってどのようにソース(後述)を入力するか」と、「それをどのように、コンパイル+リンク(後述)するか」だけ聞いてください。  さて、そもそもプログラミングとはどういうことをするのでしょうか。C++では、次の操作を行います。

 まず、ソース(ソースプログラム)とよばれるコンピュータにたいする命令を特別な言語で書きます。この言語にはいろいろなものがあります。それは、普通の言語にも、日本語や英語、ドイツ語、、、などといろいろ種類があるのと同じです。私たちはC++という言語を選びました。このソースがいわゆるプログラムなのです。

 ところでコンピュータはこのソースの内容を直接理解することはできません。ソースをコンピュータにわかる言葉(バイナリなどといいます)に翻訳する必要があるのです。 この翻訳をコンパイルといい、翻訳するソフトをコンパイラといいます。実はコンパイルするだけでは、まだ、そのプログラムを実行することはできません。そのあと、リンクとよばれる作業が必要です(リンクをしてくれるソフトをリンカといいます)。これは、コンパイルされたプログラムを実際に使える形につなぎあわせるということなのですが、初心者にはこの「リンク」はピンとこないと思います。このような知識はずいぶん先にならないと必要ないので、今は「ソースプログラムを実行可能ファイルにするのに、コンパイルやらリンクやらとよばれる作業がある」とだけ理解すれば十分です。

 このコンパイル・リンクは「処理系」などとよばれるソフト(ソフト群)が一発でやってくれますので、気にすることはありません。「コンパイラとリンカ」、あるいは「コンパイラ、リンカ、およびエディタなどを含む統合開発環境を提供するソフト」などを簡単に「処理系」などと言います。また、これらの作業で中心的役割をはたすのはコンパイラなので、全部をひっくるめて「コンパイラ」とよぶ人も多いと思います。厳密には誤りですが、上に書いた事情をわかった上で簡単に言っているのです。
 こうしてできあがった実行可能ファイルをコンピュータは理解して、実行してくれるわけです。

 以上をまとめると、プログラムを書いて実行するということは

1.ソースを書く
2.コンパイル+リンク
3.実行

ということになります。ここで、2と3は簡単にできます。自分のインストールしたC++処理系(つまりコンパイルやリンクをしてくれるソフト)が、クリックやコマンド一発でやってくれるからです。

 Borland C++とVisual C++ .NETを使う場合については、簡単に付録1で説明します。ただ、知っている人が近くにいれば、その人に聞いてみてください。他の開発環境についても同様です。質問を嫌う人もいますが、ソースの入力方法とコンパイル+リンクの簡単な方法なら、相手にもそれほど迷惑にならないはずです。(もちろん、聞くのですから、礼はつくすべきですが、、、。)

 私たちがおぼえることは、要するに「どのようにソースプログラムを書くか」ということになるわけです。

2007年7月12日木曜日

双数组trie树的基本构造及简单优化

一、 基本构造

Trie树是搜索树的一种,来自英文单词"Retrieval"的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现。它本质上是一个确定的有限状态自动机(DFA),每个节点代表自动机的一个状态。在词典中这此状态包括"词前缀","已成词"等。

双数组Trie(Double-Array Trie)是trie树的一个简单而有效的实现,由两个整数数组构成,一个是base[],另一个是check[]。设数组下标为i ,如果base[i],check[i]均为0,表示该位置为空。如果base[i]为负值,表示该状态为词语。Check[i]表示该状态的前一状态, t=base[i]+a, check[t]=i 。

下面举例(源自<<双数组Trie(Double-Array Trie)的数据结构与具体实现>>)来说明用双数组Trie(Double-Array Trie)构造分词算法词典的过程。假定词表中只有“啊,阿根廷,阿胶,阿拉伯,阿拉伯人,埃及”这几个词,用Trie树可以表示为:

我们首先对词表中所有出现的10个汉字进行编码:啊-1,阿-2,唉-3,根-4,胶-5,拉-6,及-7,廷-8,伯-9,人-10。。对于每一个汉字,需要确定一个base值,使得对于所有以该汉字开头的词,在双数组中都能放下。例如,现在要确定“阿”字的base值,假设以“阿”开头的词的第二个字序列码依次为a1,a2,a3……an,我们必须找到一个值i,使得base[i+a1],check[i+a1],base[i+a2], check[i+a2]……base[i+an],check[i+an]均为0。一旦找到了这个i,“阿”的base值就确定为i。用这种方法构建双数组Trie(Double-Array Trie),经过四次遍历,将所有的词语放入双数组中,然后还要遍历一遍词表,修改base值。因为我们用负的base值表示该位置为词语。如果状态i对应某一个词,而且Base[i]=0,那么令Base[i]=(-1)*i,如果Base[i]的值不是0,那么令Base[i]=(-1)*Base [i]。得到双数组如下:

下标


1


2


3


4


5


6


7


8


9


10


11


12


13


14

Base


-1


4


4


0


0


0


0


4


-9


4


-11


-12


-4


-14

Check


0


0


0


0


0


0


0


2


2


2


3


8


10


13

词缀























阿根


阿胶


阿拉


埃及


阿根廷


阿拉伯


阿拉伯人

用上述方法生成的双数组,将“啊”,“阿”,“埃”,“阿根”,“阿拉”,“阿胶”,“埃及”,“阿拉伯”,“阿拉伯人”,“阿根廷”均视为状态。每个状态均对应于数组的一个下标。例如设“阿根”的下标为i=8,那么check[i]的内容是“阿”的下标,而base[i]是“阿根廷”的下标的基值。“廷”的序列码为x=8,那么“阿根廷”的下标为base[i]+x=base[8]+8=12。

二、 基本操作与存在问题

1, 查询
trie树的查询过程其实就是一个DFA的状态转移过程,在双数组中实现起来比较简单:只需按照状态标志进行状态转移即可.例如查询“阿根廷”,先根据“阿”的序列码b=2,找到状态“阿”的下标2,再根据“根”的序列码d=4找到“阿根”的下标base[b]+d=8,同时根据 check[base[b]+d]=b,表明“阿根”是某个词的一部分,可以继续查询。然后再找到状态“阿根廷”。它的下标为y=12,此时base [y]<0,check[y]=base[b]+d=8,表明“阿根廷”在词表中,查询完毕。

查询过程中我们可以看到,对于一个词语的查询时间是只与它的长度相关的,也就是说它的时间复杂度为O(1).在汉语中,词语以单字词,双字词居多,超过三字的词语少之又少.因此,用双数组构建的trie树词典查询是理论上中文机械分词中的最快实现。

2, 插入与删除
双数组的缺点在于:构造调整过程中,每个状态都依赖于其他状态,所以当在词典中插入或删除词语的时候,往往需要对双数组结构进行全局调整,灵活性能较差。

将一个词语插入原有的双数组trie树中,相当于对DFA增加一个状态。首先我们应根据查询方法找出该状态本应所处的位置,如果该位置为空,那好办,直接插入即可。如果该位置不为空。那么我们只好按照构造时一样的方法重新扫描得出该状态已存在的最大前缀状态的BASE值,并由此依次得出该状态后继结点的BASE值。在这其中还要注意CHECK值的相应变化。

例如说,如果"阿拉根"某一天也成为了一个词,我们要在trie树中插入这一状态。按计算它的位置应在8,但8是一个已成状态.所以我们得重新确定"阿拉"这一最大已成前缀状态的BASE值.重新扫描得出BASE[10]=11。这样状态15为"阿拉根",且BASE[15]为负(成词), CHECK[15]=10;状态20为"阿拉佰",且BASE[20]=-4,CHECK=10。

这样的处理其实是非常耗时间的,因为得依次对每一个可能BASE值进行扫描来进行确定最大已成前缀状态的BASE值。这个确定过程在构造时还是基本可以忍受的,毕竟你就算用上一,两天来构造也没有问题(只要你构造完后可以在效运行即可)。但在插入比较频繁时,如果每次都需要那么长的运行时间,那确实是无法忍受的。

双数组删除实现比较简单,只需要将删除词语的对应状态设为空即可――即BASE值,CHECK均为设0。但它存在存在一个空间效率的问题.例如,当我们在上面删除"埃及"这一词语时,状态11被设为空。而状态10则成了一个无用结点――它不成词,而且在插入新词时也不可重用。所以,随着删除的进行,空状态点和无用状态点不断增多,空间的利用率会不断的降低。

三、 简单优化

优化的基本思路是将双数组trie树构建为一种动态检索方法,从而解决插入和删除所存在的问题。

1, 插入优化
在插入需要确定新的BASE值时,我们是只需要遍历空状态的。非空状态的出现意味着某个BASE值尝试的打败,我们可以完全不必理会。所以,我们可以对所有的空状态构建一个序列,在确定BASE值时只需要扫描该序列即可。
对双数组中的空状态的递增结点r1,r2, …, rm,我们可以这样构建这一空序列:
CHECK[ri]=−ri+1 (1 i m−1),
CHECK[rm]=−(DA_SIZE+1)
其中r1= E_HEAD,为第一个空值状态对应的索引点。这样我们在确定BASE值时只需扫描这一序列即可。这样就省去了对非空状态的访问时间。

这种方法在空状态并不太多的情况下可以很大程度的提高插入速度。

2, 删除优化
1) 无用结点
对于删除叶结点时产生的无用结点,可以通过依次判断将它们置为空,使得可在插入新词时得以重用。例如,如果我们删除了上例中的"阿根廷",可以看到"阿根"这一状态没有子状态,因此也可将它置为空。而"阿"这一状态不能置空,因为它还有两个子状态。

2) 数组长度的压缩
在删除了一个状态后,数组末尾可能出现的连续空状态我们是可以直接删除的。另外我们还可以重新为最大非空索引点的状态重新确定BASE值,因为它有可能已经由于删除的进行而变小。这们我们可能又得以删除一些空值状态。

2007年6月16日土曜日

プロセス間通信の方法の一つとしてsocketを使う

概要

プロセス間通信の方法の一つとしてsocketを使う。
通信を行う二つのプロセスのうち、一方をserver、もう一方をclientとする。
serverは通信を行うための口を用意する。
通信の準備

通信を行う口は以下の手順で用意する。これはserverが行う。

* socketを作る。(socket)
* socketに名前を付ける。(bind)
* 受け入れOKを示す。(listen)

これで通信を行う口は用意された。この後、clientは自分で作ったsocketをこの口に結合(connect)し、serverはそれを受け入れる(accept)。serverのsocketに名前を付けるのは、clientがその名前のsocketに結合するのに識別名が必要だからである。
connectとacceptはどちらが先に発行されてもかまわない。お互い結合されるのを待つ。

socketの名前

socketを作るとき、定義域としてAF_UNIXかAF_INETを指定する。それぞれ、UNIXドメイン、INETドメインと呼ばれる。
名前を付けるときはそのドメインの中で一意でなければならない。
UNIXドメインは、ひとつの計算機がその範囲である。この中で一意であると保証するのに簡単な方法としてファイルがある。そこで、名前を作るとその名前のファイルが作られる。こうすれば、そのシステムで一意であることが保証される。ただし、作られたファイルは残ってしまう。通信が終了しても自動的に消してはくれない。
INETドメインでは、ネットワークで結合されたすべてがその範囲である。この中で一意に名前を定めるのは容易ではない。そこで、まず計算機にネットワークで一意な名前を付け、それと組合せて名前を付ける。具体的には、計算機に付けられる名前はIPアドレスである、組み合わされるのはポート番号である。つまり、IPアドレス+ポート番号でINETdドメインで一意な名前を付ける。
通信

お互い結合してしまえば、後は通信するだけである。
送信はsend/writeで、受信はrecv/readで行う。socketを作るとファイルディスクリプタが与えられるので、低レベルのファイルアクセスと大体同じことができるので、read/writeも使える。通信と言ってもそんなに特殊なことをするわけではない。
送信は送りつけるだけだが、受信は相手から何か送られてくるのを待つので、間違えるとデッドロックしてしまう。ただし、結合そのものが切れてしまうと、recv/readで待っていた場合受信データ長0で抜けてきてくれる。
通信の終了

プロセスそのものを終わらせてしまってもいいのだが、一応終了の手順は以下の通りである。

* 結合を切る(shutdown)
* socketをクローズ(close)

UNIXドメインの場合、付けた名前のファイルが残るので、これを消しておく(unlink)。

プログラム例

以下のプログラム例は、serverがclientからのメッセージを受信して表示するものである。

* UNIXドメイン版 server
* UNIXドメイン版 client
* INETドメイン版 server
* INETドメイン版 client
* Makefile

複数のclientと通信するserver

ここまでは基本ということで1対1の通信の話をしてきたが、serverと呼ばれるものであれば複数のclientを通信する場合もある。こういう場合、selectを使うとよい。
これは、待ちたいフィルディスクリプタのリストを渡して、どれか来たら抜けて来てくれる。時間を指定すればその時間を過ぎれば何も来ていなくても抜けて来てくれる。
以下のプログラム例は、これを使って、複数のclientからの要求を処理するserverと、1秒毎にメッセージを送るclientである。UNIXドメイン版のみだが、もちろんINETドメインでも動作する。また、無限ループするプログラムなので、signalで終了を検知した場合終了処理をするようにしてある。

* server
* client
* Makefile

実際に使って気付いたこと

実際使ってみて気付いたことを以下に記すが、いろいろな環境で試したわけではないので、もしかしたら大丈夫かもしれない。

* selectを使って、複数のファイルディスクリプタを扱えるのはいいのだけど、最大で64までしか使えない。これ以上になると落ちる。
後でわかったことだが、一つのプロセスがオープンできるファイルの数は、limits.hのOPEN_MAXで既定されていて、これが64だから。(1998.11.19)
* UNIXドメインやINETドメインでもlocalhostの場合は、送受信はどんなデータでも大丈夫だが、INETドメインで別の計算機と通信する場合は文字列でないとちゃんとデータの受け渡しができない。

プロセス間通信の方法の一つとしてsocketを使う

概要

プロセス間通信の方法の一つとしてsocketを使う。
通信を行う二つのプロセスのうち、一方をserver、もう一方をclientとする。
serverは通信を行うための口を用意する。
通信の準備

通信を行う口は以下の手順で用意する。これはserverが行う。

* socketを作る。(socket)
* socketに名前を付ける。(bind)
* 受け入れOKを示す。(listen)

これで通信を行う口は用意された。この後、clientは自分で作ったsocketをこの口に結合(connect)し、serverはそれを受け入れる(accept)。serverのsocketに名前を付けるのは、clientがその名前のsocketに結合するのに識別名が必要だからである。
connectとacceptはどちらが先に発行されてもかまわない。お互い結合されるのを待つ。

socketの名前

socketを作るとき、定義域としてAF_UNIXかAF_INETを指定する。それぞれ、UNIXドメイン、INETドメインと呼ばれる。
名前を付けるときはそのドメインの中で一意でなければならない。
UNIXドメインは、ひとつの計算機がその範囲である。この中で一意であると保証するのに簡単な方法としてファイルがある。そこで、名前を作るとその名前のファイルが作られる。こうすれば、そのシステムで一意であることが保証される。ただし、作られたファイルは残ってしまう。通信が終了しても自動的に消してはくれない。
INETドメインでは、ネットワークで結合されたすべてがその範囲である。この中で一意に名前を定めるのは容易ではない。そこで、まず計算機にネットワークで一意な名前を付け、それと組合せて名前を付ける。具体的には、計算機に付けられる名前はIPアドレスである、組み合わされるのはポート番号である。つまり、IPアドレス+ポート番号でINETdドメインで一意な名前を付ける。
通信

お互い結合してしまえば、後は通信するだけである。
送信はsend/writeで、受信はrecv/readで行う。socketを作るとファイルディスクリプタが与えられるので、低レベルのファイルアクセスと大体同じことができるので、read/writeも使える。通信と言ってもそんなに特殊なことをするわけではない。
送信は送りつけるだけだが、受信は相手から何か送られてくるのを待つので、間違えるとデッドロックしてしまう。ただし、結合そのものが切れてしまうと、recv/readで待っていた場合受信データ長0で抜けてきてくれる。
通信の終了

プロセスそのものを終わらせてしまってもいいのだが、一応終了の手順は以下の通りである。

* 結合を切る(shutdown)
* socketをクローズ(close)

UNIXドメインの場合、付けた名前のファイルが残るので、これを消しておく(unlink)。

プログラム例

以下のプログラム例は、serverがclientからのメッセージを受信して表示するものである。

* UNIXドメイン版 server
* UNIXドメイン版 client
* INETドメイン版 server
* INETドメイン版 client
* Makefile

複数のclientと通信するserver

ここまでは基本ということで1対1の通信の話をしてきたが、serverと呼ばれるものであれば複数のclientを通信する場合もある。こういう場合、selectを使うとよい。
これは、待ちたいフィルディスクリプタのリストを渡して、どれか来たら抜けて来てくれる。時間を指定すればその時間を過ぎれば何も来ていなくても抜けて来てくれる。
以下のプログラム例は、これを使って、複数のclientからの要求を処理するserverと、1秒毎にメッセージを送るclientである。UNIXドメイン版のみだが、もちろんINETドメインでも動作する。また、無限ループするプログラムなので、signalで終了を検知した場合終了処理をするようにしてある。

* server
* client
* Makefile

実際に使って気付いたこと

実際使ってみて気付いたことを以下に記すが、いろいろな環境で試したわけではないので、もしかしたら大丈夫かもしれない。

* selectを使って、複数のファイルディスクリプタを扱えるのはいいのだけど、最大で64までしか使えない。これ以上になると落ちる。
後でわかったことだが、一つのプロセスがオープンできるファイルの数は、limits.hのOPEN_MAXで既定されていて、これが64だから。(1998.11.19)
* UNIXドメインやINETドメインでもlocalhostの場合は、送受信はどんなデータでも大丈夫だが、INETドメインで別の計算機と通信する場合は文字列でないとちゃんとデータの受け渡しができない。

プロセス間通信の方法の一つとしてsocketを使う

概要

プロセス間通信の方法の一つとしてsocketを使う。
通信を行う二つのプロセスのうち、一方をserver、もう一方をclientとする。
serverは通信を行うための口を用意する。
通信の準備

通信を行う口は以下の手順で用意する。これはserverが行う。

* socketを作る。(socket)
* socketに名前を付ける。(bind)
* 受け入れOKを示す。(listen)

これで通信を行う口は用意された。この後、clientは自分で作ったsocketをこの口に結合(connect)し、serverはそれを受け入れる(accept)。serverのsocketに名前を付けるのは、clientがその名前のsocketに結合するのに識別名が必要だからである。
connectとacceptはどちらが先に発行されてもかまわない。お互い結合されるのを待つ。

socketの名前

socketを作るとき、定義域としてAF_UNIXかAF_INETを指定する。それぞれ、UNIXドメイン、INETドメインと呼ばれる。
名前を付けるときはそのドメインの中で一意でなければならない。
UNIXドメインは、ひとつの計算機がその範囲である。この中で一意であると保証するのに簡単な方法としてファイルがある。そこで、名前を作るとその名前のファイルが作られる。こうすれば、そのシステムで一意であることが保証される。ただし、作られたファイルは残ってしまう。通信が終了しても自動的に消してはくれない。
INETドメインでは、ネットワークで結合されたすべてがその範囲である。この中で一意に名前を定めるのは容易ではない。そこで、まず計算機にネットワークで一意な名前を付け、それと組合せて名前を付ける。具体的には、計算機に付けられる名前はIPアドレスである、組み合わされるのはポート番号である。つまり、IPアドレス+ポート番号でINETdドメインで一意な名前を付ける。
通信

お互い結合してしまえば、後は通信するだけである。
送信はsend/writeで、受信はrecv/readで行う。socketを作るとファイルディスクリプタが与えられるので、低レベルのファイルアクセスと大体同じことができるので、read/writeも使える。通信と言ってもそんなに特殊なことをするわけではない。
送信は送りつけるだけだが、受信は相手から何か送られてくるのを待つので、間違えるとデッドロックしてしまう。ただし、結合そのものが切れてしまうと、recv/readで待っていた場合受信データ長0で抜けてきてくれる。
通信の終了

プロセスそのものを終わらせてしまってもいいのだが、一応終了の手順は以下の通りである。

* 結合を切る(shutdown)
* socketをクローズ(close)

UNIXドメインの場合、付けた名前のファイルが残るので、これを消しておく(unlink)。

プログラム例

以下のプログラム例は、serverがclientからのメッセージを受信して表示するものである。

* UNIXドメイン版 server
* UNIXドメイン版 client
* INETドメイン版 server
* INETドメイン版 client
* Makefile

複数のclientと通信するserver

ここまでは基本ということで1対1の通信の話をしてきたが、serverと呼ばれるものであれば複数のclientを通信する場合もある。こういう場合、selectを使うとよい。
これは、待ちたいフィルディスクリプタのリストを渡して、どれか来たら抜けて来てくれる。時間を指定すればその時間を過ぎれば何も来ていなくても抜けて来てくれる。
以下のプログラム例は、これを使って、複数のclientからの要求を処理するserverと、1秒毎にメッセージを送るclientである。UNIXドメイン版のみだが、もちろんINETドメインでも動作する。また、無限ループするプログラムなので、signalで終了を検知した場合終了処理をするようにしてある。

* server
* client
* Makefile

実際に使って気付いたこと

実際使ってみて気付いたことを以下に記すが、いろいろな環境で試したわけではないので、もしかしたら大丈夫かもしれない。

* selectを使って、複数のファイルディスクリプタを扱えるのはいいのだけど、最大で64までしか使えない。これ以上になると落ちる。
後でわかったことだが、一つのプロセスがオープンできるファイルの数は、limits.hのOPEN_MAXで既定されていて、これが64だから。(1998.11.19)
* UNIXドメインやINETドメインでもlocalhostの場合は、送受信はどんなデータでも大丈夫だが、INETドメインで別の計算機と通信する場合は文字列でないとちゃんとデータの受け渡しができない。

selectを使う

ここでは、selectを使って複数のソケットからデータを受け取る方法を説明したいと思います。
select

普通の状態では、recvやrecvfromはデータが受信できるまでブロッキングします。ソケットを一つしか利用していない場合にはブロッキングは非常に便利なのですが、ソケットが複数になると困ってしまいます。複数のソケットを扱うとき、片方のソケットでブロッキングしたままになってしまうと他のソケットにデータが到着しても受信が出来なくなってしまいます。そのため、複数のソケットを扱っていると、どのソケットからデータが受信可能か知りたくなります。

ブロッキングとは、関数が返ってこない事を表します。例えば、recvはデータを受信して関数が戻ってきます。言い方を変えると、データを受信するまでブロックしています。 recvやrecvfromをブロッキングしないノンブロッキング方式で使う事も可能ですが、ここではブロッキング方式のまま使う方法を説明します。

そのような機能を提供するのがselectです。 selectを使うと、データが受信可能なソケットでのみrecvやrecvfromを実行することができます。 selectは登録したソケットをブロッキング状態で監視し、どれかがデータ受信するとブロッキング状態を解除します。
selectを使うサンプルコード

selectは受信可能かどうかだけではなく、送信可能かどうか、エラーがあるかなどもチェックできますがここでは受信可能かどうかをチェックする方法のみをとりあげます。

下記サンプルコードでは、UDPソケットを2つ作成しています。作成した2つのUDPソケットは、selectに登録してselectはブロッキング状態に入ります。 UDPソケットにデータが到着するとrecvを行い、受信した内容を表示します。


#include
#include

int
main()
{
SOCKET sock1, sock2;
struct sockaddr_in addr1, addr2;
fd_set fds, readfds;
char buf[2048];
WSADATA wsaData;

WSAStartup(MAKEWORD(2,0), &wsaData);

// 受信ソケットを2つ作ります
sock1 = socket(AF_INET, SOCK_DGRAM, 0);
sock2 = socket(AF_INET, SOCK_DGRAM, 0);

addr1.sin_family = AF_INET;
addr2.sin_family = AF_INET;

addr1.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addr2.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

// 2つの別々のポートで待つために別のポート番号をそれぞれ設定します
addr1.sin_port = htons(11111);
addr2.sin_port = htons(22222);

// 2つの別々のポートで待つようにbindします
bind(sock1, (struct sockaddr *)&addr1, sizeof(addr1));
bind(sock2, (struct sockaddr *)&addr2, sizeof(addr2));

// fd_setの初期化します
FD_ZERO(&readfds);

// selectで待つ読み込みソケットとしてsock1を登録します
FD_SET(sock1, &readfds);
// selectで待つ読み込みソケットとしてsock2を登録します
FD_SET(sock2, &readfds);

// 無限ループです
// このサンプルでは、この無限ループを抜けません
while (1) {
// 読み込み用fd_setの初期化
// selectが毎回内容を上書きしてしまうので、毎回初期化します
memcpy(&fds, &readfds, sizeof(fd_set));

// fdsに設定されたソケットが読み込み可能になるまで待ちます
select(0, &fds, NULL, NULL, NULL);

// sock1に読み込み可能データがある場合
if (FD_ISSET(sock1, &fds)) {
// sock1からデータを受信して表示します
memset(buf, 0, sizeof(buf));
recv(sock1, buf, sizeof(buf), 0);
printf("%s\n", buf);
}

// sock2に読み込み可能データがある場合
if (FD_ISSET(sock2, &fds)) {
// sock2からデータを受信して表示します
memset(buf, 0, sizeof(buf));
recv(sock2, buf, sizeof(buf), 0);
printf("%s\n", buf);
}
}

// このサンプルでは、ここへは到達しません
closesocket(sock1);
closesocket(sock2);

WSACleanup();

return 0;
}

selectを使うサンプルコードにデータを送信するコード

selectのサンプルコードは、UDPの11111番と22222番のポートでデータを待っています。 UDPを使って11111番と22222番ポートにデータを送信するサンプルを以下に示します。


#include
#include

int
main()
{
SOCKET sock;
struct sockaddr_in dest1, dest2;
char buf[1024];
WSADATA wsaData;

WSAStartup(MAKEWORD(2,0), &wsaData);

sock = socket(AF_INET, SOCK_DGRAM, 0);

dest1.sin_family = AF_INET;
dest2.sin_family = AF_INET;

dest1.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
dest2.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

dest1.sin_port = htons(11111);
dest2.sin_port = htons(22222);

memset(buf, 0, sizeof(buf));
_snprintf(buf, sizeof(buf), "data to port 11111");
sendto(sock,
buf, strlen(buf), 0, (struct sockaddr *)&dest1, sizeof(dest1));

memset(buf, 0, sizeof(buf));
_snprintf(buf, sizeof(buf), "data to port 22222");
sendto(sock,
buf, strlen(buf), 0, (struct sockaddr *)&dest2, sizeof(dest2));

closesocket(sock);

WSACleanup();

return 0;
}

最後に

ここでは、UDPを使ったサンプルを示しましたが、TCPを使っても同様の事が出来ます。 TCPの場合は、acceptした後のソケットだけではなく、listenしているソケットに対するselectも可能です。また、winsockにはWindowsイベント方式でイベントが飛んでくるselectもあります。

SOCKETプログラミング

このページではBerkeley SOCKET、Winsock及びJava Socketのプログラミングについて紹介する。SOCKETも好きだな。

 記述例およびサンプルに含まれるファイルの全部、または一部を使用したことによる損害等について、一切の責任を負いません。また、サンプルの文字コードはS-JISで提供しますので、ご使用の際はWindowsからFTPするなどして適切な文字コードに変換してください。尚、サンプル中には説明の簡略化のため意味のないコードや、実用上問題のあるコードも含まれていますのでご注意ください。

1. [UNIX]スレッドでrecv
2. [UNIX]selectを使う
3. [Winsock]スレッドでrecv
4. [Winsock]selectを使う
5. [Winsock]イベントを使う
6. [Java]Socket(クライアント)
7. [Java]ServerSocket(サーバー)

[UNIX]スレッドでrecv
 SOCKETを使う上で考慮しなければならないのが受信の実現方法だ。データを受信する関数であるrecvがブロッキングするからだ。僕が一番好きなのはスレッドを使用する方法だ。受信専用のスレッドを新たに生成すればrecvがブロッキングしても影響はそのスレッドのみに限定されるので、例えばキー入力などは平行して処理されるので問題はなくなる。

 今までスレッドを使用したことがない方は、スレッドと聞くだけで躊躇されるかもしれないが、実際にやってみればそれほど難しいものではない。スレッドに関しては別にページを用意しているのでそちらも参照して欲しい。

<受信スレッドの生成>

/* 受信スレッド生成 */
pthread_create(&tid,NULL,waitReceiveThread,NULL);
pthread_join(tid,NULL); /* 受信スレッド終了待ち */

<受信スレッドルーチン>

void*
waitReceiveThread(void* pParam)
{
char buf[1024];
int recvSize;

while(1)
{
/* データ受信待ち */
recvSize = recv(fd,buf,1024,0);

printf("Received %d bytes\n",recvSize);

if(recvSize == 0) /* 相手CLOSE? */
{
printf("check\n");
close(fd);
break;
}
}

return NULL;
}

 受信スレッドルーチンは単にrecvのループになっている。recvはデータを受信するまで戻ってこないのでCPU時間は消費しない。SOCKETがクローズされるとrecvは戻り値=0で戻ってくるので、その場合にはループをbreakする処理を入れている。受信スレッドの生成元スレッドは pthread_joinで新たに生成したスレッドの終了を待機しているので、受信スレッドルーチンが終了すれば生成元スレッドも終了する。
[UNIX]selectを使う
 正直言って僕はselectはあまり好きではない。でも、とりあえず載せておく。まず、FD_ZEROマクロでfd_setを初期化します。次にFD_SETマクロでselectで待ちたいSOCKETの fdを設定する。このfd_setを使ってselectを呼び出すと、データを受信するまでselectはブロックするがCPU時間は消費しない。 selectから戻ったところでFD_ISSETマクロを用いて該当fdの読み込みデータがあるかどうかを調べ、データがある場合はrecvを呼び出す。

int
waitReceive(void)
{
fd_set fds;
char buf[1024];
int recvSize;
/* バッチ共通初期処理 */
while(1) /* メッセージループ */
{
FD_ZERO(&fds); /* fd_set初期化 */
FD_SET(fd,&fds); /* fd設定 */
select(fd + 1,&fds,NULL,NULL,NULL); /* データ受信待ち */

if(FD_ISSET(fd,&fds)) /* 該当fd読み込みデータあり? */
{
/* データ受信 */
recvSize = recv(fd,buf,1024,0);

printf("Received %d bytes\n",recvSize);

if(recvSize == 0) /* 相手CLOSE? */
{
printf("check\n");
close(fd);
break;
}
}
}

return 0;
}

[Winsock]スレッドでrecv
 Win32でもスレッドが使用できるので、新たにスレッドを生成して通常の処理は平行して行いながらrecvで待つことができる。Win32のスレッドモデルはpthreadに比べるとやや煩雑になっており、加えてWinsockでは様々な処理方式を選択可能なのだが、スレッドを使用した方式が最もシンプルであろう。この例ではCのライブラリを使用するために、_beginthreadexを使用しているが、CreateThreadでも同じだ。スレッドに関しては別にページを用意しているのでそちらも参照して欲しい。

<受信スレッドの生成>

/* 受信スレッド生成 */
hThread = (HANDLE)_beginthreadex(NULL,0,waitReceiveThread,NULL,0,&threadId);

WaitForSingleObject(hThread,INFINITE); /* 受信スレッド終了待ち */
CloseHandle(hThread); /* ハンドルクローズ */

<受信スレッドルーチン>

DWORD WINAPI
waitReceiveThread(LPVOID pParam)
{
char buf[1024];
int recvSize;

while(1)
{
/* データ受信待ち */
recvSize = recv(fd,buf,1024,0);

printf("Received %d bytes\n",recvSize);

if(recvSize == 0) /* 相手CLOSE? */
{
printf("check\n");
closesocket(fd); /* CLOSE */
break;
}
}

return 0;
}

 この例でも受信スレッドルーチンの生成元スレッドは、pthreadのときと同様に新たに生成したスレッドの終了を待機している。Win32ではスレッドのハンドルがシグナル状態になるのをWaitForSingleObjectで待機する。受信スレッドルーチンが終了すれば生成元スレッドも終了する。

スレッドrecvのサンプル(ソースはLinuxでもコンパイルできるようになっている。UNIXとWindowsの違いを見てみて欲しい。)
[Winsock]selectを使う
 selectを使う場合はUNIXのSOCKETとまったく同じだ。よりWindowsっぽく書くなら、WSAAsyncSelectを使うという手があるが、こちらはメッセージを使用する。メッセージを使用するにはウィンドウが必要になる僕はメッセージの仕組みをあまり詳しくないので専らイベントを使用する。
[Winsock]イベントを使う
 Winsock2.0ではWin32のイベントオブジェクトを使用することができる。メッセージを利用しないのでウィンドウが必要なく、GUIがないプログラムで使いやすい。Anpsockもこの方式を使っている。ところが、イベントを使ったWinsockの解説書はあまりない。そもそも、Winsock2.0の解説書が少ない。

 イベントに関しては別にページを用意しているのでそちらも参照して欲しい。

int
waitReceive(void)
{
char buf[1024];
int recvSize;
int nRet;
HANDLE hEvent = WSACreateEvent();
WSANETWORKEVENTS events;

WSAEventSelect(fd,hEvent,FD_READ|FD_CLOSE);

while(1)
{
/* イベント待ち */
nRet = WSAWaitForMultipleEvents(1,&hEvent,FALSE,WSA_INFINITE,FALSE);

if(nRet == WSA_WAIT_FAILED)
{
printf("!!!!!!!!!!!!!!!!!!!!1\n");
break;
}

/* イベントの調査 */
if(WSAEnumNetworkEvents(fd,hEvent,&events) == SOCKET_ERROR)
{
printf("???\n");
}
else
{
/* CLOSE */
if(events.lNetworkEvents & FD_CLOSE)
{
printf("close\n");
closesocket(fd);
break;
}
/* READ */
if(events.lNetworkEvents & FD_READ)
{
recvSize = recv(fd,buf,1024,0);
printf("Received %d bytes\n",recvSize);
}
}
}

WSACloseEvent(hEvent); /* イベントクローズ */

return 0;
}

イベントrecvのサンプル
[Java]Socket(クライアント)
 Javaでクライアントのソケットを作成するにはホストとポートを指定してSocketをnewするだけである。たったのこれだけでCで言うところのsocketからconnectの呼び出しまで済んでしまうから驚きである。

// クライアントソケット作成
Socket conn = new Socket("localhost",port);
System.out.println("Connecting to port " + port);
// データ受信スレッド生成
RecvData rd = new RecvData(conn.getInputStream());
rd.start();

 データの受信は他の言語と同様スレッドを用いている。実際にサンプルを見ていただければお分かりになると思うが、わずか5、60行でクライアントのプログラムが書けるのである。素晴らしい!

class RecvData extends Thread {
InputStream st;

public RecvData(InputStream st) {
this.st = st;
}

public void run() {
int data;

try {
// データ読み込み
while((data = st.read()) != -1) {
System.out.print((char)data);
}
} catch(IOException e) {
System.out.println(e);
}
}
}

[Java]ServerSocket(サーバー)
 Javaでサーバーソケットを作成する場合はポートを指定してServerSocketをnewする。

// サーバーソケット作成
ServerSocket server = new ServerSocket(port);
System.out.println("Listening on port " + port);

 作成したサーバーソケットがAcceptするとクライアントの項で既出のSocketを返すので、以降同様に処理してやれば良い。サンプルでは受信スレッドのクラスはクライアントと共有している。

// Accept
Socket conn = server.accept();
System.out.println("Accept");
// データ受信スレッド生成
RecvData rd = new RecvData(conn.getInputStream());
rd.start();

ソケットのサンプル(サーバー、クライアント)

2007年6月14日木曜日

ocket编程中select的使用

Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式 block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用 Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。下面详细介绍一下!

Select的函数格式(我所说的是Unix系统下的伯克利socket编程,和windows下的有区别,一会儿说明):

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

先说明两个结构体:

第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合FD_ZERO (fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。

第二,struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。

具体解释select的参数:

int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。

fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。

struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:

负值:select错误 正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件

在有了select后可以写出像样的网络程序来!举个简单的例子,就是从网络上接受数据写入一个文件中。

例子:

main()

{

int sock;

FILE *fp;

struct fd_set fds;

struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0

char buffer[256]={0}; //256字节的接收缓冲区

/* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开

sock=socket(...);

bind(...);

fp=fopen(...); */

while(1)

{

FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化

FD_SET(sock,&fds); //添加描述符

FD_SET(fp,&fds); //同上

maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1

switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用

{

case -1: exit(-1);break; //select错误,退出程序

case 0:break; //再次轮询

default:

if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据

{

recvfrom(sock,buffer,256,.....);//接受网络数据

if(FD_ISSET(fp,&fds)) //测试文件是否可写

fwrite(fp,buffer...);//写入文件

buffer清空;

}// end if break;

}// end switch

}//end while

}//end main

TCP的socket连接示例

Made In Zeal 转载请保留原始链接:http://www.zeali.net/entry/13
标签 ( Tags ): socket , tcp , timeout , 超时 , 异步 , ioctl , 源代码
用C实现的TCP socket连接/读/写操作。采用fcntl设置非阻塞式连接以实现connect超时处理;采用select方法来设置socket读写超时。此示例可被编译运行于Windows/unix系统。

源文件connector.c

原来的代码在windows下编译不通过,今天qzj问起才发现。因为加了异步的处理,没有对这部分代码进行兼容性处理。本着做学问一丝不苟嘀精神,重新修改了一下源代码。以下代码在VC++6和linux下编译执行通过 :)

view plainprint?

1. /*
2.
3. * on Unix:
4.
5. * cc -c connector.c
6.
7. * cc -o connector connector.o
8.
9. *
10.
11. * on Windows NT:
12.
13. * open connector.c in Visual Studio
14.
15. * press 'F7' to link -- a project to be created
16.
17. * add wsock32.lib to the link section under project setting
18.
19. * press 'F7' again
20.
21. *
22.
23. * running:
24.
25. * type 'connector' for usage
26.
27. */
28.
29.
30. #include
31.
32. #include
33.
34. #include
35.
36. #include
37.
38. #include
39.
40. #include
41.
42. #ifdef WIN32
43.
44. #include
45.
46. #else
47.
48. #include
49.
50. #include
51.
52. #include
53.
54. #include
55.
56. #include
57.
58. #include
59.
60. #include
61.
62. #endif
63.
64.
65. #ifndef INADDR_NONE
66.
67. #define INADDR_NONE 0xffffffff
68.
69. #endif
70.
71. #define MAX_STRING_LEN 1024
72.
73. #define BUFSIZE 2048
74.
75.
76. #ifndef WIN32
77.
78. #define SOCKET int
79.
80. #else
81.
82. #define errno WSAGetLastError()
83.
84. #define close(a) closesocket(a)
85.
86. #define write(a, b, c) send(a, b, c, 0)
87.
88. #define read(a, b, c) recv(a, b, c, 0)
89.
90. #endif
91.
92.
93. char buf[BUFSIZE];
94.
95.
96. static char i_host[MAX_STRING_LEN]; /* site name */
97.
98. static char i_port[MAX_STRING_LEN]; /* port number */
99.
100.
101. void err_doit(int errnoflag, const char *fmt, va_list ap);
102.
103. void err_quit(const char *fmt, ...);
104.
105. int tcp_connect(const char *host, const unsigned short port);
106.
107. void print_usage();
108.
109.
110. //xnet_select x defines
111.
112. #define READ_STATUS 0
113.
114. #define WRITE_STATUS 1
115.
116. #define EXCPT_STATUS 2
117.
118.
119. /*
120.
121. s - SOCKET
122.
123. sec - timeout seconds
124.
125. usec - timeout microseconds
126.
127. x - select status
128.
129. */
130.
131. SOCKET xnet_select(SOCKET s, int sec, int usec, short x)
132.
133. {
134.
135. int st = errno;
136.
137. struct timeval to;
138.
139. fd_set fs;
140.
141. to.tv_sec = sec;
142.
143. to.tv_usec = usec;
144.
145. FD_ZERO(&fs);
146.
147. FD_SET(s, &fs);
148.
149. switch(x){
150.
151. case READ_STATUS:
152.
153. st = select(s+1, &fs, 0, 0, &to);
154.
155. break;
156.
157. case WRITE_STATUS:
158.
159. st = select(s+1, 0, &fs, 0, &to);
160.
161. break;
162.
163. case EXCPT_STATUS:
164.
165. st = select(s+1, 0, 0, &fs, &to);
166.
167. break;
168.
169. }
170.
171. return(st);
172.
173. }
174.
175.
176. int tcp_connect(const char *host, const unsigned short port)
177.
178. {
179.
180. unsigned long non_blocking = 1;
181.
182. unsigned long blocking = 0;
183.
184. int ret = 0;
185.
186. char * transport = "tcp";
187.
188. struct hostent *phe; /* pointer to host information entry */
189.
190. struct protoent *ppe; /* pointer to protocol information entry*/
191.
192. struct sockaddr_in sin; /* an Internet endpoint address */
193.
194. SOCKET s; /* socket descriptor and socket type */
195.
196. int error;
197.
198.
199. #ifdef WIN32
200.
201. {
202.
203. WORD wVersionRequested;
204.
205. WSADATA wsaData;
206.
207. int err;
208.
209.
210.
211. wVersionRequested = MAKEWORD( 2, 0 );
212.
213.
214.
215. err = WSAStartup( wVersionRequested, &wsaData );
216.
217. if ( err != 0 ) {
218.
219. /* Tell the user that we couldn't find a usable */
220.
221. /* WinSock DLL. */
222.
223. printf("can't initialize socket library\n");
224.
225. exit(0);
226.
227. }
228.
229. }
230.
231. #endif
232.
233.
234.
235. memset(&sin, 0, sizeof(sin));
236.
237. sin.sin_family = AF_INET;
238.
239.
240.
241. if ((sin.sin_port = htons(port)) == 0)
242.
243. err_quit("invalid port \"%d\"\n", port);
244.
245.
246.
247. /* Map host name to IP address, allowing for dotted decimal */
248.
249. if ( phe = gethostbyname(host) )
250.
251. memcpy(&sin.sin_addr, phe->h_addr, phe->h_length);
252.
253. else if ( (sin.sin_addr.s_addr = inet_addr(host)) == INADDR_NONE )
254.
255. err_quit("can't get \"%s\" host entry\n", host);
256.
257.
258.
259. /* Map transport protocol name to protocol number */
260.
261. if ( (ppe = getprotobyname(transport)) == 0)
262.
263. err_quit("can't get \"%s\" protocol entry\n", transport);
264.
265.
266.
267. /* Allocate a socket */
268.
269. s = socket(PF_INET, SOCK_STREAM, ppe->p_proto);
270.
271. if (s < 0)
272.
273. err_quit("can't create socket: %s\n", strerror(errno));
274.
275.
276.
277. /* Connect the socket with timeout */
278.
279. #ifdef WIN32
280.
281. ioctlsocket(s,FIONBIO,&non_blocking);
282.
283. #else
284.
285. ioctl(s,FIONBIO,&non_blocking);
286.
287. #endif
288.
289. //fcntl(s,F_SETFL, O_NONBLOCK);
290.
291. if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) == -1){
292.
293. struct timeval tv;
294.
295. fd_set writefds;
296.
297. // 设置连接超时时间
298.
299. tv.tv_sec = 10; // 秒数
300.
301. tv.tv_usec = 0; // 毫秒
302.
303. FD_ZERO(&writefds);
304.
305. FD_SET(s, &writefds);
306.
307. if(select(s+1,NULL,&writefds,NULL,&tv) != 0){
308.
309. if(FD_ISSET(s,&writefds)){
310.
311. int len=sizeof(error);
312.
313. //下面的一句一定要,主要针对防火墙
314.
315. if(getsockopt(s, SOL_SOCKET, SO_ERROR, (char *)&error, &len) < 0)
316.
317. goto error_ret;
318.
319. if(error != 0)
320.
321. goto error_ret;
322.
323. }
324.
325. else
326.
327. goto error_ret; //timeout or error happen
328.
329. }
330.
331. else goto error_ret; ;
332.
333.
334. #ifdef WIN32
335.
336. ioctlsocket(s,FIONBIO,&blocking);
337.
338. #else
339.
340. ioctl(s,FIONBIO,&blocking);
341.
342. #endif
343.
344.
345. }
346.
347. else{
348.
349. error_ret:
350.
351. close(s);
352.
353. err_quit("can't connect to %s:%d\n", host, port);
354.
355. }
356.
357. return s;
358.
359. }
360.
361.
362. void err_doit(int errnoflag, const char *fmt, va_list ap)
363.
364. {
365.
366. int errno_save;
367.
368. char buf[MAX_STRING_LEN];
369.
370.
371. errno_save = errno;
372.
373. vsprintf(buf, fmt, ap);
374.
375. if (errnoflag)
376.
377. sprintf(buf + strlen(buf), ": %s", strerror(errno_save));
378.
379. strcat(buf, "\n");
380.
381. fflush(stdout);
382.
383. fputs(buf, stderr);
384.
385. fflush(NULL);
386.
387. return;
388.
389. }
390.
391.
392. /* Print a message and terminate. */
393.
394. void err_quit(const char *fmt, ...)
395.
396. {
397.
398. va_list ap;
399.
400. va_start(ap, fmt);
401.
402. err_doit(0, fmt, ap);
403.
404. va_end(ap);
405.
406. exit(1);
407.
408. }
409.
410.
411. #ifdef WIN32
412.
413. char *optarg;
414.
415.
416. char getopt(int c, char *v[], char *opts)
417.
418. {
419.
420. static int now = 1;
421.
422. char *p;
423.
424.
425. if (now >= c) return EOF;
426.
427.
428. if (v[now][0] == '-' && (p = strchr(opts, v[now][1]))) {
429.
430. optarg = v[now+1];
431.
432. now +=2;
433.
434. return *p;
435.
436. }
437.
438.
439. return EOF;
440.
441. }
442.
443.
444. #else
445.
446. extern char *optarg;
447.
448. #endif
449.
450.
451. #define required(a) if (!a) { return -1; }
452.
453.
454. int init(int argc, char *argv[])
455.
456. {
457.
458. char c;
459.
460. //int i,optlen;
461.
462. //int slashcnt;
463.
464.
465. i_host[0] = '\0';
466.
467. i_port[0] = '\0';
468.
469.
470. while ((c = getopt(argc, argv, "h:p:?")) != EOF) {
471.
472. if (c == '?')
473.
474. return -1;
475.
476. switch (c) {
477.
478. case 'h':
479.
480. required(optarg);
481.
482. strcpy(i_host, optarg);
483.
484. break;
485.
486. case 'p':
487.
488. required(optarg);
489.
490. strcpy(i_port, optarg);
491.
492. break;
493.
494. default:
495.
496. return -1;
497.
498. }
499.
500. }
501.
502.
503. /*
504.
505. * there is no default value for hostname, port number,
506.
507. * password or uri
508.
509. */
510.
511. if (i_host[0] == '\0' || i_port[0] == '\0')
512.
513. return -1;
514.
515.
516. return 1;
517.
518. }
519.
520.
521. void print_usage()
522.
523. {
524.
525. char *usage[] =
526.
527. {
528.
529. "Usage:",
530.
531. " -h host name",
532.
533. " -p port",
534.
535. "example:",
536.
537. " -h 127.0.0.1 -p 4001",
538.
539. };
540.
541. int i;
542.
543.
544. for (i = 0; i < sizeof(usage) / sizeof(char*); i++)
545.
546. printf("%s\n", usage[i]);
547.
548.
549.
550. return;
551.
552. }
553.
554.
555. int main(int argc, char *argv[])
556.
557. {
558.
559. SOCKET fd;
560.
561. int n;
562.
563.
564. /* parse command line etc ... */
565.
566. if (init(argc, argv) < 0) {
567.
568. print_usage();
569.
570. exit(1);
571.
572. }
573.
574.
575. buf[0] = '\0';
576.
577.
578. /* pack the info into the buffer */
579.
580. strcpy(buf, "HelloWorld");
581.
582.
583. /* make connection to the server */
584.
585. fd = tcp_connect(i_host, (unsigned short)atoi(i_port));
586.
587.
588. if(xnet_select(fd, 0, 500, WRITE_STATUS)>0){
589.
590. /* send off the message */
591.
592. write(fd, buf, strlen(buf));
593.
594. }
595.
596. else{
597.
598. err_quit("Socket I/O Write Timeout %s:%s\n", i_host, i_port);
599.
600. }
601.
602.
603. if(xnet_select(fd, 3, 0, READ_STATUS)>0){
604.
605. /* display the server response */
606.
607. printf("Server response:\n");
608.
609. n = read(fd, buf, BUFSIZE);
610.
611. buf[n] = '\0';
612.
613. printf("%s\n", buf);
614.
615. }
616.
617. else{
618.
619. err_quit("Socket I/O Read Timeout %s:%s\n", i_host, i_port);
620.
621. }
622.
623. close(fd);
624.
625.
626. #ifdef WIN32
627.
628. WSACleanup();
629.
630. #endif
631.
632.
633. return 0;
634.
635. }

2007年5月26日土曜日

inux Programming: 编写一个简单的Shell命令解释程序

这个版本的Shell支持管道功能和I/O重定向.
下一个版本将增加对内建(buildins)命令的支持,如if ..then else等控制语句的支持.
-------------CODE-------------------

#include
#include
#include
#include
#include
#define OPEN_MAX 10
#define MAXNAME 100
#define FALSE 0
#define TRUE 1
char *readcmd( FILE *fp ); //读入命令行字符串
char **getarg(char *argv); //把命令行字符串分解成字符数组指针
void getfile(char *name); //获得重定向文件名
void freearg(char **arg); //释放内存
char *buf; //存放用户输入的字符串
char *p; //指向buf的指针
int cmdnum; //记录命令个数
int pipefd[2]; //保存管道文件描述符
int wait_pid; //记录父进程PID
char infile[MAXNAME+1]; //输入重定向文件
char outfile[MAXNAME+1]; //输出重定向文件
int background; //记录命令是否在后台执行
int append; //记录是否写入文件末尾
struct cmd{
char ** argv; //命令行参数
int infd; //输入文件描述符
int outfd; // 输出文件描述符
}cmdline[BUFSIZ]; //命令行结构数组
int main()
{
pid_t pid;
int status;
int i, fd;
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_IGN); //处理中断(Ctrl-C)和退出(Ctrl-\)信号
printf("%% ");


while( (buf = readcmd(stdin)) != NULL ){
//-----------初始化-----------------
int k;
cmdnum = 0;
background = FALSE;
infile[0] = '\0';
outfile[0] = '\0';
append = FALSE;
for( k = 0; k < OPEN_MAX; k++ )
{
cmdline[k].infd = 0;
cmdline[k].outfd = 1;
}
for( k = 3; k < OPEN_MAX; k++ )
close(k);
fflush(stdout); //清空输出缓冲区
p = buf;
//-----------计算命令个数并获得命令参数----------
for(i = 0; i <= cmdnum; i++)
cmdline[i].argv = getarg(buf);

if( infile[0] != '\0' )
cmdline[0].infd = open(infile, O_RDONLY);
if( outfile[0] != '\0' )
if( append == FALSE )
cmdline[cmdnum].outfd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
else
cmdline[cmdnum].outfd = open(outfile, O_WRONLY | O_CREAT | O_APPEND, 0666);
if( background == TRUE )
signal(SIGCHLD, SIG_IGN);
else
signal(SIGCHLD, SIG_DFL);
//-----------执行命令行-------------
for(i = 0; i <= cmdnum; i++)
{
//-----------处理管道-------------
if( i < cmdnum )
{
pipe(pipefd);
cmdline[i+1].infd = pipefd[0];
cmdline[i].outfd = pipefd[1];
}



if ( pid = fork() )
wait_pid = pid;
else if ( pid == 0 ){
if( cmdline[i].infd == 0 && background == TRUE )
cmdline[i].infd = open("/dev/null", O_RDONLY);
if(cmdline[i].infd != 0)
{
close(0);
dup(cmdline[i].infd);

}
if(cmdline[i].outfd != 1)
{
close(1);
dup(cmdline[i].outfd);

}

if( background == FALSE )
{
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
}
for( k = 3; k < OPEN_MAX; ++k)
close(k);
execvp(cmdline[i].argv[0], cmdline[i].argv);

}
if(fd = cmdline[i].infd)
close(fd);
if((fd = cmdline[i].outfd) != 1)
close(fd);
}
if( background == FALSE )
while ( waitpid(pid, &status, 0) != wait_pid )
;

printf("%% ");
//-------释放内存-----------
for(i = 0; i <= cmdnum; i++)
freearg(cmdline[i].argv);
free(buf);
}

exit(0);
}
void freearg(char **arg)
{
char **cp = arg;
while(*cp)
free(*cp++);
free(arg);
}
char *readcmd(FILE *fp)
{
char *buf;
int buflen = 0;
int i = 0;
int c;

while( (c = getc(fp)) != EOF ){
if( i +1 >= buflen ){
if( buflen == 0 ){
buf = (char*) malloc( BUFSIZ );
if( buf == NULL )
return NULL;
}
else{
buf = (char *)realloc(buf, buflen + BUFSIZ);
if( buf == NULL )
return NULL;
}
buflen += BUFSIZ;
}
if ( c == '\n' )
break;
buf[i++] = c;
}
if( c == EOF && i == 0 )
return NULL;
buf[i] = '\0';
return buf;
}
char **getarg(char * argv)
{
char **args;
if( (args = (void *)malloc(BUFSIZ)) == NULL )
{
printf("out of memory\n");
return NULL;
}

int bufsize = sizeof(argv)/sizeof(argv[0]);
char argbuf[bufsize];
char *cp = argbuf;
char *end;
char *start = argbuf;
char * tempstr;
int len;
int i = 0;
while( *p != '\0' )

{
while( *p == ' ' || *p == '\t' )
p++; //去掉多余的空格
if( *p == '\0' )
break;
if( *p == '|') //管道,使命令数加1
{
if( *(p+1) == ' ' )
p +=2; //跳过空格
else
p++;
cmdnum++;
break;
}
if( *p == '<' ) //输入重定向
{
if( *(p+1) == ' ' )
p +=2;
else
p++;
getfile(infile);
break;
}
if( *p == '>' ) //输出重定向
{
if( *(p+1) == ' ' )
p +=2;
else
p++;
if( *p == '>' ) //追加到文件末尾
{
if( *(p+1) == ' ' )
p +=2;
else
p++;
append = TRUE;
}
getfile(outfile);
break;
}
if( *p == '&' ) //后台执行命令
{
if( *(p+1) == ' ' )
p +=2;
else
p++;
background = TRUE;
break;
}
*cp++ = *p++;
if( *p == ' ' || *p == '\0' )
{
*cp = '\0';
end = cp;
len = end - start;
if( (tempstr = (char *)malloc(len + 1)) == NULL )
{
printf("out of memory\n");
return NULL;
}
strncpy(tempstr, start, len);
tempstr[len] = '\0';
if( i + 1 > BUFSIZ )
{
if( (args = (void *)realloc(args, BUFSIZ * 2)) == NULL )
{
printf("out of memory\n");
return NULL;
}
}
args[i++] = tempstr;
start = end;
if( *p == '\0')
break;
p++;
}
}
args[i] = NULL;

return args;
}
void getfile(char *name)
{
int i;
for( i = 0; i < MAXNAME; ++i )
{
if( *p == ' ' || *p == '|' || *p == '>' || *p == '\0' ||
*p == '<' || *p == '&' || *p == '\t' )
{
*name = '\0';
return;
}
else
*name++ = *p++;

}
*name = '\0';
}

树控件的创建

MSDN上说,在MFC中树控件有三种创建方法:

1. 在对话框模版中添加一个TreeBox控件(假设其ID 为:IDR_TREE), 则自动生成一个树控件对象。通过在对话框类中的OnInitDlg()函数中, 使用CTreeCtrl *pTree = (CTreeCtrl *) GetDlgItem(IDR_TREE); 将得到CTreeCtrl对象的指针, 使用此指针就可以对树控件进行操作。

另外, 对树控件的消息以及消息处理函数都是在对话框类中添加。

2. 使用CTreeView。 这样AppWzard将自动创建一个相关的CTreeCtrl 对象。

3. 使用CTreeCtrl::Create()函数创建。



void CTreeCtrl::Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID )



其中dwStyle使用以下树控件专用的风格:

TVS_HASLINES 在父/子结点之间绘制连线

TVS_LINESATROOT 在根/子结点之间绘制连线

TVS_HASBUTTONS 在每一个结点前添加一个按钮,用于表示当前结点是否已被展开

TVS_EDITLABELS 结点的显示字符可以被编辑

TVS_SHOWSELALWAYS 在失去焦点时也显示当前选中的结点

TVS_DISABLEDRAGDROP 不允许Drag/Drop

TVS_NOTOOLTIPS 不使用ToolTip显示结点的显示字符

TVS_CHECKBOXES 在每项旁边添加一个选择框



此外, dwStyle在树控件创建时一定要加上WS_VISIBLE , 否则树控件将会被隐藏!!当然,如果没有加上这个WS_VISIBLE也可以在创建之后立即调用ShowWindow(SW_SHOWNORMAL),控件便显示出来了,若调用ShowWindow(SW_HIDE)控件便隐藏了。同时,一般情况下,树控件是作为子窗口使用,那么在创建时要加上WS_CHILD,表示控件是子窗口(那么在创建时当然要指定父窗口了)。

nID可以随便定义一个和其他ID值不冲突的数字便于标识(当然也可以直接用数字,这里用的是IDD_TREE)。 而pParentWnd则指向树控件的父窗口, 在对话框中是对话框类对象的指针, 在视图中, 是视图类对象的指针(这里使用的是this指针)。 我在视图类CScrollView中的OninitialUpdate()函数中加入一个树控件:

m_tree.Create(WS_VISIBLE | WS_TABSTOP | WS_CHILD | WS_BORDER

| TVS_HASBUTTONS | TVS_LINESATROOT | TVS_HASLINES

| TVS_DISABLEDRAGDROP, CRect(0,0,100,100),this,IDD_TREE);



HTREEITEM hItem,hSubItem;



hItem = m_tree.InsertItem(_T("Parent1"),TVI_ROOT);

//在根结点上添加Parent1



hSubItem = m_tree.InsertItem(_T("Child1_1"),hItem);

//在Parent1上添加一个子结点



hSubItem = m_tree.InsertItem(_T("Child1_2"),hItem,hSubItem);

//在Parent1上添加一个子结点,排在Child1_1后面



hSubItem = m_tree.InsertItem(_T("Child1_3"),hItem,hSubItem);



hItem = m_tree.InsertItem(_T("Parent2"),TVI_ROOT,hItem);

hItem = m_tree.InsertItem(_T("Parent3"),TVI_ROOT,hItem);



其中的m_tree在CMyScrollView头文件中声明为:

Private:

CTreeCtrl m_tree;





注意: 一定要在头文件中声明一个树控件类的对象, 不能在函数中声明(表示对象是临时的), 如果在函数中声明了类对象, 那么在函数返回后该对象会被消除, 这样就无法显示树控件了!!!



父窗口指定为this指针,即CMyScrollView指针,表示树控件将会向它发送命令消息(WM_COMMAND)和通知消息(WM_NOTIFY)。那么所有的树控件处理消息都将在这个父窗口进行处理。(在对话框中的情形相同)

例如:为了能对树控件进行操作, 必须添加消息和消息处理函数。 此时由于是用Create()函数创建的树控件, 无法用AppWzard 为它产生消息以及消息处理函数, 必须手动添加。

例如:在CMyScrollView的头文件中声明: (用于处理双击树控件项事件)



afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);



在CMyScrollView的实现文件中添加:

BEGIN_MESSAGE_MAP(CMyScrollView, CScrollView)

…。

…。

ON_NOTIFY(NM_DBLCLK, IDD_TREE, OnTreeDblClk)

…。

…。

END_MESSAGE_MAP()



void CMyScrollView::OnTreeDblClk(NMHDR* pNMHDR, LRESULT *pResult)

{

HTREEITEM hSelected = m_tree.GetSelectedItem();

if (hSelected != NULL) {



if(m_tree.GetItemText(hSelected) == "Child1_1"){

AfxMessageBox(_T("This Child1_1 is Clicked!"));

}



}



*pResult = 0;

}

树控件的结点插入

树控件结点的插入使用CTreeCtrl::InsertItem()函数。

HTREEITEM InsertItem( LPTVINSERTSTRUCT lpInsertStruct );



HTREEITEM InsertItem(UINT nMask, LPCTSTR lpszItem, int nImage, int nSelectedImage, UINT nState, UINT nStateMask, LPARAM lParam, HTREEITEM hParent, HTREEITEM hInsertAfter );



HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST );



HTREEITEM InsertItem( LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST);





一般情况下使用最后两种形式,一种是用于插入结点,另一种增加了对图标的插入操作。

树控件可以用于树结构,其中有一个根结点(Root)然后下面有许多子结点,而每个子结点上允许有一个或多个或没有子结点。在树控件中每一个结点都有一个句柄(HTREEITEM),同时添加结点时必须提供的参数是该结点的父结点句柄,(其中根Root结点只有一个,既不可以添加也不可以删除)利用

HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST );可以添加一个结点,pszItem为显示的字符,hParent代表父结点的句柄,当前添加的结点会排在hInsertAfter表示的结点的后面,返回值为当前创建的结点的句柄。如例子中,在调用了Create()函数之后进行了结点的插入操作它生成了如下形式的树结构:

+--- Parent1

+--- Child1_1

+--- Child1_2

+--- Child1_3

+--- Parent2

+--- Parent3



如果你希望在每个结点前添加一个小图像,就必需先调用CTreeCtrl:: SetImageList( CImageList * pImageList, int nImageListType );指明当前所使用的ImageList,nImageListType为TVSIL_NORMAL。在调用完成后控件中使用图片以设置的ImageList中图片为准。然后调用

HTREEITEM InsertItem( LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST);添加结点,nImage为结点没被选中时所使用图片序号,nSelectedImage为结点被选中时所使用图片序号。下面的代码演示了ImageList的设置。

/*m_list 为CImageList对象IDB_TREE 为16*(16*4)的位图,每个图片为16*16共4个图标*/

m_list.Create(IDB_TREE,16, 4, RGB(0,0,0));

m_tree.SetImageList(&m_list,TVSIL_NORMAL);

m_tree.InsertItem("Parent1" , 0, 1);//添加,选中时显示图像1,未选中时显示图像0





添加结点最复杂的函数是前两个,它们使用了各种结构以及字段值来标识结点所处的状态和属性。

第一个函数:

HTREEITEM InsertItem( LPTVINSERTSTRUCT lpInsertStruct );

要求传入一个TVINSERTSTRUCT结构的指针。它的定义为:(包括两个结点句柄,一个描述树控件结点的结构)



//TVINSERTSTRUCT





typedef struct tagTVINSERTSTRUCT {

HTREEITEM hParent; //父结点句柄

HTREEITEM hInsertAfter; //插入其后的结点句柄

#if (_WIN32_IE >= 0x0400)

union

{

TVITEMEX itemex;

TVITEM item;

} DUMMYUNIONNAME;

#else

TVITEM item; //描述树控件结点的结构

#endif

} TVINSERTSTRUCT, FAR *LPTVINSERTSTRUCT;





而TVITEM定义为:

//TVITEM





typedef struct tagTVITEM{

UINT mask;

HTREEITEM hItem;

UINT state;

UINT stateMask;

LPTSTR pszText;

int cchTextMax;

int iImage;

int iSelectedImage;

int cChildren;

LPARAM lParam;

} TVITEM, FAR *LPTVITEM;



mask

一组标志位,表明哪个成员数据是有效的。当此结构用于TVM_GETITEM消息时,mask成员表示获得的项属性。它可以是以下值中的一个或多个组合:

TVIF_CHILDREN :cChildren 成员有效;

TVIF_HANDLE :hItem 成员有效;

TVIF_IMAGE :iImage 成员有效;

TVIF_PARAM :lParam成员有效;

TVIF_SELECTEDIMAGE :iSelectedImage成员有效;

TVIF_STATE :state 和 stateMask成员有效;

TVIF_TEXT :pszText 和 cchTextMax成员有效。



hItem

表示相关项的句柄。

state

一组标志位以及图像列表索引,表示项状态。当设置项状态时,stateMask表示此成员的哪些位是有效的;当获取项状态时,此成员返回由stateMask指示的当前有效的项状态位。此成员的0到7位包含项状态标志。见附录。8到11位代表从1开始的覆盖图像索引。覆盖图像用来叠加在项图像上。如果这些位是0,那么项就没有覆盖图像。为了隔离这些位,使用TVIS_OVERLAYMASK掩码(mask)。要设置项覆盖图像索引,应该使用INDEXTOOVERLAYMASK宏。图像列表的覆盖图像使用ImageList::SetOverlayImage()函数设置。因为使用的从 1 开始的索引是 4 位的,所以覆盖图像必须在图像列表的前 15 位。12到15位代表项状态图像索引。状态图像显示在项图标的旁边,表示应用程序定义的状态。也就是说指定了状态图像后,树控件就在每项的图标左边为状态图像保留空间。应用程序可以用状态图像(如选定和未选定的复选框)指示应用程序定义的项状态。如果这些位是0,那么就没有状态图像。为了隔离这些位,使用TVIS_STATEIMAGEMASK掩码(mask)。要设置状态图像,应该使用INDEXTOSTATEIMAGEMASK宏。状态图像索引表示要画的图像列表的图像索引。状态图像列表由TVM_SETIMAGELIST消息指定。

附:树视图控件的状态标志

TVIS_BOLD 项标签文本(ItemText)是粗体

TVIS_CUT 所选择的项作为粘贴复制操作

TVIS_DROPHILITED 所选择的项作为拖拽标志

TVIS_EXPANDED 子项列表当前是展开的。也就是子项是可见的。此值仅用于父项。

TVIS_EXPANDEDONCE子项列表已经至少展开了一次。如果父项有此状态,并对TV_EXPAND消息作出了响应,就不会为该父项产生 TVN_ITEMEXPANDING和TVN_ITEMEXPANDED 通知消息。把 TVE_COLLAPSE 和 TVE_COLLAPSERESET同 TVM_EXPAND一起使用就会使此状态重置。此值仅用于父项。

TVIS_EXPANDPARTIAL 部分展开树视图项。在此状态中,只是部分并不是全部的子项是可见的,而且会显示父项的加号。

TVIS_SELECTED 此项被选中了。项的外观取决于是否有焦点。会使用系统颜色画出所选择的项。

要设置或获取项的覆盖图像索引或者状态图像索引,必须在stateMask成员中指定以下标志:(这些值也可以用于去掉不感兴趣的状态位)

TVIS_OVERLAYMASK 用来指定项覆盖图像索引的位掩码。

TVIS_STATEIMAGEMASK 用来指定项状态图像的位掩码。

TVIS_USERMASK 同TVIS_STATEIMAGEMASK相同。



stateMask

表示state成员的有效位。如果要获取项状态,则在stateMask成员中设置相应位来指示state成员所返回的值;如果想要设置项状态,则在stateMask成员中设置相应位来指示想在sate成员中设置的位。如果要设置或获取项覆盖图像索引,则设置TVIS_OVERLAYMASK位;如果要设置或获取项状态索引,则设置TVIS_STATEIMAGEMASK位。



pszText

以NULL结尾的字符串地址,如果TVITEM结构要求项属性,那么它代表项标签文本(Item Text)。如果此成员的值是LPSTR_TEXTCALLBACK,那么父窗口负责存储项标签文本的名称。此种情况下,当树控件需要显示项标签、对项标签排序、编辑项标签或者当项标签改变时需要发送TVN_SETDISPINFO通知消息时,它会给父窗口发送TVN_GETDISPINFO通知消息。

如果TVITEM正在接收项属性时,此成员表示接收到的项标签文本的缓冲区地址。



cchTextMax

由pszText指向的缓冲区地址的字节大小。如果用TVITEM设置项属性,此成员被忽略。

说明:通常在向树控件添加项时指定项标签文本。InsertItem 成员函数能传递 TVITEM 结构。该结构定义了项的属性,项属性中有一个包含标签文本的字符串。树控件分配存储各项的内存,其中大部分内存都被项标签文本占用。如果应用程序保存了树控件中字符串的副本,就可以减少控件所需的内存空间,方法是在 TV_ITEM 的 pszText 成员中或者在 lpszItem 参数中指定 LPSTR_TEXTCALLBACK 值,而不是将实际字符串传递给树控件。每当需要重绘某项时,LPSTR_TEXTCALLBACK 使树控件从应用程序中检索该项的标签文本。为了检索标签文本,树控件发送 TVN_GETDISPINFO 通知消息,该消息包括 NMTVDISPINFO 结构的地址。必须通过设置所含结构的适当成员来响应该通知消息。

树控件使用从创建树控件的进程堆分配的内存。树控件最多可以包含的项数取决于堆中可用的内存量。每个项占用 64 字节。

iImage

当树控件的项结点是非选中状态时所使用的图标在图像列表中的索引值。如果此成员的值是I_IMAGECALLBACK,则父窗口负责存储该索引值。在此情况下,当树控件需要显示项图像时它向父窗口发送TVN_GETDISPINFO通知消息来获得索引值。



iSelectedImage

当树控件的项结点是选中状态时所使用的图标在图像列表中的索引值。如果此成员的值是I_IMAGECALLBACK,则父窗口负责存储该索引值。在此情况下,当树控件需要显示项图像时它向父窗口发送VN_GETDISPINFO通知消息来获得索引值。



cChildren

表示树控件项是否有相关的子结点的标志位。此成员可以是以下值之一:

0 —— 表示此项没有子结点。

1 —— 表示此此项有一个或多个子结点。

I_CHILDRENCALLBACK —— 父窗口会始终跟踪确定此项是否有子结点。此时,如果树控件需要显示项时,它会给父窗口发送TVN_GETDISPINFO通知消息,决定此项是否有子结点。如果树控件含有TVS_HASBUTTONS风格,则它使用此成员来决定是否显示按钮以表明存在子结点。也可以使用此成员来强制树控件显示按钮,尽管没有在此项之下插入任何子结点。这样可以最小化由于在此项之下插入了子结点所占用的控件内存。



lParam

同控件相关的32位值。





下面这段代码显示了如何用此结构插入树控件项,同时也演示了如何为项插入图像。

HICON hIcon[8];

int n;

m_imageList.Create(16, 16, 0, 8, 8); // 32, 32 for large icons

hIcon[0] = AfxGetApp()->LoadIcon(IDI_WHITE);

hIcon[1] = AfxGetApp()->LoadIcon(IDI_BLACK);

hIcon[2] = AfxGetApp()->LoadIcon(IDI_RED);

hIcon[3] = AfxGetApp()->LoadIcon(IDI_BLUE);

hIcon[4] = AfxGetApp()->LoadIcon(IDI_YELLOW);

hIcon[5] = AfxGetApp()->LoadIcon(IDI_CYAN);

hIcon[6] = AfxGetApp()->LoadIcon(IDI_PURPLE);

hIcon[7] = AfxGetApp()->LoadIcon(IDI_GREEN);

for (n = 0; n < 8; n++) {

m_imageList.Add(hIcon[n]);

}

CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);

pTree->SetImageList(&m_imageList, TVSIL_NORMAL);

TV_INSERTSTRUCT tvinsert;

tvinsert.hParent = NULL;

tvinsert.hInsertAfter = TVI_LAST;

tvinsert.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE |

TVIF_TEXT;// | TVIF_STATE;

tvinsert.item.hItem = NULL;

tvinsert.item.state = 0;

//tvinsert.item.state = TVIS_EXPANDED;

tvinsert.item.stateMask = 0;

//tvinsert.item.stateMask = TVIS_OVERLAYMASK;

tvinsert.item.cchTextMax = 6;

tvinsert.item.iSelectedImage = 1;

tvinsert.item.cChildren = 0;

tvinsert.item.lParam = 0;

// top level

tvinsert.item.pszText = "Parent1";

tvinsert.item.iImage = 2;

HTREEITEM hDad = pTree->InsertItem(&tvinsert);

tvinsert.item.pszText = "Parent2";

HTREEITEM hMom = pTree->InsertItem(&tvinsert);

// second level

tvinsert.hParent = hDad;

tvinsert.item.pszText = "Children1-1";

tvinsert.item.iImage = 3;

pTree->InsertItem(&tvinsert);

tvinsert.item.pszText = "Children1-2";



pTree->InsertItem(&tvinsert);

// second level

tvinsert.hParent = hMom;

tvinsert.item.pszText = "Children2-1";

tvinsert.item.iImage = 4;

pTree->InsertItem(&tvinsert);

tvinsert.item.pszText = "Children2-2";

pTree->InsertItem(&tvinsert);

tvinser.item.pszText = "Children2-3";

HTREEITEM hOther = pTree->InsertItem(&tvinsert);

// third level

tvinsert.hParent = hOther;

tvinsert.item.pszText = "Children2-3-1";

tvinsert.item.iImage = 7;

pTree->InsertItem(&tvinsert);

tvinsert.item.pszText = "Children2-3-2";

pTree->InsertItem(&tvinsert);

―――――――――――――――――――――――――――――――――――

即:

+Parent1

--Children1-1

--Children1-2

+Parent2

--Children2-1

--Children2-2

+Children2-3

--Children2-3-1

--Children2-3-2

Win32怎么做?

对Win32程序来说,插入树控件项也不难,只需要向树控件发送TVM_INSERTITEM消息即可。下面是用Win32写出的等价代码(为了简明起见,去掉了对控件项图像的支持)在主窗口过程中加入如下代码:

HWND hwndTree;

TVINSERTSTRUCT tvinsert;

switch (message)

{

case WM_CREATE:

{ hwndTree = CreateWindow(WC_TREEVIEW, _T("TreeView"),

WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT,

0,0,200,100, hWnd,(HMENU)1, hInst,NULL);

tvinsert.hParent = NULL;

tvinsert.hInsertAfter = TVI_LAST;

tvinsert.item.mask = TVIF_TEXT;

tvinsert.item.hItem = NULL;

tvinsert.item.state = 0;

tvinsert.item.stateMask = 0;

tvinsert.item.cchTextMax = 6;

tvinsert.item.cChildren = 0;

tvinsert.item.lParam = 0;

// top level

tvinsert.item.pszText = _T("Parent1");

HTREEITEM hDad = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

tvinsert.item.pszText = _T("Parent2");

HTREEITEM hMom = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



// second level

tvinsert.hParent = hDad;

tvinsert.item.pszText = _T("Children1-1");

SendMessage(hwndTree,TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

tvinsert.item.pszText = _T("Children1-2");

SendMessage(hwndTree,TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



// second level

tvinsert.hParent = hMom;

tvinsert.item.pszText = _T("Children2-1");



SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

tvinsert.item.pszText = _T("Children2-2");

SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

tvinsert.item.pszText = _T("Children2-3");

HTREEITEM hOther = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

// third level

tvinsert.hParent = hOther;

tvinsert.item.pszText = _T("Children2-3-1");



SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

tvinsert.item.pszText = _T("Children2-3-2");

SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



break;

}

case: WM_COMMAND:





break;

other case …

… …

return 0;

}

此外还要添加 InitCommonControls();函数,它的头文件为:commctrl.h,在工程设置链接选项卡中的“对象/库模块”中添加comctrl32.lib。这样便可顺利通过编译和链接。

树控件消息

树控件一共有43个控制消息(TVM_XXX)和23个通知消息(TVN_XXX)下面按操作的不同进行简单的说明。

一、 一、拖拽消息

拖拽(drag-drop)操作包含这样几步:

1. 1。选中一个节点后,按住鼠标左键(此时是左键拖拽)开始拖动鼠标时,树控件向其父窗口发送TVN_BEGINDRAG通知消息(通过此消息可以得到被拖拽节点的句柄)。此时,需要准备在拖拽操作时所显示的拖拽图标。那么需要向树控件发送TVM_CREATEDRAGIMAGE消息,它创建一个Image List对象,并把被选中节点的图标复制到这个Image List中,并返回Image List句柄。然后使用ImageList_BeginDrag以及ImageList_DragEnter宏对这个Image List做一些设置,比如拖拽时鼠标所处的作标点。



case TVN_BEGINDRAG:

{

POINT pt;

pt.x = 0;

pt.y = 0;

ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &pt);

NMTREEVIEW *lpnmtv = (NMTREEVIEW*) lParam;

hitemDrag = lpnmtv->itemNew.hItem;

hitemDrop = NULL;

himge = (HIMAGELIST)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_CREATEDRAGIMAGE,0,(LPARAM)(HTREEITEM)lpnmtv->itemNew.hItem);

if(!himge)

break;

bdragging = TRUE;

ImageList_BeginDrag(himge, 0, pt.x ,pt.y );

POINT point = lpnmtv->ptDrag;

ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &point);

ImageList_DragEnter(GetDlgItem(hWnd, ID_TREEVIEW),point.x ,point.y );

SetCapture(hWnd);

break;

}

2.2。 拖拽过程中会产生一系列的WM_MOUSEMOVE消息,因此要在这个消息的处理例程中要确定拖拽图标的位置(ImageList_DragMove())以及拖拽目标的位置。对于拖拽目标的确定,一般使用TVM_HITTEST消息来获得拖拽目标节点的句柄。并把这个节点加亮显示(使用带TVGN_DROPHILITE标记的TVM_SELECTITEM消息可以加亮显示拖拽节点)。



case WM_MOUSEMOVE:

{

TVHITTESTINFO tvht;

UINT flags;

flags = (UINT)wParam;





if(bdragging){

pos.x = LOWORD(lParam);

pos.y = HIWORD(lParam);

ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &pos);

ImageList_DragMove(pos.x, pos.y);

ImageList_DragShowNolock(FALSE);

ScreenToClient(GetDlgItem(hWnd, ID_TREEVIEW), &pos);

tvht.pt.x = pos.x;

tvht.pt.y = pos.y;



if(hitemDrop = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW),

TVM_HITTEST, (WPARAM)&flags ,(LPARAM)&tvht))

{

SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,

TVGN_DROPHILITE,(LPARAM)hitemDrop);

}

ImageList_DragShowNolock(TRUE);



}

break;

}

3.3。 最后选定拖拽目标节点后,释放鼠标键产生WM_LBUTTONUP消息。在这里,Image List已完成它的任务,应该释放它(ImageList_DragLeave 、ImageList_EndDrag)。然后将源节点插入到目标节点处,并删除源节点。



case WM_LBUTTONUP:

{

if (bdragging){

bdragging = FALSE;

ImageList_DragLeave(GetDlgItem(hWnd, ID_TREEVIEW));

ImageList_EndDrag();

ReleaseCapture();



if( hitemDrag == hitemDrop )

break;

HTREEITEM htiParent = hitemDrop;

while( (htiParent = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW),

TVM_GETNEXTITEM,

(WPARAM)(UINT)TVGN_PARENT,

(LPARAM)htiParent )) != NULL )

{

if( htiParent == hitemDrag )

break;

}



DeleteObject(himge);



SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_EXPAND,

(WPARAM) (UINT)TVE_EXPAND, (LPARAM)hitemDrop);



HTREEITEM htiNew = CopyBranch(GetDlgItem(hWnd, ID_TREEVIEW),

hitemDrag, hitemDrop, TVI_LAST );



SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_DELETEITEM,

0, (LPARAM)hitemDrag);

/*检查最后高亮显示的节点,并选中它。还必须使得其不再高亮显示,否则其它的节点被选中时就不能高亮显示了。*/

SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,

TVGN_DROPHILITE,(LPARAM)0);

SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,

TVGN_CARET, (LPARAM)htiNew);

}

break;

}



为了将源节点插入到目标节点处,还需要进行插入操作。为此需要两个函数如下:



char Text[128];



HTREEITEM CopyItem(HWND htreewnd, HTREEITEM hItem, HTREEITEM htiNewParent, HTREEITEM htiAfter /*= TVI_LAST*/ )

{

TV_INSERTSTRUCT tvstruct;

HTREEITEM hNewItem;



// 获得源节点信息

tvstruct.item.hItem = hItem;

tvstruct.item.mask = TVIF_CHILDREN | TVIF_HANDLE |

TVIF_IMAGE | TVIF_SELECTEDIMAGE ;

SendMessage(htreewnd, TVM_GETITEM, 0 ,(LPARAM)&tvstruct.item);





// 在适当的位置上插入节点

tvstruct.hParent = htiNewParent;

tvstruct.hInsertAfter = htiAfter;

tvstruct.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT;

tvstruct.item.pszText = Text;

tvstruct.item.cchTextMax = 128;

SendMessage(htreewnd, TVM_GETITEM, 0 ,(LPARAM)&tvstruct.item);

hNewItem = (HTREEITEM)SendMessage(htreewnd, TVM_INSERTITEM, 0, (LPARAM)&tvstruct);







// 复制节点数据和状态

TVITEM item;

item.mask = TVIF_PARAM | TVIF_HANDLE | TVIF_STATE;

item.hItem = hItem;

item.stateMask = TVIS_STATEIMAGEMASK;

item.state = 0;

SendMessage(htreewnd, TVM_GETITEM, 0, (LPARAM)&item);



item.hItem = hNewItem;

SendMessage(htreewnd, TVM_SETITEM, 0, (LPARAM)&item);



return hNewItem;



}



HTREEITEM CopyBranch(HWND htreewnd, HTREEITEM htiBranch, HTREEITEM htiNewParent, HTREEITEM htiAfter )

{

HTREEITEM hChild;

HTREEITEM hNewItem = CopyItem(htreewnd, htiBranch, htiNewParent, htiAfter);

hChild = (HTREEITEM)SendMessage(htreewnd, TVM_GETNEXTITEM, TVGN_CHILD,

(LPARAM) htiBranch);

while( hChild != NULL)

{

// 递归地移动所有节点

CopyBranch(htreewnd, hChild, hNewItem, htiAfter);

hChild = (HTREEITEM)SendMessage(htreewnd, TVM_GETNEXTITEM, TVGN_CHILD, (LPARAM)hChild);

}



return hNewItem;



}

二、编辑标签消息

可以对树控件节点标签(节点文本)进行编辑,此时控件会在节点文本上产生一个编辑控件(Edit Control)。想要对文本进行编辑并使其生效,必须获得该编辑控件的窗口句柄(发送TVM_GETEDITCONTROL消息)。之后,通过这个窗口句柄获得用户在控件上所编辑修改后的节点文本。最后再把这个节点文本设置进该节点属性中才能使编辑生效(若不然编辑修改后,在节点上显示的文本仍将是原来的文本)。

编辑节点文本只需要处理TVN_BEGINLABELEDIT和TVN_ENDLABELEDIT两个通知消息。用户已选中的节点上再次单击鼠标左键时树控件就会向其父窗口发送TVN_BEGINLABELEDIT通知消息,这时在节点上产生一个编辑控件,那么在处理这个通知消息时通常是获取编辑控件的窗口句柄:

case TVN_BEGINLABELEDIT:

{

//TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)lParam;

hedit =(HWND) SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_GETEDITCONTROL ,0, 0);

break;

}

当用户在节点的编辑控件上编辑完文本后在树控件空白处单击鼠标左键或按Enter键后,树控件向其父窗口发送TVN_ENDLABELEDIT通知消息结束编辑操作。在这里要更新节点文本并使其生效。此外,为了更新节点文本以及其他属性,在选中一个节点时要设法保存被选中节点的节点句柄(HTREEITEM),把它存储在一个全局变量hSelected中。

case TVN_ENDLABELEDIT:

{

char Text[256]="";

tvitem.mask = TVIF_HANDLE | TVIF_TEXT;

tvitem.hItem = hSelected;

SendMessage(GetDlgItem(hWnd,ID_TREECHECK), TVM_GETITEM,

0, (LPARAM)&tvitem);

GetWindowText(hedit, Text, sizeof(Text));

tvitem.pszText = Text;

SendMessage(GetDlgItem(hWnd,ID_TREECHECK), TVM_SETITEM,

0, (LPARAM)&tvitem);

break;

}

此时编辑节点标签操作全部完成。至于获得那个节点句柄的操作可以在NM_CLICK通知消息中发送TVM_HITTEST消息进行处理处理(当然也可以有其他的处理办法):

HTREEITEM hSelected;

case NM_CLICK:

{

UINT uFlags = 0;

POINT pt;

GetCursorPos(&pt);

ScreenToClient(GetDlgItem(hWnd, ID_TREECHECK),&pt);



TVHITTESTINFO hti;

hti.pt = pt;

//Selected必须是全局变量,然后到处理TVN_ENDLABELEDIT消息这个

//变量才有效!!

hSelected = (HTREEITEM)SendMessage(GetDlgItem(hWnd,

ID_TREECHECK),TVM_HITTEST, 0, (LPARAM)&hti);

//……

//……

//其他处理代码

break;

}

节点展开/折叠消息

当点击节点左边的加号(+)或减号(-)时,带有子节点的项目就相应展开或折叠起来。这时树控件向其父窗口发送TVN_ITEMEXPANDING通知消息。这个消息的lParam参数是NMTREEVIEW结构指针。它含有两个TVITEM成员itemOld和itemNew,itemOld成员代表刚刚失去焦点的节点而itemNew成员表示正在获得焦点的节点。其中itemNew成员的hItem字段即为正在获得焦点的节点句柄。在处理TVN_ITEMEXPANDING通知消息时就是通过该句柄来对此节点的状态属性进行设置或获取操作的。而NMTREEVIEW结构的action成员用于进一步确定节点是展开还是折叠:当action == TVE_EXPAND节点展开;当action == TVE_COLLAPSE节点折叠。通常节点展开时希望该节点的图标换成展开状态的图标,比如一个表示已打开文件样式的图标等等而结点折叠则用另外的图标表示之,下面的代码就是处理这样的情况:

case TVN_ITEMEXPANDING:

{

lpnmtv = (LPNMTREEVIEW)lParam;

HTREEITEM hSelected = lpnmtv->itemNew.hItem;



if(lpnmtv->action == TVE_EXPAND){

tvitem.mask = TVIF_HANDLE | TVIF_IMAGE |

TVIF_SELECTEDIMAGE | TVIF_STATE;

tvitem.hItem = hSelected;

tvitem.state = 0;

tvitem.stateMask = TVIS_SELECTED;

tvitem.iImage = 3;

tvitem.iSelectedImage = 3;

SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SETITEM,

(WPARAM)0, (LPARAM)(const LPTVITEM)&tvitem);



}

else if(lpnmtv->action == TVE_COLLAPSE){

tvitem.mask = TVIF_HANDLE | TVIF_IMAGE |

TVIF_SELECTEDIMAGE | TVIF_STATE;

tvitem.hItem = hSelected;

tvitem.state = 0;

tvitem.stateMask = TVIS_SELECTED;

tvitem.iSelectedImage = 2;

tvitem.iImage = 2;

SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SETITEM,

(WPARAM)0, (LPARAM)(const LPTVITEM)&tvitem);

}

break;

}



四、 节点状态图标消息

可以使用TVS_CHECKBOXES样式的树控件使每个节点旁都有一个checkbox,也可以自己手动添加checkbox样式的图标来表示下面的代码负责手动添加checkbox图标:



先在WM_CREATE消息处理中插入节点:

hbitmapcheck = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP2));

himlc = ImageList_Create(13, 13, ILC_COLOR16 , 8, 0);

ImageList_Add(himlc, hbitmapcheck, 0);

DeleteObject(hbitmapcheck);



tvinsert.hParent = NULL;

tvinsert.hInsertAfter = TVI_LAST;

tvinsert.item.mask = TVIF_TEXT | TVIF_HANDLE |TVIF_STATE;

tvinsert.item.hItem = NULL;



tvinsert.item.state = INDEXTOSTATEIMAGEMASK(1);

tvinsert.item.stateMask = TVIS_STATEIMAGEMASK;

tvinsert.item.cchTextMax = 6;



SendMessage(hwndTreeCheck, TVM_SETIMAGELIST,(WPARAM)TVSIL_STATE,

(LPARAM)(HIMAGELIST)himlc);



tvinsert.item.pszText = _T("Item01");



SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);

tvinsert.item.pszText = _T("Item02");

SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



tvinsert.item.pszText = _T("Item03");

SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



tvinsert.item.pszText = _T("Item04");

SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



tvinsert.item.pszText = _T("Item05");

SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);



然后在NM_CLICK消息中处理当鼠标单击节点的checkbox图标时变化checkbox的状态用于确认节点被选中:

case NM_CLICK:

{

UINT uFlags = 0;

POINT pt;

GetCursorPos(&pt);

ScreenToClient(GetDlgItem(hWnd, ID_TREECHECK),&pt);



TVHITTESTINFO hti;

hti.pt = pt;

TVITEM item;

//Selected必须是全局变量,然后到处理TVN_ENDLABELEDIT消息这个变量才//有效!!

Selected = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREECHECK),

TVM_HITTEST, 0, (LPARAM)&hti);

uFlags = hti.flags;

if( uFlags & TVHT_ONITEMSTATEICON){

item.mask = TVIF_HANDLE | TVIF_STATE;

item.hItem = Selected;

item.stateMask = TVIS_STATEIMAGEMASK;

item.state = 0;

SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_GETITEM,

(WPARAM)0, (LPARAM)&item);

item.state = INDEXTOSTATEIMAGEMASK((item.state>>12) == 1 ? 2 : 1);

item.stateMask = TVIS_STATEIMAGEMASK;

SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_SETITEM,

(WPARAM)0, (LPARAM)&item);

break;

}

用于树控件消息处理的几个结构

众所周知,Windows程序是消息驱动模式,各种消息由Windows操作系统侦测得到,并由用户创建的窗口所取得。这时,操作系统将消息的具体信息包含在两个典型的消息参数里:LPARAM和WPARAM,它们都是32位无符号整型参数UINT类型。其实,奥妙就在这两个参数当中。它们依据不同的消息,包含不同的参数格式。比如,当用户在窗口中点击鼠标左键时,就会发送WM_LBUTTIONDOWN消息。想想看,点击消息一定要包含点击的鼠标位置,那么自然地,这个消息中的LPARAM中包含了POINT数据类型。其中,低字节是X轴坐标,高字节是Y轴坐标,可以通过LOWORD和HIWORD宏取得:

POINT pt;

pt.x = LOWORD(lParam);

pt.y = HIWORD(lParam);

同时,消息参数WPARAM包含一些标志位flags,用于确定在点击鼠标的同时是否还按下了键盘上的功能键等等。

而有时候,有些消息需要传递一些比较大的结构struct,这时32位的无符号整数UINT肯定是装不下的。那么,就把传递的结构指针(地址)放在消息参数当中。当窗口接收到这种消息时,用户负责把消息参数转换成适当的指针类型即可。比如下面将要说明的NMHDR结构。当控件向其父窗口发送通知消息WM_NOTIFY时,消息参数LPARAM就是一个结构的地址指针。那么使用时,可以这样处理:

NMHDR *pnmhdr = (NMHDR *)lParam;

此外,对于控件来说,当控件上发生什么事件时(产生了相应的消息),那么控件会向其父窗口发送通知消息WM_NOTIFY,其中的lParam是NMHDR结构地址。给出这个结构还要进一步确定究竟是什么通知消息,这时code字段包含具体的消息类型。它可以是通用控件的通知消息,以NM_XXX形式命名,也可以是具体控件的通知消息。对于树控件来说它们以TVN_XXX形式命名。

另外,还要对控件进行控制,这些控制操作是通过向控件发送消息来实现。这些消息以TVM_XXX形式命名。

好了,可以简单地说,树控件上发生了某种事件时,树控件会向其父窗口发送通知消息WM_NOTIFY, 它们是TVN_XXX;当需要控制树控件时,通过SendMessage向它发送通知消息,它们是TVM_XXX。
1.NMHDR结构



typedef struct tagNMHDR {

HWND hwndFrom;

UINT idFrom;

UINT code;

} NMHDR;





该结构包含有关通知消息的信息。

hwndFrom

发送消息的控件窗口句柄。

idFrom

发送消息的控件标识符(ID)。

code

通知码。此成员可以是特殊控件的通知码,例如,TVN_ITEMEXPANDING.。也可以是通用控件的通知码之一。

通用通知码列表如下:

通知码


发送原因

NM_CLICK


用户在控件上单击鼠标左键

NM_DBLCLK


用户在控件上双击鼠标左键

NM_RCLICK


用户在控件上单击鼠标右键

NM_RDBLCLK


用户在控件上双击鼠标右键

NM_RETURN


当控件具有输入焦点时,用户按下回车键

NM_SETFOCUS


控件已经具有输入焦点

NM_KILLFOCUS


控件已经失去焦点

NM_OUTOFMEMORY


因为没有足够的内存可用,控件不能完成指定的操作





此结构一般作为控件通知消息处理函数的参数,例如上面代码中的通知消息处理函数:

afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);

――――――――――――――――――――――――――――――――――――――


2.NM_TREEVIEW结构



typedef struct _NM_TREEVIEW {

NMHDR hdr;

UINT action;

TV_ITEM itemOld;

TV_ITEM itemNew;

POINT ptDrag; }

NM_TREEVIEW;

typedef NM_TREEVIEW FAR *LPNM_TREEVIEW;





此结构包含树控件消息的信息。

此结构中action成员是特定的通知消息位。它表明是由鼠标动作引起的选择操作的改变,还是由键盘动作引起的。比如,当在树控件的小加号/减号(+/-)上点击鼠标左键时,带有子节点的项就要展开或者合拢。那么究竟是展开还是合拢,就要通过这个action字段来区分。当action == TVE_EXPAND时,就展开;当action == TVE_COLLAPSE时就合拢。 而itemOld和itemNew分别是旧结点的状态和新结点的状态。ptDrag是引起消息发送的事件发生时的鼠标在客户区上的坐标。

在通知消息处理函数中一般使用HMHDR指针,例如:

afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);

而有时,甚至是大多数时候,在消息处理函数需要使用NM_TREEVIEW对象,这时可以将消息处理函数传进来的NMHDR指针强制转换为NM_TREEVIEW指针。例如:

void CComDlg::OnSelchangedTreeview(NMHDR* pNMHDR, LRESULT* pResult)

{

NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;

CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);

HTREEITEM hSelected = pNMTreeView->itemNew.hItem;

if (hSelected != NULL) {

char text[31];

TV_ITEM item;

item.mask = TVIF_HANDLE | TVIF_TEXT;

item.hItem = hSelected;

item.pszText = text;

item.cchTextMax = 30;

VERIFY(pTree->GetItem(&item));

SetDlgItemText(IDC_STATIC_TREEVIEW1,

pTree->GetItemText(hSelected));

}



*pResult = 0;

}



――――――――――――――――――――――――――――――――――――

此函数处理了NM_SELCHANGED消息,在树控件中某项的选择状态被改变后,在一个静态文本框中显示改变后新项的项标签文本。其实这段代码也可以用如下代码代替:

CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);

HTREEITEM hSelected = pTree->GetSelectedItem();

if (hSelected != NULL) {

SetDlgItemText(IDC_STATIC_TREEVIEW1,

pTree->GetItemText(hSelected));

}

pResult = 0;

――――――――――――――――――――――――――――――――――――

区别是前一段代码中选择在程序栈中为项标签文本缓冲区分配内存,而后一段代码中选择在程序堆中为项标签文本分配内存。

基于TCP/IP的多线程通信及其在远程监控系统中的应用

 传统的应用程序都是单线程的,即在程序运行期间,由单个线程独占CPU的控制权,负责执行所有任务。在这种情况下,程序在执行一些比较费时的任务时,就无法及时响应用户的操作,影响了应用程序的实时性能。在监控系统,特别是远程监控系统中,应用程序往往不但要及时把监控对象的最新信息反馈给监视客户(通过图形显示),还要处理本地机与远程机之间的通信以及对控制对象的实时控制等任务,这时,仅仅由单个线程来完成所有任务,显然无法满足监控系统的实时性要求。在DOS系统下,这些工作可以由中断来完成。而在Windows NT下,中断机制对用户是不透明的。为此,可引进多线程机制,主线程专门负责消息的响应,使程序能够响应命令和其他事件。辅助线程可以用于完成其他比较费时的工作,如通信、图形显示和后台打印等,这样就不至于影响主线程的运行。

  1 Windows NT 多线程概述

  Windows NT是一个真正的抢占式多任务操作系统。在 Windows NT中,启动一个应用程序就是启动该应用程序的一个实例,即进程。进程由一个或多个线程构成,拥有内存和资源,但自己不能执行自己,而是进程中的线程被调度执行。进程至少要有一个线程,当创建一个进程时,就创建了一个线程,即主线程。主线程可以创建其他辅助线程,由主线程创建的线程又可创建线程。每个线程都可指定优先级,操作系统根据线程的优先级调度线程的执行。

  Windows NT中使用多线程的方法有三种:

  · 使用C多线程库函数;

  · 使用CreateThread() 等Win32函数;

  · 使用MFC类。

  本文采用第三种方法。在Visual C++5.0 中,MFC应用程序用CWinThread 对象表示线程。基本操作如下:

  · 创建新线程:调用MFC全局函数AfxBeginThread ()创建新线程。AfxBeginThread()启动新线程并返回控制,然后,新线程和调用AfxBeginThread()的线程同时运行。它的返回值为指向CWinThread对象的指针;

  · 暂停/恢复线程:调用CWinThread类成员函数SuspendThread()暂停线程的运行,调用ResumeThread()成员函数恢复线程的运行;

  · 终止线程:在线程内部可调用全局函数AfxBeginThread()终止线程的运行,否则,线程执行结束后,线程自动从线程函数返回并释放线程占有的资源。

  2 基于TCP/IP的多线程编程

  TCP/IP是lnternet上广泛使用的一种协议,可用于异种机之间的互联。TCP/IP协议本身是非常复杂的,然而在网络编程中,程序员不必考虑 TCP/IP的实现细节,只需利用协议的网络编程接口Socket(亦称套接字)即可。在 Windows 中,网络编程接口是 Windows Socket它包含标准的Berkley Sockets的功能调用的集合,以及为 Windows 所做的一些扩展。TCP/IP协议的应用一般采用客户/服务器模式,面向连接的应用调用如图1所示。

  根据上述顺序调用函数建立连接后,通信双方便可交换数据。然而,在调用带*号的函数时,操作常会阻塞,特别是当套接字工作在同步阻塞模式(Blocking Mode)时。这时,程序无法响应任何消息。为了避免出现这种情况,本文引进辅助线程。在执行含有可能阻塞的函数的任务时,动态创建新的线程,专门处理该任务。主线程把任务交给辅助线程后,不再对辅助线程加以控制与调度。本文分别针对connect()、accept()、receive()、send ()等可能阻塞的函数创建了相应的线程,如表1所示。

  多线程编程常常还要考虑线程间的通信。线程间的通信可以采用全局变量、指针参数和文件映射等方式。本文采用指针参数方式。在调用AfxBeginThread()函数时,通过传递指针参数的方式在主线程与辅助线程间通信。

  AfxBeginThread()函数的用法如下:

  CWinThread*AfxBeginThread (AFX_THREADPROC pfnThreadproc,

  LPVOID pParam,

  int nPriority=THREAD_PRIORITY_NORMAL,

  UINT nStackSixe=0,

  DWORD dwCreateFlags=0,

  LPSECURITY_ATTRIBUTES pSecurityAttrs=NULL);

  参数pfnThreadProc指定线程函数必须如下定义:

  UINT MyControllingFunction(LPVOID pParam); 

  参数pParam 是调用线程传递给线程函数pfThreadProc的参数;

  其他参数一般只需采用缺省值。

  指针参数通信方式就是通过参数pParam在线程间通信的,它可为指向任何数据类型的指针。本文中,定义了一个名叫EXCHANGE_INFO的结构如下:

  typedef struct

  { SOCKET sServerSocket;

  SOCKET *pcCoientSocket;

  SOCKADDR_IN *pClientAddr;

  BOOL *pbConnected;

  unsigned char *pucBuffer;

  int *pnMessageLen;

  } EXCHANGE_INFO;

  在需要通信时,先声明一个结构变量,再把变量的指针作为pParam参数,调用AfxBeginThread((AFX_THREADPROC) CSocketThread::WaitFor ConnectThread,(LPVOID)& m_Exchangeinfo)函数即可。

  为了利用面向对象技术编程所具有的模块性强、便于修改、可移植性好等优点,本文还把表1中的线程封装为父类为CWinThread的自定义类 CSocketThread中。还自定义了一个叫CSocketComm的新类,封装了一些函数,如CreateSocket、 ConnectToServer、WaitForClient、ReadMessage、SendMessage等,这些函数屏蔽了面向连接的通信程序的实现细节,如创建、连接、发送和接收等,在这些函数里,动态创建辅助线程。

  下面以CSocketComm类中的等待客户连接请求的函数WaitForClient()为例,注释说明多线程编程的具体细节。

  BOOL CSocketComm::WaitForClient

  {

  if(m_bConnected)return( TRUE );

  //配置bind函数的参数即服务器的套接字地址结构

  SOCKADDR_IN Addr;

  memset(&Addr,0,sizeof(SOCKADDR_IN));

  Addr.sin_family=AF_INET;

  ADDR.SIN_port= htonl(m_nPort); 

  Addr.sin_addr.s_addrr = htonl(INADDR_ANY); 

  //将套接字地址结构赋予套接字(绑定),以指定本地半相关

  int nReturnValue;

  nReturnValue =::bind( m_sSserverSocket,( LPSOCKADDR)&Addr,sizeof (SOCKADDR_IN )); 

  if(nReturnValue == SOCKET_ERROR)  returu( FALSE );

  //配置传给WaitForConnectThread线程函数的参数m_Exchangeinfo

  m_Exchangeinfo.sServerSocket = m_serverSocket;

  m_Exchangeinfo.psClientSocket = &m_sClientSocket;

  m_Exchangeinfo.pClientAddr = &m_lientAddr;

  m_Exchangeinfo.pbConnected = &m_bConnected;

  //以m_Exchangeinfo的指针为参数调用WaitforConnectThread线程等待客户端连接

  AfxBeginThread((AFX_THREADPROC)CSocketThread::

  WaitForConnectThread,(LPVOID) &m_Exchanginfo); 

  returi( TRUE )

  }

  //等待连接线程

  UINT CSocketThread::WaitForConnectThread(LPVOIDpParam)

  {

  EXCHANGE_INFO*pExchangelnfo=(EXCHANGE_INFO*) pParam;

  int nReturnValue, nClientAddrSize= Sizeof( SOCKADDR_IN);

  //侦听连接

  nReturnValue=:: listen(pExchangelnfo ->sServerSocket, 1); 

  if( nReturnValue == SOCKET_ERROR )return(0);

  //阻塞调用accept,直至有客户连接请求

  *pExchangelnfo->psClitentSocket=:: accept(pExchangelnfo->sServerSocket, (LPSOCKADDR) pEchangelnfo ->pClientAddr,&nClientAddrSize); 

  if(( *pExchangelnfo->psClitentSocket)!= INVALID_SOCKET)

  //通过pExchangelnfo的指针在线程间通信

  * pExchangelnfo->pbConnected TRUE;

  return( 0 );



  3 应用实例-高层协议的设计

  在电厂和电站中,为了保证安全工作,保护系统必不可少。保护系统的电源供应通常使用两种方式。一般情况下,使用交流电系统对保护系统进行供电;当交流电系统出现故障时立即使用后备的蓄电池系统对保护系统进行供电。为了对蓄电池系统进行监控和管理,以保证蓄电池在关键时刻能正常工作,设计了在 Windows NT环境下具有远程通讯功能和动态人机界面的智能蓄电池远程监控系统。该系统由蓄电池智能管理、充电机控制、母线绝缘在线检测、声光报警、系统组态、远程通信等子系统组成,实现对蓄电池/充电机智能化远程管理和控制,对整个系统的运行状态进行实时监控,具有多媒体报警、事件处理、动态数据库、趋势画面和动态画面显示、操作提前提醒等功能。系统框图如图2所示。在远程通信模块中,远程监控机需把监控客户的操作命令及时传给本地机,本地机根据命令控制充电机,使之按照一定的方式工作,而本地机需定时向远程监控机反馈实时的充电机状态信息。它们之间的通信是基于TCP/IP的广域网通信,而且,我们引进了多线程机制以保证系统具有良好的实时性。

  下面以其中的充电机控制系统为例谈谈如何使用CSocketComm类进行远程通信。为简单起见,假定本地机与远程监控机之间通信的信息仅有下面三种类型:

  ·本地机接收到该命令后,控制充电机按照稳压模式运行,输出电压为电压给定值;

  ·本地机接收到该命令后,控制充电机按照稳流定时模式运行,输出电流为电流给定值;

  ·本地机向远程监控机发送充电机的实时状态数据(包括输出电压、输出电流、状态指示和故障类型指示)。

  在基于TCP/IP的面向连接的网络通信中,客户与服务器之间传送的是有序可靠的字节流(Byte Stream),所以程序员有必要在传输层TCP上定义自己的高层协议,设计帧结构,将字节流变成有意义的信息。在CSocketComm类中由 AssembleMessage()函数把数据组合成一定的帧结构。帧结构为:



  其中@为帧起始标志,#为帧终结标志

  对应的结构定义如下:

typedef struct

{ int MessageType; //信息类型

int ChargerNo; //充电机编号

int DataNo; //数据类型

float Data; //数据

} MessageStruct;

  需要通信时,先声明一个 MessageStruct变量,根据信息内容对各成员变量赋值,传给 AssembleMessage()函数组合成帧,再调用SendMessage()函数发送给接受方。接受方接到数据后,对数据内容的解释,是由 CsocketComm类中的AnalyzeMessage()函数完成的。AnalyzeMessage()函数返回一个 MessageStruct变量。应用程序就可根据它的各成员变量控制充电机或动态显示充电机的状态。

  总之,把多线程机制引进通信,有利于提高应用程序的实时性,充分利用系统资源。对于大型的工程应用来说,不同的线程完成不同的任务,也有利于提高程序的模块化,便于维护和扩展。本文给出了一种在Windows NT下基于TCP/IP协议的多线程通信的基本方法,根据该方法进行修改和扩充,便可设计出符合具体应用的高质量的多线程通信程序。

VC中利用多线程技术实现线程之间的通信

本文章地址:http://www.jztop.com/dev/vc/20060206/11217.html [点此复制地址]

 当前流行的Windows操作系统能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线 程,线程提供了多任务处理的能力。用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义。现在 的大型应用软件无一不是多线程多任务处理,单线程的软件是不可想象的。因此掌握多线程多任务设计方法对每个程序员都是必需要掌握的。本实例针对多线程技术 在应用中经常遇到的问题,如线程间的通信、同步等,分别进行探讨,并利用多线程技术进行线程之间的通信,实现了数字的简单排序。  

  一、 实现方法

  1、理解线程

   要讲解线程,不得不说一下进程,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成。进程在运行时创建的资源随 着进程的终止而死亡。线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应于Visual C++中的CwinThread类对象。单独一个执行程序运行时,缺省地包含的一个主线程,主线程以函数地址的形式出现,提供程序的启动点,如main ()或WinMain()函数等。当主线程终止时,进程也随之终止。根据实际需要,应用程序可以分解成许多独立执行的线程,每个线程并行的运行在同一进程 中。

  一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间 片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程 在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。

  线程 被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程 CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进程终止。工作线程用来执行程序的后台处 理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CWinThread类派生来创建,对它来说最重要的是如何实现工作线程 任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同 时每个线程可以拥有自己的变量。

  2、线程的管理和操作

  (一)线程的启动

  创建一个用户界 面线程,首先要从类CwinThread产生一个派生类,同时必须使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE来声 明和实现这个CwinThread派生类。第二步是根据需要重载该派生类的一些成员函数如:ExitInstance()、InitInstance ()、OnIdle()、PreTranslateMessage()等函数。最后调用AfxBeginThread()函数的一个版本: CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ) 启动该用户界面线程,其中第一个参数为指向定义的用户界面线程类指针变量,第二个参数为线程的优先级,第三个参数为线程所对应的堆栈大小,第四个参数为线 程创建时的附加标志,缺省为正常状态,如为CREATE_SUSPENDED则线程启动后为挂起状态。

  对于工作线程来说,启动一个线 程,首先需要编写一个希望与应用程序的其余部分并行运行的函数如Fun1(),接着定义一个指向CwinThread对象的指针变量*pThread,调 用AfxBeginThread(Fun1,param,priority)函数,返回值赋给pThread变量的同时一并启动该线程来执行上面的 Fun1()函数,其中Fun1是线程要运行的函数的名字,也既是上面所说的控制函数的名字,param是准备传送给线程函数Fun1的任意32位值, priority则是定义该线程的优先级别,它是预定义的常数,读者可参考MSDN。

  (二)线程的优先级

  以下的CwinThread类的成员函数用于线程优先级的操作:

int GetThreadPriority();
BOOL SetThradPriority()(int nPriority);

   上述的二个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次而言的,处于同一优先权层次的线程,优先级高的线程先运 行;处于不同优先权层次上的线程,谁的优先权层次高,谁先运行。至于优先级设置所需的常数,自己参考MSDN就可以了,要注意的是要想设置线程的优先级, 这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。对于线程的优先权层次的设置,CwinThread类没有提供相应的函 数,但是可以通过Win32 SDK函数GetPriorityClass()和SetPriorityClass()来实现。

  (三)线程的悬挂和恢复

   CWinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数,其中SuspendThread()用来悬挂线程,暂停线程的执行; ResumeThread()用来恢复线程的执行。如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的 ResumeThread()来恢复线程的运行。

  (四)结束线程

  终止线程有三种途径,线程可以在自身内部调用 AfxEndThread()来终止自身的运行;可以在线程的外部调用BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )来强行终止一个线程的运行,然后调用CloseHandle()函数释放线程所占用的堆栈;第三种方法是改变全局变量,使线程的执行函数返回,则该线程 终止。下面以第三种方法为例,给出部分代码:

////////////////////////////////////////////////////////////////
//////CtestView message handlers
/////Set to True to end thread
Bool bend=FALSE;//定义的全局变量,用于控制线程的运行;
//The Thread Function;
UINT ThreadFunction(LPVOID pParam)//线程函数
{
 while(!bend)
 {
  Beep(100,100);
  Sleep(1000);
 }
 return 0;
}
/////////////////////////////////////////////////////////////
CwinThread *pThread;
HWND hWnd;
Void CtestView::OninitialUpdate()
{
 hWnd=GetSafeHwnd();
 pThread=AfxBeginThread(ThradFunction,hWnd);//启动线程
 pThread->m_bAutoDelete=FALSE;//线程为手动删除
 Cview::OnInitialUpdate();
}
////////////////////////////////////////////////////////////////
Void CtestView::OnDestroy()
{
 bend=TRUE;//改变变量,线程结束
 WaitForSingleObject(pThread->m_hThread,INFINITE);//等待线程结束
 delete pThread;//删除线程
 Cview::OnDestroy();
}

  3、线程之间的通信

   通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种 方法实现这种通信任务:使用全局变量(上一节的例子其实使用的就是这种方法)、使用事件对象、使用消息。这里我们主要介绍后两种方法。

  (一) 利用用户定义的消息通信

   在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的非 常简单。首先用户要定义一个用户消息,如下所示:#define WM_USERMSG WMUSER+100;在需要的时候,在一个线程中调用::PostMessage((HWND)param,WM_USERMSG,0,0)或 CwinThread::PostThradMessage()来向另外一个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的目的窗口的句 柄、要发送的消息标志符、消息的参数WPARAM和LPARAM。下面的代码是对上节代码的修改,修改后的结果是在线程结束时显示一个对话框,提示线程结 束:

UINT ThreadFunction(LPVOID pParam)
{
 while(!bend)
 {
  Beep(100,100);
  Sleep(1000);
 }
 ::PostMessage(hWnd,WM_USERMSG,0,0);
 return 0;
}
////////WM_USERMSG消息的响应函数为OnThreadended(WPARAM wParam,
LPARAM lParam)
LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam)
{
 AfxMessageBox("Thread ended.");
 Retrun 0;
}

   上面的例子是工作者线程向用户界面线程发送消息,对于工作者线程,如果它的设计模式也是消息驱动的,那么调用者可以向它发送初始化、退出、执行某种特定 的处理等消息,让它在后台完成。在控制函数中可以直接使用::GetMessage()这个SDK函数进行消息分检和处理,自己实现一个消息循环。 GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的时间片让给其它线程,不无效的占用CPU的时间,如果消息队列不为空, 就获取这个消息,判断这个消息的内容并进行相应的处理。

  (二)用事件对象实现通信

  在线程之间传递信号进行通信比较复杂的方法是使用事件对象,用MFC的Cevent类的对象来表示。事件对象处于两种状态之一:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。上述例子代码修改如下:

////////////////////////////////////////////////////////////////////
Cevent threadStart ,threadEnd;
UINT ThreadFunction(LPVOID pParam)
{
 ::WaitForSingleObject(threadStart.m_hObject,INFINITE);
 AfxMessageBox("Thread start.");
 while(!bend)
 {
  Beep(100,100);
  Sleep(1000);
  Int result=::WaitforSingleObject(threadEnd.m_hObject,0);
  //等待threadEnd事件有信号,无信号时线程在这里悬停
  If(result==Wait_OBJECT_0)
   Bend=TRUE;
 }
 ::PostMessage(hWnd,WM_USERMSG,0,0);
 return 0;
}
/////////////////////////////////////////////////////////////
Void CtestView::OninitialUpdate()
{
 hWnd=GetSafeHwnd();
 threadStart.SetEvent();//threadStart事件有信号
 pThread=AfxBeginThread(ThreadFunction,hWnd);//启动线程
 pThread->m_bAutoDelete=FALSE;
 Cview::OnInitialUpdate();
}
////////////////////////////////////////////////////////////////
Void CtestView::OnDestroy()
{
 threadEnd.SetEvent();
 WaitForSingleObject(pThread->m_hThread,INFINITE);
 delete pThread;
 Cview::OnDestroy();
}

  运行这个程序,当关闭程序时,才显示提示框,显示"Thread ended"。
4、线程之间的同步

   前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏 数据的完整性。保证各个线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC支持的七个多线程的同步类可以分成两大类:同步对象 (CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象 (CmultiLock和CsingleLock)。本节主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调工作,程序运行起来更安全。

  (一) 临界区

   临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都 可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数 据。临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成 之后,调用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。同时启动两个线程,它 们对应的函数分别为WriteThread()和ReadThread(),用以对公共数组组array[]操作,下面的代码说明了如何使用临界区对象:

#include "afxmt.h"
int array[10],destarray[10];
CCriticalSection Section;
UINT WriteThread(LPVOID param)
{
 Section.Lock();
 for(int x=0;x<10;x++)
  array[x]=x;
 Section.Unlock();
}
UINT ReadThread(LPVOID param)
{
 Section.Lock();
 For(int x=0;x<10;x++)
  Destarray[x]=array[x];
  Section.Unlock();
}

  上述代码运行的结果应该是Destarray数组中的元素分别为1-9,而不是杂乱无章的数,如果不使用同步,则不是这个结果,有兴趣的读者可以实验一下。

  (二)互斥

   互斥与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互 斥与Cmutex类的对象相对应,使用互斥对象时,必须创建一个CSingleLock或CMultiLock对象,用于实际的访问控制,因为这里的例子 只处理单个互斥,所以我们可以使用CSingleLock对象,该对象的Lock()函数用于占有互斥,Unlock()用于释放互斥。实现代码如下:

#include "afxmt.h"
int array[10],destarray[10];
CMutex Section;

UINT WriteThread(LPVOID param)
{
 CsingleLock singlelock;
 singlelock (&Section);
 singlelock.Lock();
 for(int x=0;x<10;x++)
  array[x]=x;
 singlelock.Unlock();
}

UINT ReadThread(LPVOID param)
{
 CsingleLock singlelock;
 singlelock (&Section);
 singlelock.Lock();
 For(int x=0;x<10;x++)
  Destarray[x]=array[x];
  singlelock.Unlock();
}

  (三)信号量

   信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,创建一个信号量需要用Csemaphore类声明一个对象,一旦 创建了一个信号量对象,就可以用它来对资源的访问技术。要实现计数处理,先创建一个CsingleLock或CmltiLock对象,然后用该对象的 Lock()函数减少这个信号量的计数值,Unlock()反之。下面的代码分别启动三个线程,执行时同时显示二个消息框,然后10秒后第三个消息框才得 以显示。

/////////////////////////////////////////////////////////////////////////
Csemaphore *semaphore;
Semaphore=new Csemaphore(2,2);
HWND hWnd=GetSafeHwnd();
AfxBeginThread(threadProc1,hWnd);
AfxBeginThread(threadProc2,hWnd);
AfxBeginThread(threadProc3,hWnd);
UINT ThreadProc1(LPVOID param)
{
 CsingleLock singelLock(semaphore);
 singleLock.Lock();
 Sleep(10000);
 ::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK);
 return 0;
}
UINT ThreadProc2(LPVOID param)
{
 CSingleLock singelLock(semaphore);
 singleLock.Lock();
 Sleep(10000);
 ::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK);
 return 0;
}

UINT ThreadProc3(LPVOID param)
{
 CsingleLock singelLock(semaphore);
 singleLock.Lock();
 Sleep(10000);
 ::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK);
 return 0;
}


  二、 编程步骤

  1、 启动Visual C++6.0,生成一个32位的控制台程序,将该程序命名为"sequence"

  2、 输入要排续的数字,声明四个子线程;

  3、 输入代码,编译运行程序。

三、 程序代码

//////////////////////////////////////////////////////////////////////////////////////
// sequence.cpp : Defines the entry point for the console application.
/*
主要用到的WINAPI线程控制函数,有关详细说明请查看MSDN;
线程建立函数:
HANDLE CreateThread(
 LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性结构指针,可为NULL;
 DWORD dwStackSize, // 线程栈大小,若为0表示使用默认值;
 LPTHREAD_START_ROUTINE lpStartAddress, // 指向线程函数的指针;
 LPVOID lpParameter, // 传递给线程函数的参数,可以保存一个指针值;
 DWORD dwCreationFlags, // 线程建立是的初始标记,运行或挂起;
 LPDWORD lpThreadId // 指向接收线程号的DWORD变量;
);

对临界资源控制的多线程控制的信号函数:

HANDLE CreateEvent(
 LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性结构指针,可为NULL;
 BOOL bManualReset, // 手动清除信号标记,TRUE在WaitForSingleObject后必须手动//调用RetEvent清除信号。若为 FALSE则在WaitForSingleObject
 //后,系统自动清除事件信号;
 BOOL bInitialState, // 初始状态,TRUE有信号,FALSE无信号;
 LPCTSTR lpName // 信号量的名称,字符数不可多于MAX_PATH;
 //如果遇到同名的其他信号量函数就会失败,如果遇
 //到同类信号同名也要注意变化;
);

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性结构指针,可为NULL
 BOOL bInitialOwner, // 当前建立互斥量是否占有该互斥量TRUE表示占有,
 //这样其他线程就不能获得此互斥量也就无法进入由
 //该互斥量控制的临界区。FALSE表示不占有该互斥量
 LPCTSTR lpName // 信号量的名称,字符数不可多于MAX_PATH如果
 //遇到同名的其他信号量函数就会失败,
 //如果遇到同类信号同名也要注意变化;
);

//初始化临界区信号,使用前必须先初始化
VOID InitializeCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection // 临界区变量指针
);

//阻塞函数
//如果等待的信号量不可用,那么线程就会挂起,直到信号可用
//线程才会被唤醒,该函数会自动修改信号,如Event,线程被唤醒之后
//Event信号会变得无信号,Mutex、Semaphore等也会变。
DWORD WaitForSingleObject(
 HANDLE hHandle, // 等待对象的句柄
 DWORD dwMilliseconds // 等待毫秒数,INFINITE表示无限等待
);
//如果要等待多个信号可以使用WaitForMutipleObject函数
*/

#include "stdafx.h"
#include "stdlib.h"
#include "memory.h"
HANDLE evtTerminate; //事件信号,标记是否所有子线程都执行完
/*
下面使用了三种控制方法,你可以注释其中两种,使用其中一种。
注意修改时要连带修改临界区PrintResult里的相应控制语句
*/
HANDLE evtPrint; //事件信号,标记事件是否已发生
//CRITICAL_SECTION csPrint; //临界区
//HANDLE mtxPrint; //互斥信号,如有信号表明已经有线程进入临界区并拥有此信号
static long ThreadCompleted = 0;
/* 用来标记四个子线程中已完成线程的个数,当一个子线程完成时就对ThreadCompleted进行加一操作, 要使用InterlockedIncrement(long* lpAddend)和InterlockedDecrement(long* lpAddend)进行加减操作*/

//下面的结构是用于传送排序的数据给各个排序子线程
struct MySafeArray
{
 long* data;
 int iLength;
};

//打印每一个线程的排序结果
void PrintResult(long* Array, int iLength, const char* HeadStr = "sort");

//排序函数
unsigned long __stdcall BubbleSort(void* theArray); //冒泡排序
unsigned long __stdcall SelectSort(void* theArray); //选择排序
unsigned long __stdcall HeapSort(void* theArray); //堆排序
unsigned long __stdcall InsertSort(void* theArray); //插入排序
/*以上四个函数的声明必须适合作为一个线程函数的必要条件才可以使用CreateThread
建立一个线程。
(1)调用方法必须是__stdcall,即函数参数压栈顺序由右到左,而且由函数本身负责
栈的恢复, C和C++默认是__cdecl, 所以要显式声明是__stdcall
(2)返回值必须是unsigned long
(3)参数必须是一个32位值,如一个指针值或long类型
(4) 如果函数是类成员函数,必须声明为static函数,在CreateThread时函数指针有特殊的写法。如下(函数是类CThreadTest的成员函数中):
static unsigned long _stdcall MyThreadFun(void* pParam);
handleRet = CreateThread(NULL, 0, &CThreadTestDlg::MyThreadFun, NULL, 0, &ThreadID);
之所以要声明为static是由于,该函数必须要独立于对象实例来使用,即使没有声明实例也可以使用。*/

int QuickSort(long* Array, int iLow, int iHigh); //快速排序

int main(int argc, char* argv[])
{
 long data[] = {123,34,546,754,34,74,3,56};
 int iDataLen = 8;
 //为了对各个子线程分别对原始数据进行排序和保存排序结果
 //分别分配内存对data数组的数据进行复制
 long *data1, *data2, *data3, *data4, *data5;
 MySafeArray StructData1, StructData2, StructData3, StructData4;
 data1 = new long[iDataLen];
 memcpy(data1, data, iDataLen << 2); //把data中的数据复制到data1中
 //内存复制 memcpy(目标内存指针, 源内存指针, 复制字节数), 因为long的长度
 //为4字节,所以复制的字节数为iDataLen << 2, 即等于iDataLen*4
 StructData1.data = data1;
 StructData1.iLength = iDataLen;
 data2 = new long[iDataLen];
 memcpy(data2, data, iDataLen << 2);
 StructData2.data = data2;
 StructData2.iLength = iDataLen;
 data3 = new long[iDataLen];
 memcpy(data3, data, iDataLen << 2);
 StructData3.data = data3;
 StructData3.iLength = iDataLen;
 data4 = new long[iDataLen];
 memcpy(data4, data, iDataLen << 2);
 StructData4.data = data4;
 StructData4.iLength = iDataLen;
 data5 = new long[iDataLen];
 memcpy(data5, data, iDataLen << 2);
 unsigned long TID1, TID2, TID3, TID4;
 //对信号量进行初始化
 evtTerminate = CreateEvent(NULL, FALSE, FALSE, "Terminate");
 evtPrint = CreateEvent(NULL, FALSE, TRUE, "PrintResult");
 //分别建立各个子线程
 CreateThread(NULL, 0, &BubbleSort, &StructData1, NULL, &TID1);
 CreateThread(NULL, 0, &SelectSort, &StructData2, NULL, &TID2);
 CreateThread(NULL, 0, &HeapSort, &StructData3, NULL, &TID3);
 CreateThread(NULL, 0, &InsertSort, &StructData4, NULL, &TID4);
 //在主线程中执行行快速排序,其他排序在子线程中执行
 QuickSort(data5, 0, iDataLen - 1);
 PrintResult(data5, iDataLen, "Quick Sort");
 WaitForSingleObject(evtTerminate, INFINITE); //等待所有的子线程结束
 //所有的子线程结束后,主线程才可以结束
 delete[] data1;
 delete[] data2;
 delete[] data3;
 delete[] data4;
 CloseHandle(evtPrint);
 return 0;
}

/*
冒泡排序思想(升序,降序同理,后面的算法一样都是升序):从头到尾对数据进行两两比较进行交换,小的放前大的放后。这样一次下来,最大的元素就会被交换的最后,然后下一次
循环就不用对最后一个元素进行比较交换了,所以呢每一次比较交换的次数都比上一次循环的次数少一,这样N次之后数据就变得升序排列了*/
unsigned long __stdcall BubbleSort(void* theArray)
{
 long* Array = ((MySafeArray*)theArray)->data;
 int iLength = ((MySafeArray*)theArray)->iLength;
 int i, j=0;
 long swap;
 for (i = iLength-1; i > 0; i--)
 {
  for(j = 0; j < i; j++)
  {
   if(Array[j] > Array[j+1]) //前比后大,交换
   {
    swap = Array[j];
    Array[j] = Array[j+1];
    Array[j+1] = swap;
   }
  }
 }
 PrintResult(Array, iLength, "Bubble Sort"); //向控制台打印排序结果
 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1
 if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完
 //若都执行完则设置程序结束信号量
 return 0;
}

/* 选择排序思想:每一次都从无序的数据中找出最小的元素,然后和前面已经有序的元素序列的后一个元素进行交换,这样整个源序列就会分成两部分,前面一部分是 已经排好序的有序序列,后面一部分是无序的,用于选出最小的元素。循环N次之后,前面的有序序列加长到跟源序列一样长,后面的无序部分长度变为0,排序就 完成了。*/
unsigned long __stdcall SelectSort(void* theArray)
{
 long* Array = ((MySafeArray*)theArray)->data;
 int iLength = ((MySafeArray*)theArray)->iLength;
 long lMin, lSwap;
 int i, j, iMinPos;
 for(i=0; i < iLength-1; i++)
 {
  lMin = Array[i];
  iMinPos = i;
  for(j=i + 1; j <= iLength-1; j++) //从无序的元素中找出最小的元素
  {
   if(Array[j] < lMin)
   {
    iMinPos = j;
    lMin = Array[j];
   }
  }
  //把选出的元素交换拼接到有序序列的最后
  lSwap = Array[i];
  Array[i] = Array[iMinPos];
  Array[iMinPos] = lSwap;
 }
 PrintResult(Array, iLength, "Select Sort"); //向控制台打印排序结果
 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1
 if(ThreadCompleted == 4) SetEvent(evtTerminate);//检查是否其他线程都已执行完
 //若都执行完则设置程序结束信号量
 return 0;
}

/* 堆排序思想:堆:数据元素从1到N排列成一棵二叉树,而且这棵树的每一个子树的根都是该树中的元素的最小或最大的元素这样如果一个无序数据集合是一个堆那 么,根元素就是最小或最大的元素堆排序就是不断对剩下的数据建堆,把最小或最大的元素析透出来。下面的算法,就是从最后一个元素开始,依据一个节点比父节 点数值大的原则对所有元素进行调整,这样调整一次就形成一个堆,第一个元素就是最小的元素。然后再对剩下的无序数据再进行建堆,注意这时后面的无序数据元 素的序数都要改变,如第一次建堆后,第二个元素就会变成堆的第一个元素。*/
unsigned long __stdcall HeapSort(void* theArray)
{
 long* Array = ((MySafeArray*)theArray)->data;
 int iLength = ((MySafeArray*)theArray)->iLength;
 int i, j, p;
 long swap;
 for(i=0; i<iLength-1; i++)
 {
  for(j = iLength - 1; j>i; j--) //从最后倒数上去比较字节点和父节点
  {
   p = (j - i - 1)/2 + i; //计算父节点数组下标
   //注意到树节点序数跟数组下标不是等同的,因为建堆的元素个数逐个递减
   if(Array[j] < Array[p]) //如果父节点数值大则交换父节点和字节点
   {
    swap = Array[j];
    Array[j] = Array[p];
    Array[p] = swap;
   }
  }
 }
 PrintResult(Array, iLength, "Heap Sort"); //向控制台打印排序结果
 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1
 if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完
 //若都执行完则设置程序结束信号量
 return 0;
}

/*插入排序思想:把源数据序列看成两半,前面一半是有序的,后面一半是无序的,把无序的数据从头到尾逐个逐个的插入到前面的有序数据中,使得有序的数据的个数不断增大,同时无序的数据个数就越来越少,最后所有元素都会变得有序。*/
unsigned long __stdcall InsertSort(void* theArray)
{
 long* Array = ((MySafeArray*)theArray)->data;
 int iLength = ((MySafeArray*)theArray)->iLength;
 int i=1, j=0;
 long temp;
 for(i=1; i<iLength; i++)
 {
  temp = Array[i]; //取出序列后面无序数据的第一个元素值
  for(j=i; j>0; j--) //和前面的有序数据逐个进行比较找出合适的插入位置
  {
   if(Array[j - 1] > temp) //如果该元素比插入值大则后移
    Array[j] = Array[j - 1];
   else //如果该元素比插入值小,那么该位置的后一位就是插入元素的位置
    break;
  }
  Array[j] = temp;
 }
 PrintResult(Array, iLength, "Insert Sort"); //向控制台打印排序结果
 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1
 if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完
  //若都执行完则设置程序结束信号量
 return 0;
}

/*快速排序思想:快速排序是分治思想的一种应用,它先选取一个支点,然后把小于支点的元素交换到支点的前边,把大于支点的元素交换到支点的右边。然后再对支点左边部分和右
边部分进行同样的处理,这样若干次之后,数据就会变得有序。下面的实现使用了递归
建 立两个游标:iLow,iHigh;iLow指向序列的第一个元素,iHigh指向最后一个先选第一个元素作为支点,并把它的值存贮在一个辅助变量里。那 么第一个位置就变为空并可以放置其他的元素。 这样从iHigh指向的元素开始向前移动游标,iHigh查找比支点小的元素,如果找到,则把它放置到空置了的位置(现在是第一个位置),然后iHigh 游标停止移动,这时iHigh指向的位置被空置,然后移动iLow游标寻找比支点大的元素放置到iHigh指向的空置的位置,如此往复直到iLow与 iHigh相等。最后使用递归对左右两部分进行同样处理*/

int QuickSort(long* Array, int iLow, int iHigh)
{
 if(iLow >= iHigh) return 1; //递归结束条件
 long pivot = Array[iLow];
 int iLowSaved = iLow, iHighSaved = iHigh; //保未改变的iLow,iHigh值保存起来
 while (iLow < iHigh)
 {
  while (Array[iHigh] >= pivot && iHigh > iLow) //寻找比支点大的元素
   iHigh -- ;
  Array[iLow] = Array[iHigh]; //把找到的元素放置到空置的位置
  while (Array[iLow] < pivot && iLow < iHigh) //寻找比支点小的元素
   iLow ++ ;
  Array[iHigh] = Array[iLow]; //把找到的元素放置到空置的位置
 }
 Array[iLow] = pivot; //把支点值放置到支点位置,这时支点位置是空置的
 //对左右部分分别进行递归处理
 QuickSort(Array, iLowSaved, iHigh-1);
 QuickSort(Array, iLow+1, iHighSaved);
 return 0;
}

//每一个线程都要使用这个函数进行输出,而且只有一个显示器,产生多个线程
//竞争对控制台的使用权。
void PrintResult(long* Array, int iLength, const char* HeadStr)
{
 WaitForSingleObject(evtPrint, INFINITE); //等待事件有信号
 //EnterCriticalSection(&csPrint); //标记有线程进入临界区
 //WaitForSingleObject(mtxPrint, INFINITE); //等待互斥量空置(没有线程拥有它)
 int i;
 printf("%s: ", HeadStr);
 for (i=0; i<iLength-1; i++)
 {
  printf("%d,", Array[i]);
  Sleep(100); //延时(可以去掉)
/*只是使得多线程对临界区访问的问题比较容易看得到
如果你把临界控制的语句注释掉,输出就会变得很凌乱,各个排序的结果会
分插间隔着输出,如果不延时就不容易看到这种不对临界区控制的结果
*/
 }
 printf("%dn", Array[i]);
 SetEvent(evtPrint); //把事件信号量恢复,变为有信号
}

  四、 小结

  对复杂的应用程序来说,线程的应用给应用程序提供了高效、快速、安全的数据处理能力。本实例讲述了线程处理中经常遇到的问题,希望对读者朋友有一定的帮助,起到抛砖引玉的作用。