• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            sherrylso

            C++博客 首頁 新隨筆 聯系 聚合 管理
              18 Posts :: 0 Stories :: 124 Comments :: 0 Trackbacks

            #

            摘要:
                    在對多線程并發的編程環境下,死鎖是我們經常碰到的和經常需要解決的問題。所謂死鎖,即:由于資源占用是互斥的,當某個線(進)程提出申請資源后,使得有關進程在無外力協助下,永遠分配不到必需的資源而無法繼續運行,這就產生了一種特殊現象死鎖,如下圖:

                    線程#1在獲得Lock A后,需要獲得Lock B,而同時,線程#2在Lock B后,需要獲得Lock A。對于線程#1和#2,由于都不能獲得滿足的條件,而無法繼續執行,死鎖就形成了。
                    死鎖是多線程并發編程的大難題,我們可以通過Log Trace、多線程編程輔助工具、IDE調試環境等手段進行調試、跟蹤。然而,另一個更難對付的問題是“假死鎖”(我在這里暫且稱為“假死鎖”,實在找不到什么更好的稱呼)。所謂的假死鎖,我給出的定義是:在有限的時間內的死鎖。與死鎖不同的是,其持續的時間是有限的,而大家都知道,死鎖持續的時間是無限的,如果碰到死鎖,程序接下來是什么都干不了了。而正是由于假死鎖的相對的持續時間,給我們編程人員會帶來更大的麻煩??梢韵胂蟮玫?,我們想通過某些工具來Trace這樣一個特定的時間段是非常困難的,更多的情況下,我們需要結合LOG進行合理的分析,使得問題得以解決。本文就假死鎖產生的條件,環境,以及解決的辦法做一個討論。
            一、假死鎖的產生條件。

                考慮下面的例子(我只是給給出了偽代碼),假設我們系統中的線程個數是確定的,有限的。在本例中,系統總的線程數目是3。如下圖:

            線程#1,#2,#3都可能被調度進入臨界區A,我們假設線程#1執行臨界區A時花費了10s的時間,而在這10s的時間里,線程#2與線程#3都處于等待的狀態。也就是說:在這個10s的時間里,系統是沒法響應任何的其他請求。我們稱之為10s的假死鎖。如果在這段時間里,系統需要一些關鍵的請求被執行,這些關鍵請求是需要real time地被處理,比如說是Timer事件,則后果是不堪設想的。(注意:我們的假定是系統中的線程只有#1,#2,#3)。
                   以此,總結一下發生假死鎖的條件,如下:
            --〉臨界區的代碼在集中的時間段內,可能被系統中的任意線程執行,完全由操作系統決定。
            --〉臨界區的代碼在某些情況下,可能是很耗時的。(比如:其執行時間大于100ms,或者,甚至是秒級別的)
            二、在Proactor(IOCP)中的假死鎖。
                    在前面的文章中,我提到過在windows平臺上,Proactor設計模式是基于IOCP的。在這里,本文不會用過多的語言來闡述Proactor是怎樣的設計,重點放在Proactor的假死鎖及其一些解決的辦法。另外需要說明的是,我這里所說的Proactor,在技術層面上,等同于IOCP,我們也可以按照IOCP來理解我所闡釋的概念。
                    我們都知道,IOCP是靠工作者線程來驅動的。工作者線程與一個完成端口對象相關聯,當IO 請求被投遞到完成端口對象時,這些線程為完成端口服務。需要說明的是,應該創建多少個線程來為完成端口服務,是你的應用設計來決定的(很重要的的一點是:在調用CreateIoCompletionPort時指定的并發線程的個數,和創建的工作者線程的個數是有區別的,詳細的技術細節,請參考其他資料)。但是總的來說,在你的系統交付運行后,工作者線程的線程數目是一個確定的值。其結構圖,大致如下:

                     我們假定使用了線程數目為4的工作者線程來為完成端口服務,它們通過調用來GetQueuedCompletionStatus方法來從完成端口中獲取IO相關的packet,一旦獲得,它們都會回調業務邏輯層的代碼來進行相關的業務邏輯處理。到這里我們看到,假設,在業務邏輯層存在臨界互斥區,并且在某一個集中的時間段內,工作者線程都可能被調度執行該臨界互斥區,那么,假死鎖的條件基本形成,如果某一個線程在該區域花費的時間比較長,假死鎖就會發生。
                    一般來說,解決這樣的問題的關鍵就是打破形成假死鎖的條件:
                   第一、在回調函數里,盡量減少鎖的使用。
                   第二、減量減少臨界互斥區的執行時間。對于一些慢速的操作尤其注意。比如:當你在臨界互斥區訪問慢速的IO操作時(打開文件,讀寫文件等),可能需要考慮Cache機制,通過使用內存來代替慢速的disk。
                   第三、將臨界互斥區代碼委托給另外獨立的線程(或線程組)執行,代價是增加這些線程間的通訊。
                   第四、通過使用流控等手段,避免讓所有的線程在集中的時間段內訪問該臨界互斥區。
            三、結束語:

                     事實上,類似這樣的問題,一旦存在,是很難發現和調試的。不過對于多線程的編程,我們都應該遵守以下的基本原則,以最大化的防止死鎖和假死鎖的發生。

                     --> 盡量減少鎖的使用頻率和保護范圍。
                     --> 當線程在互斥鎖的保護范圍內執行代碼時,應該:盡量減少對慢速IO設備的訪問(如:disk),盡量避免獲得其它互斥資源。
                     --〉正確使用各種鎖,包括:原子操作原語,Read Lock, Write Lock, 和Recursive Lock等。這些鎖在不同的場景下有著不同的作用。
            posted @ 2007-08-12 15:41 愛上龍卷風 閱讀(4107) | 評論 (9)編輯 收藏

                 一般來說,基于CS(client-server)軟件架構的開發技術有很多種。比較常用的有:基于socket的網絡編程、RPC、基于Java技術的RMI(當然C#也有類似技術)、CORBA等。在這里我們只是對基于socket的網絡編程與RMI作個對比,有助于我們了解它們各自的應用領域,幫助我們在面對一個具體問題的時候選用適合的技術。另外,本文所做的討論可以認為是脫離了語言層面的東西,只是對技術的本身做一個討論,無關乎你是用C++、C#或Java 在開發。
            一、RMI技術簡介
                    本文就以Java為例,簡單介紹一下RMI技術。
                    從Java1.1開始,遠程方法調用作為Java分布式對象技術成為Java核心的API之一(在java.rmi.* 包)。RMI的引入,使得Java程序之間能夠實現靈活的,可擴展的分布式通信。RMI允許Java對象存在于多個不同的地址空間,分布在不同的Java虛擬機上。每一個地址空間可以在同一臺主機上或者網絡上不同的計算機上。由于遠程方法調用跨越不同的虛擬機邊界到不同的指定的地址空間,所以沒有對象共享的全局變量,這就需要對象序列化(Object Serialization)API,它使得Java對象能夠在不同的JVM之間傳遞。對象序列化是特別為Java的對象設計的,這就意味著Java程序中的對象可以作為對象參數存?。尚蛄谢膶ο蟊仨殞崿FSerializable接口)。結合RMI和對象序列化機制,就可以訪問越過本地Java虛擬機邊界的對象以及數據。通過RMI,可以調用遠程對象的遠程方法,而通過Java對象序列化機制可以將對象傳遞給這些方法。
                    最基本的Java模型并沒有提供將遠程主機上的Java對象看作本地Java程序地址空間一部分的能力,而RMI禰補了這一不足。另外,由于Java與硬件平臺無關的特性,無論是同構的系統還是異構的系統,RMI不需移植就可以順利運行。
                   RMI為Java平臺的分布式計算提供了一個簡單而直接的模型。因為Java的RMI技術是基于Java平臺的,所以它將Java平臺的安全性和可移植性等優點帶到了分布式計算中。RMI大大擴展Java的網絡計算能力,它為編寫基于分布式對象技術的企業級Internet/Intranet應用提供了強大的系統平臺支持。
                  Java RMI體系結構如下圖:


            二、基于socket的網絡編程
                    當你使用socket進行網絡應用開發的時候,一般的思路是“消息驅動邏輯”,即這樣的軟件系統一般具有以下特點:
                   (1) 客戶端與服務器端依靠消息進行通訊。
                   (2) 客戶端或者服務器端都需要一個消息派遣器,將消息投遞給具體的massage handler
                   (3) 客戶端或者服務器端利用massage handler進行邏輯事務處理
             見下圖:

                    使用socket開發的軟件系統,從技術的本質上來講,有以下幾個特點:
                    (1) 基于TCP協議的通訊
                    (2) 應用程序本身需要提供對消息的序列化處理(所謂的序列化指的是將消息輸出到網絡流中)
                    (3) 客戶端與服務器端需要事先商議好它們之間的通訊協議即它們交互的消息格式
                    (4) 由于是消息驅動邏輯,從本質上決定了這樣的編程模式很難面向對象化
            三、RMI Vs Sochet
                    RMI技術比較socket的網絡編程主要有以下幾個方面:
                    第一、.RMI是面向對象的,而后者不是。
                    第二、.RMI是與語言相綁定的。比如當你使用Java RMI技術的時候,客戶端與服務器端都必須使用Java開發。而socket的網絡編程是使用獨立于開發語言的,甚至獨立于平臺?;趕ocket的網絡編程,客戶端與服務器端可以使用不同開發語言和不同的平臺。
                   第三、從網絡協議棧的觀點來看,RMI與socket的網絡編程處于不同層次上。基于socket的網絡編程位于TCP協議之上,而RMI在TCP協議之上,又定義了自己的應用協議,其傳輸層采用的是Java遠程方法協議(JRMP)。可見,在網絡協議棧上,基于RMI的應用位置更高一些,這也決定了,與socket的網絡編程相比,RMI會喪失一些靈活性和可控性,但是好處是它帶給了應用開發者更多的簡潔,方便和易用。比如:如果你用的是RMI,你不需要關心消息是怎么序列化的,你只需要像本地方法調用一樣,使用RMI。代價是:應用開發者無法很好地控制消息的序列化機制。
                  第四、這是最后一點不同,我認為也是比較重要的一點,就是兩種方法的性能比較,其往往決定著你將使用那種技術來開發你的應用。以下引用Adrian Reber在Network-programming with RMI文中對TCP和RMI所做的一個比較,其做的實驗主要是對兩者在網絡傳輸的帶寬上作的對比: 在網絡上傳輸2 byte的有效數據,對于TCP而言,總共有478 byte被額外傳輸,而對于RMI, 1645byte被額外傳輸。
            以下是兩者的trace結果:
            TCP:
            46037 > 12345 [SYN] Seq=801611567 Ack=0 Win=5840 Len=0
            12345 > 46037 [SYN, ACK] Seq=266515894 Ack=801611568 Win=10136 Len=0
            46037 > 12345 [ACK] Seq=801611568 Ack=266515895 Win=5840 Len=0
            12345 > 46037 [PSH, ACK] Seq=266515895 Ack=801611568 Win=10136 Len=1
            46037 > 12345 [ACK] Seq=801611568 Ack=266515896 Win=5840 Len=0
            12345 > 46037 [FIN, PSH, ACK] Seq=266515896 Ack=801611568 Win=10136 Len=1
            46037 > 12345 [RST, ACK] Seq=801611568 Ack=266515898 Win=5840 Len=0
            RMI:
            42749 > rmiregistry [SYN, ECN, CWR]
            Seq=3740552479 Ack=0 Win=32767 Len=0
            rmiregistry > 42749 [SYN, ACK, ECN]
            Seq=3749262223 Ack=3740552480 Win=32767 Len=0
            42749 > rmiregistry [ACK] Seq=3740552480 Ack=3749262224 Win=32767 Len=0
            JRMI, Version: 2, StreamProtocol
            rmiregistry > 42749 [ACK] Seq=3749262224 Ack=3740552487 Win=32767 Len=0
            JRMI, ProtocolAck
            42749 > rmiregistry [ACK] Seq=3740552487 Ack=3749262240 Win=32767 Len=0
            Continuation
            rmiregistry > 42749 [ACK] Seq=3749262240 Ack=3740552506 Win=32767 Len=0
            JRMI, Call
            rmiregistry > 42749 [ACK] Seq=3749262240 Ack=3740552556 Win=32767 Len=0
            JRMI, ReturnData
            42749 > rmiregistry [ACK] Seq=3740552556 Ack=3749262442 Win=32767 Len=0
            JRMI, Ping
            JRMI, PingAck
            42749 > rmiregistry [ACK] Seq=3740552557 Ack=3749262443 Win=32767 Len=0
            JRMI, DgcAck
            42749 > rmiregistry [FIN, ACK]
            Seq=3740552572 Ack=3749262443 Win=32767 Len=0
            rmiregistry > 42749 [FIN, ACK]
            Seq=3749262443 Ack=3740552573 Win=32767 Len=0
            42749 > rmiregistry [ACK] Seq=3740552573 Ack=3749262444 Win=32767 Len=0
                    實驗的結果是:RMI與TCP based socket相比,傳輸相同的有效數據,RMI需要占用更多的網絡帶寬(protocol overhead)。從這里,我們可以得出一個一般性的結論:RMI主要是用于遠程方法的”調用“(RMI是多么的名符其實:)),其技術內涵強調的是“調用”,基于此,我能想到的是:移動計算,和遠程控制,當你的應用不需要在client與server之間傳輸大量的數據時,RMI是較好的選擇,它簡潔、易于開發。但是,一旦你的應用需要在client與server之間傳輸大量的數據,極端的,比如FTP應用,則RMI是不適合的,我們應該使用socket。

            四、參考資料:
            Network-programming with RMI, by Adrian Reber, URL:
            http://42.fht-esslingen.de/~adrian/master/rmi.pdf
            posted @ 2007-07-28 19:06 愛上龍卷風 閱讀(5646) | 評論 (2)編輯 收藏

                 摘要: 在windows平臺下,用于對多線程(包括進程)之間的同步保護機制,基本上有這么幾種:
            1)Critical Section對象 2)Event對象 3)Mutext 對象 4) Semaphore對象。網上已經有很多的文章在介紹這些對象是怎么使用的。本文的著眼點在于:總結出這些同步保護機制的一些明顯的行為特征,而這些行為特征,也是我們再寫程序時經常會碰到的。  閱讀全文
            posted @ 2007-07-22 21:10 愛上龍卷風 閱讀(4913) | 評論 (6)編輯 收藏

            不知道大家是否有同感,在開發軟件應用系統的過程中我們經常面臨一個非常tedious的問題,即如何分類和處理軟件行為產生的錯誤值,大致有這樣幾個方面的問題:
                    第一.錯誤值如何分類。參考microsoft的開放文檔,按照錯誤值的嚴重程度分為critical、error、warning、information等四個級別。
                    第二.錯誤值如何在各個軟件子模塊中有效的表示和溝通。
                    第三.如何劃分出用戶所關心的錯誤值集合。
            這篇文章對錯誤類型定義與處理作了很好的闡釋。推薦給大家,大家如果有什么好的開發經驗,歡迎探討。
            FYI:
            Taxonomy of Error Types and Error Handling
            Posted: Jun 21, 2007 8:13 PM

            Most of what is written about error and exception handling is fairly abstract and vague. When specific recommendations are made, those usually consist of examples given for specific circumstances. Yet, error handling is a fundamental task for developers. This brief review attempts to categorize the different types of error conditions a program can encounter. By describing these categories, I also provide suggestions on how to handle each type of error condition.

            In general, errors a program can encounter tend to be the result of one of three things:

            • Restrictions: Arguments to a routine that can never work, and always result in an error, define a restriction on the use of the routine. Ensuring that only correct arguments are passed to a routine is the type of thing that programming by contract is meant to address.
            • Inconsistencies: When values or resources are not what they are expected to be, or are missing, that creates an inconsistency between the expected state of the environment and the actual state. This may be the internal environment, such as a null pointer, or the external environment, such as a corrupt file. It doesn't encompass inconsistencies in the data model, which often needs to be temporarily inconsistent during an operation (e.g. adding a node to a linked list).
            • Failures When an operation simply does not work, and it's out of the program's control, this is a failure. For example, a pulled network cable.

            These types of errors overlap to an extent (say, a working network could be considered part of the expected state, making it an inconsistency error). In general, though, most errors can fall into one of these categories.

            Sometimes failures are not errors, and are just a way of detecting the current external state. For example, opening a file that doesn't exist may fail, but results in an error only when that file is actually needed.

            Error Handling Responsibilities

            Program code is responsible for the consistency of the internal program state. Generally certain code has primary (ideally, exclusive) responsibility for parts of the internal state. Inconsistency errors that occur within the code responsible for that state are bugs.

            Sometimes the state management responsibility is shared between different sections of code. This is a bad idea, because it makes assigning responsibility for an inconsistency error harder, but it does happen in practice.

            It's important to make a distinction between error detection and debugging. Often, data generated in the process of error handling is mixed together with diagnostic information. If possible, these types of information should be kept completely separate—at least conceptually, even if combined in a single data structure.

            Safe Zones

            Restrictions can be checked before calling a routine, or within a routine. It seems a waste of time to check arguments every time a routine is called when you already know those arguments are correct. One strategy is to separate parameter checking from parameter usage. This doesn't work reliably for library code, where anything can happen between the check and the use of the parameters, but within a base of code for a particular application or within a library, you can restrict the code to not change a value known to be safe.

            The code between a parameter check and the next change to a parameter variable is a safe zone, where parameters don't have to be re-checked. This is only valid for restriction errors, because inconsistency and failure errors can be caused by things outside the code's safe zone. Things like critical sections (in multithreaded environments), semaphores and file locks are meant to create a very limited kind of safe zone for inconsistency and failure errors.

            The code safe zones for parameters can overlap with others, and may not be well defined. One way to deal with this is to assign known safe values to variables which indicate this safety. Joel Spolsky wrote about one way to do this using variable naming conventions in Making Wrong Code Look Wrong. Safe values should be assigned to variables declared constant.

            Reporting Errors

            Code calling a routine needs to know three things to decide how to proceed: First, whether the data is returned, if any, or if the method invocation succeeded; second, whether an error occurred; and, third, whether the error is permanent or transitory. This defines the following possible error states returned from a routine:

            1. Successful
            2. Restriction error (always permanent)
            3. Permanent (bug) inconsistency
            4. Transitory (detected) inconsistency
            5. Failure (transitory for all we know)

            It's often a bad idea to mix an error code with a return value, such as designating a specific values—say, 0 or -1—to be invalid. Some languages, like Python, allow multiple values to be returned from a method as tuples. A tuple is basically an anonymous class, and can be implemented in a language like Java by defining a class for objects returned by a method, or in C by defining a struct which is passed as a parameter and is updated by the function. But in many cases, exceptions are a much better way to separate error information from return values.

            Exceptions transmit an object from the exception location to a handler in a scope surrounding it, or surrounding the point where the routine was called. The exception objects include information by the object type and class, and debugging information the data contained within that type. Exceptions by themselves don't indicate the error state, so that must be included as an attribute of the exception object, or the error state must be deduced from the debugging information (object type and data).

            Java introduced the controversial notion of checked exceptions, which must either be caught or declared to be thrown by a method in order to compile, while unchecked (or runtime) exceptions behave like exceptions in other languages. The main cause of the controversy is that there has been no good definition of why there should be a difference and, as a result, no consistent strategy in the implementation in various libraries, including standard parts of the different Java runtime libraries.

            In general, unchecked exceptions are meant for bugs, where an error indicates that the code is simply wrong and must be fixed (restriction and bug inconsistency errors). An example is a NullPointerException. Checked exceptions are for detected inconsistency and failure errors, where the program may have a strategy of handling the error. An example is an I/O error.

            Transactional Operations

            One strategy to handle errors is to make all operations transactional, so that if they fail, it's as if the operation was never tried. One way implement this is to define an "undo" operation for every change:

            Ideal transactional function

            In this example, the functions are also transactional, and thus don't need to be rolled back if they fail. This can be done with nested if/else blocks, or with nested try/catch blocks. If the "undo" operations themselves have errors, the result looks more like this:

            Realistic transactional function

            One way of dealing with this is to modify a copy of the program state, and if all operations succeed, only then commit the changes. The commit may fail, but this isolates the possible state changing errors to one point, and is similar how databases implement transactions. Another way to implement transactional operations is to make a copy of before any state is changed, and use that copy to restore the expected state, in case of an error.

            In summary, having a clear taxonomy of error conditions that code may encounter helps develop better strategies for dealing with, and possibly recovering from, those errors.

            posted @ 2007-07-15 18:31 愛上龍卷風 閱讀(1137) | 評論 (0)編輯 收藏

            一、異步IO
                    對于應用程序而言,有兩種類型的IO調用:即同步IO與異步IO。其本質的區別是:同步IO會block當前的調用線程,而異步IO則允許發起IO請求的調用線程繼續執行,等到IO請求被處理后,會通知調用線程。在windows平臺上,應用程序可以調用CreateFile API, 并通過設置FILE_FLAG_OVERLAPPED標志來決定是否發起異步IO請求。
                    對于異步的IO請求,其最大的好處是:慢速的IO請求相對于應用程序而言是異步執行,這樣可以極大提高應用程序的處理吞吐量。發起IO請求的應用程序需要關心的是IO執行完成的結果,而不必忙等IO請求執行的過程。
                   事實上,無論對于同步IO,還是異步IO,當IO請求發送到device driver后,device driver的執行總是異步的,當它接到IO請求之后,總會馬上返回給IO System。而IO System是否立即返回給調用線程,則取決于FILE_FLAG_OVERLAPPED標志的設置,如下圖:


            二、異步IO的同步問題。
                    我們使用異步IO,是為了提高應用程序的處理吞吐量。但是,當異步IO不再異步時(無論你是否設置FILE_FLAG_OVERLAPPED標志),應用程序的性能會受到極大的影響。根據Microsoft Knowledge Base 156932, 在下列幾種情況下,異步IO會失去它的異步性,而表現出同步的性質:
            1)如果文件使用了NTFS compression壓縮,則system driver不會異步地存取這樣的文件。
            2)擴展文件長度的IO操作不會是異步操作。
            3)Cache機制。如果IO操作使用了file system cache,則這樣的IO操作會被當成同步IO,而非異步IO。
            即使你使用了FILE_FLAG_OVERLAPPED標志。在這種情況下,
            a.如果需要讀取的數據已經在Cache里,那么I/O drivers會認為這樣的IO請求可以被立即處理,其結果是ReadFile 或者WriteFile調用返回TRUE,表示是:同步處理完成。
            b.如果需要讀取的數據不在Cache里,windows NT的file system是使用page-faulting機制來實現cache管理,而page-faulting總是被同步處理, Windows NT沒有提供異步的page-faulting機制。的確, file system driver使用了線程池來緩解這一問題,但是,當應用程序發起的IO請求足夠多時,線程池還是不能應付的。
                    在我們開發基于異步IO應用程序時,應該避免上述問題的出現,因為它們會使程序的性能大打折扣。
            那么,對于Cache,我們如何避免呢?答案是:請使用FILE_FLAG_NO_BUFFERING標志。這個標志會使異步IO真實地異步執行。
            三、性能的測試數據(僅供參考)。
                  我在我的機器上,簡單地對使用FILE_FLAG_NO_BUFFERING標志的異步IO,與不使用FILE_FLAG_NO_BUFFERING標志的異步IO進行了對比。
            操作:順序讀取1G的文件。
            x軸表示:每次讀取的字節數(單位:K/每次)
            Y軸表示:讀取完成所需要的時間。(單位:millisecond)
            注意:每次測試讀取的內容總數是相等的(1000M)。
            例如:如果每次讀取128k,則需要讀取8000次(128k*8000 = 1000M)。
            如果每次讀取256k,則需要讀取4000次(256k*4000 = 1000M)。
            粉紅色的線沒有使用FILE_FLAG_NO_BUFFERING標志,而黃色的線使用了FILE_FLAG_NO_BUFFERING標志。

            從以上的數據,我們可以得出以下結論:
            1) 當使用FILE_FLAG_NO_BUFFERING標志,應用程序的性能會極大提高,大概有50%的提高。
            2)在使用異步IO的時候,還有一個注意的問題是:當你每次讀取的字節數增大的時候,性能也會提高。尤其在小于1024k時,當增大次讀取的字節數,性能都有明顯的提高。在混合了網絡傳輸等復雜因素的應用程序開發過程中,建議將該值設置為可配置的參數,通過調整該參數,使你的應用達到最好的性能。

            參考資料:

            1) Microsoft Knowledge Base 156932

            2)  Microsoft Windows Internals, Fourth Edition. 

            posted @ 2007-07-01 18:45 愛上龍卷風 閱讀(9038) | 評論 (6)編輯 收藏

                    作為程序員,一直困擾我的一個問題是:一名優秀的程序員,應該是注重面向對象分析能力的培養,還是注重算法分析能力的培養。我相信,這也是一個很多人面臨的問題。我的感覺是:很多system level的程序員更加側重于算法,而application level的程序員,更多的傾向于討論面向對象。大家也可以看到,很多知名IT公司的面試,比如google,比如微軟,很喜歡考察程序員的算法方面的能力。而自從設計模式理論風靡IT界以來,好像這些狀況有些改變,他們開始考察設計模式相關的問題,考察程序員面向對象的分析能力。不可否認的是,設計模式理論,其基于面向對象的理論技術,提供了開發者非常實效,有用的解決問題的模式。依賴于問題的上下文,應用設計模式,開發者可以開發出更加"面向對象"的系統。
               設計一個復雜的系統的本質,就是:將復雜的問題分解成小的,為我們所理解的問題,然后分而治之。人類的智力是有限的,當我們在面對一個復雜問題的時候,總會習慣于首先將他分解,分解到問題足夠的簡單,足夠為我們所理解,解決。事實上,無論是面向對象,還是算法,它們都是分解復雜問題的方法與手段。是采用面向對象的方法去分析,還是使用算法的分析方法,完全是由客觀的主體決定的。非常遺憾的是,這兩類分析方法是互斥的,排他的,你是不可能同時使用這兩種方法的分析解決問題。我們先看一個簡單的例子:
            問題的定義:client和server使用TCP/IP進行一個簡單的交互。
            算法的分解方法如下:


            問題空間被分解成為幾個執行步驟,accept,connet,send,recieve。
            面向對象的分解方法如下:


            問題空間被分解成為幾個對象:c_connector, 主要負責建立TCP連接,在連接成功后,會得到一個c_socket_stream對象,該對象負責主要負責發送和接收網路數據。c_acceptor,負責監聽網絡連接請求,在一個TCP連接成功建立后,返回給調用者一個c_socket_stream。
            兩者的區別在于:兩者分解方法的著重點是不同的,算法的分析方法強調的是事物內部各類事件之間的順序,依賴,耦合關系。算法所關心的是事件本身,例如上例中:它關心的是send,recv這樣發生在事物內部的事件,以及它們之間的調度關系。面向對象的分析方法在于強調的是事物內部各類客觀的主體,以及它們之間的相互協助。
            從這點上可以看到:在分解一個問題的時候,算法偏重于微觀,面向對象側重于宏觀;算法偏重于細節,面向對象側重于整體??梢钥吹?,我們很容易得出這樣的結論:當面對一個復雜的問題的時候,我們的直覺會告訴我們,我們會更加傾向于使用面向對象方法理論來分析問題。這也是幾十年來面向對象的軟件實踐經驗告訴我們的真理。在計算機應用開發領域,面向領域問題本身的復雜性(這包括許多方面:比如你的需求在不斷變化,你的應用方式在不斷變化等等),決定了其更適合使用面向對象的方法來分析問題。面向對象的軟件系統會更加的富有彈性,更加的能適應這種快速的變化。
                 如何做面向對象的設計分析?關鍵在于:
                 1) 對復雜問題的抽象,將復雜的問題抽象成為一組對象,就是我們熟知的objects。object是面向對象軟件系統的行為主體。抽象也意味著我們應該忽略細節的東西,注重整體的東西。
                 2)組織這些objects,使他們形成具有一定結構的整體。比如:通過繼承,使它們成為父子關系,通過組合,使它們具有合作依賴關系。通過組織這些objects,我們更加能清楚地看到這些這些objects公共的行為和屬性。這就形成了面向對象軟件服用的基礎。
                很多人說:算法是程序設計的靈魂,但是我們也不能忘記;面向對象,幫助我們能夠更加容易理解問題復雜性的本質?;蛟S算法與面向對象的最佳的結合點在于: 使用面向對象的方法分解問題,而使用精良的算法解決問題。

            posted @ 2007-06-24 22:31 愛上龍卷風 閱讀(1878) | 評論 (7)編輯 收藏

            一般來說,網絡應用系統服務器的實現,我們從設計模式的角度看,有兩種設計方案可供選擇:
            Reactor服務器,或者Proactor服務器。無論是Reactor,或者Proactor, 都是基于事件驅動的架構設計(Event-driven architecture), 它們的核心是思想是:分離網絡事件的監視,驅動與事物本身的邏輯處理。我們能看到的是:對任何的網絡應用而言,其對網絡事件的監視,處理往往是大同小異的,是可以分離出來作為可復用組件而存在的。
            對所有這些網絡應用,所不用的是對網絡請求的邏輯處理。不同于Proactor, 或者Reactor,一般的設計方法是:
             

            而Proactor, 或者Reactor,設計的精髓是:


            別看這一點小小的變化,這是所謂的Hollywood Principle: 'Don't call us, we'll call you'。這是Framework設計者慣用的設計技巧,依賴倒轉(The Dependency-Inversion Principle):
            a. 高層模塊不應該依賴于低層模塊,應當依賴于抽象。
            b. 抽象不應該依賴于細節,細節依賴于抽象。
            言歸正傳,我們來具體談談Reatctor and Proactor.
            一 、Reactor服務器實現。
            在Reactor的實現可分為兩層:
            a.Demultiplexing/Dispatching層:獨立于應用邏輯層,捕獲,分發網絡請求給具體的網絡請求處理器。
            在這里注意的是,Demultiplexing和Dispatching是兩個不同的概念,Demultiplexing指的是網絡請求的偵聽,及其偵聽以后尋找適合的處理器。Dispatching指的是對處理器的回調。
            b.應用邏輯層組件:主要處理與應用相關的事物處理。
            其基本的結構圖為


            其各個組成模塊的功能為:

            --〉句柄: 調用操作系統API獲得,用于identify事件源,如:網絡連接,打開的文件。
            --〉同步事件分離器:操作系統功能,通過API提供,典型的如: select調用,如windows平臺上的WaitForMultileObj
            --〉事件處理器:這是Demultiplexing/Dispatching層定義的抽象借口,是Demultiplexing/Dispatching層與應用邏輯層之間的協議約定。應用邏輯層依賴于該抽象借口。
            --〉具體事件處理器:事件處理器的具體實現,一般來說,由應用邏輯層組件實現。
            --〉Reactor:提供接口給應用邏輯層,注冊或者反注冊事件處理器,運行應用的事件循環(反復調用同步事件分離器),驅動Demultiplexing/Dispatching層。

            二 、Proactor服務器實現。
            在Proactor的實現也分為兩層:
            a.Demultiplexing/Dispatching層:獨立于應用邏輯層,捕獲,分發網絡請求給具體的網絡請求處理器,不同于Reactor的是,這一層的策略處理
            基于異步操作,從實現的復雜度上,要復雜于Reactor。
            b.應用邏輯層組件:主要處理與應用相關的事物處理。
            Proactor基本的結構圖為


            --〉句柄: 調用操作系統API獲得,用于identify事件源,如:網絡連接,打開的文件。與Reactor不同的是,由于Proactor操作是異步的,所以,與每一個異步IO操作項關聯的,有一個完成事件(Completion Event),當一個異步的IO操作執行完成后,一個操作系統的IO子系統會產生一個完成事件。
            --〉異步操作:一個異步操作可以是一個服務請求的具體實現,如從連接的socket上讀入數據,或者寫入數據到本地磁盤。異步操作的發起執行,并不會由于慢速的IO而阻塞調用線程,調用線程在發起異步操作之后,可以去做別的事情。這是Proactor能增大服務器處理吞吐量的關鍵所在。我們付出的代價是,開發的復雜度要比Reactor。
            --〉完成 事件處理器:Demultiplexing/Dispatching層定義的抽象借口。由應用邏輯層組件繼承來實現這些接口
            函數。
            --〉具體完成事件處理器:事件處理器的具體實現。
            --〉異步操作處理器:由native的操作系統實現。當一個異步操作完成執行時,異步操作處理器會產生一個完成事件,然后把這個完成事件插入到完成事件隊列。
            --〉完成事件隊列:用于保存異步操作完成事件的隊列。在windows平臺上,就是我們所說的完成端口。
            --〉異步事件分離器: 操作系統功能,通過API提供。在windows平臺上,就是GetQueuedCompletionStatus()。異步事件分離器等待在完成事件隊列上,當有完成事件被插入到完成事件隊列,異步事件分離器會從完成事件隊列取出該完成事件,返回給調用者。
            --〉Proactor:運行應用的事件循環(反復調用異步操作處理器),驅動Demultiplexing/Dispatching層。
            --〉發起者:用于發起異步IO操作。
            三 、選擇?是Reactor,還是Proactor。
            在我們實現我們的服務器,是選擇Reactor,還是Proactor,個人認為需要考慮的因素是:
            1)對于Reactor而言,好的地方是開發的復雜度小于Proactor,但缺點同樣明顯:
            第一,基于Reactor實現的服務器可擴展性不如Proactor,這是由其本質決定的。在wiondows平臺上,無論是
            Select,或者是WaitForMultiObj,其等待的最大handle數有一個最大的上限值(默認值是64)。
            第二,基于Reactor實現的服務器服務請求處理的吞吐量不如Proactor好。特別是當對客戶端的請求處理需要更長的時間時,在Reactor的Demultiplexing/Dispatching層,由于Demultiplexing和Dispatching被順序化處理,這樣的話,很容易成為服務器性能的瓶頸。
            2)對于Proactor,如果你需要一個高性能的,高吞吐量的,并發服務器,Proactor絕對是首選的解決方案,它的問題主要在開發的難度比較大。
            不過,總的來說,我們可以看到,基于模式設計的Reactor和Proactor,都很好地把Application-Specific的處理邏輯從網絡,IO事件的Demultiplexing/Dispatching中分離出來,對應用程序而言,由于這樣的松耦合關系,我們的應用完全可以在Reactor和Proactor中自由切換,而不需要修改任何的代碼。、
            四 、后記。
            ACE Framework,對Reactor和Proactor設計模式給出了經典的實現。請參考:
            http://www.cs.wustl.edu/~schmidt/
            個人覺得是很好的學習材料。

            posted @ 2007-06-10 20:19 愛上龍卷風 閱讀(1052) | 評論 (1)編輯 收藏

                 摘要: 1. 什么是Multi-methods. 在闡述這個概念之前,我們先看一下什么是多態(Polymorphisn)。多態是面向對 象程序設計的一個重要的特征, 多態是允許你將父對象的指針(或者引用)設置成為它的子對象的技術,賦值之后,該父對象指針(或者引用)就可以根據當前賦 值給它的子對象的特性以不同的方式運作。簡單的說,就是一句話:允許將子類類型的指針賦值給父類類型的指針。多態性在...  閱讀全文
            posted @ 2007-05-06 22:28 愛上龍卷風 閱讀(2207) | 評論 (4)編輯 收藏

            僅列出標題
            共2頁: 1 2 
            国产精品综合久久第一页| 久久精品一区二区三区中文字幕| 久久久久久久波多野结衣高潮 | 久久91综合国产91久久精品| 波多野结衣AV无码久久一区| 91久久婷婷国产综合精品青草| 99久久国产综合精品五月天喷水| 久久青青草原亚洲av无码| 精品久久久久久国产| 国产高潮国产高潮久久久91| 久久亚洲中文字幕精品一区| 久久国产免费观看精品3| 久久久久一本毛久久久| 久久人妻少妇嫩草AV无码专区| 韩国三级中文字幕hd久久精品| 一本色道久久HEZYO无码| 久久99精品久久久久久水蜜桃| 无码人妻久久一区二区三区免费丨| 亚洲综合精品香蕉久久网97| 国色天香久久久久久久小说| 99热都是精品久久久久久| 久久青青草原亚洲av无码app| 色婷婷久久综合中文久久一本| 国产精品久久免费| 亚洲中文字幕无码久久2017| 欧美久久一级内射wwwwww.| 青青草原综合久久大伊人精品| 亚洲中文字幕无码一久久区| 人妻少妇精品久久| 久久av免费天堂小草播放| 久久―日本道色综合久久| 国产亚洲精品自在久久| 久久婷婷五月综合色高清| 波多野结衣AV无码久久一区| 国产免费久久精品99re丫y| 理论片午午伦夜理片久久| 久久免费视频一区| 中文国产成人精品久久亚洲精品AⅤ无码精品 | 久久精品国产只有精品66| 国产麻豆精品久久一二三| 久久亚洲AV无码精品色午夜|