多線程之線程同步

来源:http://www.cnblogs.com/HopeGi/archive/2017/01/19/6306076.html
-Advertisement-
Play Games

多線程內容大致分兩部分,其一是非同步操作,可通過專用,線程池,Task,Parallel,PLINQ等,而這裡又涉及工作線程與IO線程;其二是線程同步問題,鄙人現在學習與探究的是線程同步問題。 通過學習《CLR via C#》裡面的內容,對線程同步形成了脈絡較清晰的體繫結構,在多線程中實現線程同步的是 ...


多線程內容大致分兩部分,其一是非同步操作,可通過專用,線程池,Task,Parallel,PLINQ等,而這裡又涉及工作線程與IO線程;其二是線程同步問題,鄙人現在學習與探究的是線程同步問題。

通過學習《CLR via C#》裡面的內容,對線程同步形成了脈絡較清晰的體繫結構,在多線程中實現線程同步的是線程同步構造,這個構造分兩大類,一個是基元構造,一個是混合構造。所謂基元則是在代碼中使用最簡單的構造。基原構造又分成兩類,一個是用戶模式,另一個是內核模式。而混合構造則是在內部會使用基元構造的用戶模式和內核模式,使用它的模式會有一定的策略,因為用戶模式和內核模式各有利弊,混合構造則是為了平衡兩者的利與弊而設計出來。下麵則列舉整個線程同步體繫結構

  1. 基元

    1.1 用戶模式

    1.1.1 volatile

    1.1.2 Interlock

    1.2 內核模式

    1.2.1 WaitHandle

    1.2.2 ManualResetEvent與AutoResetEvent

    1.2.3 Semaphore

    1.2.4 Mutex

  2. 混合

    2.1 各種Slim

    2.2 Monitor

    2.3 MethodImplAttribute與SynchronizationAttribute

    2.4 ReaderWriterLock

    2.5 Barier(少用)

    2.6 CoutdownEvent(少用)

   

先從線程同步問題的原因說起,當記憶體中有一個整形的變數A,裡面存放的值是2,當線程1執行的時候它會把A的值從記憶體中取出存放到CPU的寄存器中,並把A賦值為3,此時剛好線程1的時間片結束;接著CPU把時間片分給線程2,線程2同樣把A從記憶體中的值取出來放到記憶體中,但是由於線程1並沒有把變數A的新值3放回記憶體,故線程2讀到的仍然是舊的值(也就是臟數據)2,然後線程2要是需要對A值進行一些判斷之類的就會出現一些非預期的結果了。

而針對上面這種對資源的共用問題處理,往往會使用各種各樣辦法。下麵則逐一介紹

   

先說說基元構造中的用戶模式,凡是用戶模式的優點是它的執行相對較快,因為它是通過一系列CPU指令來協調,它造成的阻塞只是極短時間的阻塞,對操作系統而言這個線程是一直在運行,從未被阻塞。缺點就是唯有系統內核才能停止這樣的一個線程運行。另一方面就是由於線程在自旋而非阻塞,那麼它還會占用這CPU的時間,造成對CPU時間的浪費。

首先是基元用戶模式構造中的volatile構造,這個構造網上很多說法是讓CPU對指定欄位(Field,也就是變數)的讀都是從記憶體讀,每次寫都是往記憶體寫。然而它和編譯器的代碼優化有關係。先看看如下代碼

    public class StrageClass
    {
        vo int mFlag = 0;
        int mValue = 0;
 
        public void Thread1()
        {
            mValue = 5;
            mFlag = 1;
        }
 
        public void Thread2()
        {
            if (mFlag == 1)
                Console.WriteLine(mValue);
        }
    }

 

在懂得多線程同步問題的同學們都會知道如果用兩個線程分別去執行上面兩個方法時,得出的結果有兩個:1.不輸出任何東西;2.輸出5。但是在CSC編譯器編譯成IL語言或JIT編譯成機器語言的過程中,會進行代碼優化,在方法Thread1中,編譯器會覺得給兩個欄位賦值會沒什麼所謂,它只會站在單個線程執行的角度來看,完全不會顧及多線程的問題,因此它有可能會把兩行代碼的執行順序調亂,導致先給mFlag賦值為1,再給mValue賦值為5,這就導致了第三種結果,輸出0。可惜這種結果我一直無法測試出來。

解決這個現象的就是volatile構造,使用了這種構造的效果是,凡是對使用了此構造的欄位進行讀操作時,該操作都保證在原有代碼順序下會在最先執行;或者是凡是對使用了此構造的欄位進行寫操作時,該操作都保證在原有代碼順序下會在最後執行。

實現了volatile的構造現在來說有三個,其一是Thread的兩個靜態方法VolatileRead和VolatileWrite,在MSND上的解析如下

Thread.VolatileRead 讀取欄位值。 無論處理器的數目或處理器緩存的狀態如何,該值都是由電腦的任何處理器寫入的最新值。

Thread.VolatileWrite 立即向欄位寫入一個值,以使該值對電腦中的所有處理器都可見。

在多處理器系統上, VolatileRead 獲得由任何處理器寫入的記憶體位置的最新值。 這可能需要刷新處理器緩存;VolatileWrite 確保寫入記憶體位置的值立即可見的所有處理器。 這可能需要刷新處理器緩存。

即使在單處理器系統上, VolatileRead 和 VolatileWrite 確保值為讀取或寫入記憶體,並不緩存 (例如,在處理器寄存器中)。 因此,您可以使用它們可以由另一個線程,或通過硬體更新的欄位對訪問進行同步。

從上面的文字看不出他和代碼優化有任何關聯,那接著往下看。

volatile關鍵字則是volatile構造的另外一種實現方式,它是VolatileRead和VolatileWrite的簡化版,使用 volatile 修飾符對欄位可以保證對該欄位的所有訪問都使用 VolatileRead 或 VolatileWrite。MSDN中對volatile關鍵字的說明是

volatile 關鍵字指示一個欄位可以由多個同時執行的線程修改。 聲明為 volatile 的欄位不受編譯器優化(假定由單個線程訪問)的限制。 這樣可以確保該欄位在任何時間呈現的都是最新的值。

從這裡可以看出跟代碼優化有關係了。而縱觀上面的介紹得出兩個結論:

1.使用了volatile構造的欄位讀寫都是直接對記憶體操作,不涉及CPU寄存器,使得所有線程對它的讀寫都是同步,不存在臟讀了。讀操作是原子的,寫操作也是原子的。

2.使用了volatile構造修飾(或訪問)欄位,它會嚴格按照代碼編寫的順序執行,讀操作將會在最早執行,寫操作將會最遲執行。

最後一個volatile構造是在.NET Framework中新增的,裡面包含的方法都是Read和Write,它實際上就相當於Thread的VolatileRead 和VolatileWrite 。這需要拿源碼來說明瞭,隨便拿一個Volatile的Read方法來看

而再看看Thraed的VolatileRead方法

   

另一個用戶模式構造是Interlocked,這個構造是保證讀和寫都是在原子操作裡面,這是與上面volatile最大的區別,volatile只能確保單純的讀或者單純的寫。

為何Interlocked是這樣,看一下Interlocaked的方法就知道了

Add(ref int,int)// 調用ExternAdd 外部方法

CompareExchange(ref Int32,Int32,Int32)//1與3是否相等,相等則替換2,返回1的原始值

Decrement(ref Int32)//遞減並返回 調用add

Exchange(ref Int32,Int32)//將2設置到1並返回

Increment(ref Int32)//自增 調用add

就隨便拿其中一個方法Add(ref int,int)來說(Increment和Decrement這兩個方法實際上內部調用了Add方法),它會先讀到第一個參數的值,在與第二個參數求和後,把結果寫到給第一參數中。首先這整個過程是一個原子操作,在這個操作裡面既包含了讀,也包含了寫。至於如何保證這個操作的原子性,估計需要查看Rotor源碼才行。在代碼優化方面來說,它確保了所有寫操作都在Interlocked之前去執行,這保證了Interlocked裡面用到的值是最新的;而任何變數的讀取都在Interlocked之後讀取,這保證了後面用到的值都是最新更改過的。

CompareExchange方法相當重要,雖然Interlocked提供的方法甚少,但基於這個可以擴展出其他更多方法,下麵就是個例子,求出兩個值的最大值,直接抄了Jeffrey的源碼

查看上面代碼,在進入迴圈之前先聲明每次迴圈開始時target的值,在求出最值之後,核對一下target的值是否有變化,如果有變化則需要再記錄新值,按照新值來再求一次最值,直到target不變為止,這就滿足了Interlocked中所說的,寫都在Interlocked之前發生,Interlocked往後就能讀到最新的值。

   

基元內核模式

內核模式則是靠操作系統的內核對象來處理線程的同步問題。先說其弊端,它的速度會相對慢。原因有兩個,其一由於它是由操作系統內核對象來實現的,需要操作系統內部去協調,另外一個原因是內核對象都是一些非托管對象,在瞭解了AppDomain之後就會知道,訪問的對象不在當前AppDomain中的要麼就進行按值封送,要麼就進行按引用封送。經過觀察這部分的非托管資源是按引用封送,這就會存在性能影響。綜合上面兩方面的兩點得出內核模式的弊端。但是他也是有利的方面:1.線程在等待資源的時候不會"自旋"而是阻塞,這個節省了CPU時間,並且這個阻塞可以設定一個超時值。2.可以實現Window線程和CLR線程的同步,也可同步不同進程中的線程(前者未體驗到,而對於後者則知道semaphores中有邊界值資源)。3.可應用安全性設置,為經授權賬戶禁止訪問(這個不知道是咋回事)。

內核模式的所有對象的基類是WaitHandle。內核模式的所有類層次如下

WaitHandle

EventWaitHandle

AutoResetEvent

ManualResetEvent

Semaphore

Mutex

   

WaitHandle繼承MarshalByRefObject,這個就是按引用封送了非托管對象。WaitHandle裡面主要是各種Wait方法,調用了Wait方法在沒有收到信號之前會被阻塞。WaitOne則是等待一個信號,WaitAny(WaitHandle[] waitHandles)則是收到任意一個waitHandles的信號,WaitAll(WaitHandle[] waitHandles)則是等待所有waitHandles的信號。這些方法都有一個版本允許設置一個超時時間。其他的內核模式構造都有類似的Wait方法。

EventWaitHandle的內部維護著一個布爾值,而Wait方法會在這個布爾值為false時線程就會被阻塞,直到該布爾值為true時線程才被釋放。操縱這個布爾值的方法有Set()和Reset(),前者是把布爾值設成true;後者則設成false。這相當於一個開關,調用了Reset之後線程執行到Wait就暫停了,直到Set才恢復。它有兩個子類,使用的方式類似,區別在於AutoResetEvent調用Set之後自動調用Reset,使得開關馬上恢復關閉狀態;而ManualResetEvent就需要手動調用Set讓開關關閉。這樣就達到一個效果一般情況下AutoResetEvent每次釋放的時候能讓一條線程通過;而ManualResetEvent在手動調用Reset之前有可能會讓多條線程通過。

Semaphore的內部是維護著一個整形,當構造一個Semaphore對象時會指定最大的信號量與初始信號量值,每當調用一次WaitOne,信號量就會加1,當加到最大值時,線程就會被阻塞,當調用Release的時候就會釋放一個或多個信號量,此時被阻塞掉的一個或多個線程就會被釋放。這個就符合生產者與消費者問題了,當生產者不斷往產品隊列中加入產品時,他就會WaitOne,當隊列滿了,就相當於信號量滿了,生成者就會被阻塞,當消費者消費掉一個商品時,就會Release釋放掉產品隊列中的一個空間,此時因沒有空間存放產品的生產者又可以開始工作往產品隊列中存放產品了。

Mutex的內部與規則相對前面兩者稍微複雜一點,先說與前面相似的地方就是同樣都會通過WaitOne來阻塞當前線程,通過ReleastMutex來釋放對線程的阻塞。區別在於WaitOne的允許第一個調用的線程通過,其餘後面的線程調用到WaitOne就會被阻塞,通過了WaitOne的線程可以重覆調用WaitOne多次,但是必須調用同樣次數的ReleaseMutex來釋放,否則會因為次數不對等導致別的線程一直處於阻塞的狀態。相比起之前的幾個構造,這個構造會有線程所有權與遞歸這兩個概念,這個是單純靠前面的構造都無法實現的,額外封裝除外。

   

混合構造

上面的基元構造是用了最簡單的實現方式,用戶 模式有用戶模式的快,但是它會帶來CPU時間的浪費;內核模式解決了這個問題,但是會帶來性能上的損失,各有利弊,而混合構造則是集合了兩者的利,它會在內部通過一定策略適當的時機使用用戶模式,再另一種情況下又會使用內核模式。但是這些層層判斷帶來的是記憶體上的開銷。在多線程同步中沒有完美的構造,各個構造都有利弊,存在即有意義,結合具體的應用場景就會有最優的構造可供使用。只是在於我們能否按照具體的場景權衡利弊而已。

各種Slim尾碼的類,在System.Threading命名空間中,可以看到若幹個以Slim尾碼結尾的類:ManualResetEventSlim,SemaphoreSlim,ReaderWriterLockSlim。除了最後一個,其餘兩個都是在基元內核模式中有一樣的構造,但是這三個類都是原有構造的簡化版,尤其是前兩個,使用方式跟原有的一樣,但是儘量避免使用操作系統的內核對象,而達到了輕量級的效果。比如在SemaphoreSlim中使用了內核構造ManualResetEvent,但是這個構造是通過延時初始化,沒達到非不得已時都不使用。至於ReaderWriterLockSlim則在後面再介紹。

Monitor與lock,lock關鍵字可謂是最廣為人知的一種實現多線程同步的手段,那麼下麵則又從一段代碼說起

這個方法相當簡單且無實際意義,它只是為了看編譯器把這段代碼編譯成什麼樣子,通過查看IL如下

留意到IL代碼中出現了try…finally語句塊、Monitor.Enter與Monotor.Exit方法。然後把代碼更改一下再編譯看看IL

IL代碼

代碼比較相似,但並非等價,實際上與lock語句塊等價的代碼如下

那麼既然lock本質上是調用了Monitor,那Monitor是如何通過對一個對象加鎖,然後實現線程同步。原來每個在托管堆裡面的對象都有兩個固定的成員,一個指向該對象類型的指針,另一個是指向一個線程同步塊索引。這個索引指向一個同步塊數組的元素,Monitor對線程加鎖就是靠這個同步塊。按照Jeffrey(CLR via C#的作者)的說法同步塊中有三個欄位,所有權的線程Id,等待線程的數量,遞歸的次數。然而我通過另一批文章瞭解到線程同步塊的成員並非單純這幾個,有興趣的同學可以去閱讀《揭示同步塊索引》的文章,有兩篇。 當Monitor需要為某個對象obj加鎖時,它會檢查obj的同步塊索引有否為數組的某個索引,如果是-1的,則從數組中找出一個空閑的同步塊與之關聯,同時同步塊的所有權線程Id就記錄下當前線程的Id;當再次有線程調用Monitor的時候就會檢查同步塊的所有權Id和當前線程Id是否對應上,能對應上的就讓其通過,在遞歸次數上加1,如果對應不上的就把該線程扔到一個就緒隊列(這個隊列實際上也是存在同步塊裡面)中,並將其阻塞;這個同步塊會在調用Exit的時候檢查遞歸次數確保遞歸完了就清除所有權線程Id。通過等待線程數量得知是否有線程在等待,如果有則從等待隊列中取出線程並釋放,否則就解除與同步塊的關聯,讓同步塊等待被下個被加鎖的對象使用。

Monitor中還有一對方法Wait與Pulse。前者可以使得獲得到鎖的線程短暫地將鎖釋放,而當前線程就會被阻塞而放入等待隊列中。直到其他線程調用了Pulse方法,才會從等待隊列中把線程放到就緒隊列中,等待下次鎖被釋放時,才有機會被再次獲取鎖,具體能否獲取就要看等待隊列中的情況了。

ReaderWriterLock讀寫鎖,傳統的lock關鍵字(即等價於Monitor的Enter和Exit),他對共用資源的鎖是全互斥鎖,一經加鎖的資源其他資源完全不能訪問。

ReaderWriterLock對互斥資源的加的鎖分讀鎖與寫鎖,類似於資料庫中提到的共用鎖和排他鎖。大致情況是加了讀鎖的資源允許多個線程對其訪問,而加了寫鎖的資源只有一個線程可以對其訪問。兩種加了不同縮的線程都不能同時訪問資源,而嚴格來說,加了讀鎖的線程只要在同一個隊列中的都能訪問資源,而不同隊列的則不能訪問;加了寫鎖的資源只能在一個隊列中,而寫鎖隊列中只有一個線程能訪問資源。區分讀鎖的線程是否在於統一個隊列中的判斷標準是,本次加讀鎖的線程與上次加讀鎖的線程這個時間段中,有否別的線程加了寫鎖,沒沒別的線程加寫鎖,則這兩個線程都在同一個讀鎖隊列中。

ReaderWriterLockSlimReaderWriterLock類似,是後者的升級版,出現在.NET Framework3.5,據說是優化了遞歸和簡化了操作。在此遞歸策略我尚未深究過。目前大概列舉一下它們通常用的方法

ReaderWriterLock常用的方法

Acqurie或Release ReaderLock或WriteLock 的排列組合

UpGradeToWriteLock/DownGradeFromWriteLock 用於在讀鎖中升級到寫鎖。當然在這個升級的過程中也涉及到線程從讀鎖隊列切換到寫鎖隊列中,因此需要等待。

ReleaseLock/RestoreLock 釋放所有鎖和恢復鎖狀態

   

ReaderWriterLock實現IDispose介面,其方法則是以下模式

TryEnter/Enter/Exit ReadLock/WriteLock/UpGradeableReadLock

(以上內容引用自另一篇筆記《ReaderWriterLock)

CoutdownEvent比較少用的混合構造,這個跟Semaphore相反,體現在Semaphore是在內部計數(也就是信號量)達到最大值的時候讓線程阻塞,而CountdownEvent是在內部計數達到0的時候才讓線程阻塞。其方法有

AddCount //計數遞增;

Signal //計數遞減;

Reset //計數重設為指定或初始;

Wait //當且僅當計數為0才不阻塞,否則就阻塞。

Barrier也是一個比較少用的混合構造,用於處理多線程在分步驟的操作中協作問題。它內部維護著一個計數,該計數代表這次協作的參與者數量,當不同的線程調用SignalAndWait的時候會給這個計數加1並且把調用的線程阻塞,直到計數達到最大值的時候,才會釋放所有被阻塞的線程。假設還是不明白的話就看一下MSND上面的示例代碼

這裡給Barrier初始化的參與者數量是3,同時每完成一個步驟的時候會調用委托,該方法是輸出count的值步驟索引。參與者數量後來增加了兩個又減少了一個。每個參與者的操作都是相同,給count進行原子自增,自增完則調用SgnalAndWait告知Barrier當前步驟已完成並等待下一個步驟的開始。但是第三次由於回調方法里拋出了一個異常,每個參與者在調用SignalAndWait的時候都會拋出一個異常。通過Parallel開始了一個並行操作。假設並行開的作業數跟Barrier參與者數量不一樣就會導致在SignalAndWait會有非預期的情況出現。

接下來說兩個Attribute,這個估計不算是同步構造,但是也能線上程同步中發揮作用

MethodImplAttribute這個Attribute適用於方法的,當給定的參數是MethodImplOptions.Synchronized,它會對整個方法的方法體進行加鎖,凡是調用這個方法的線程在沒有獲得鎖的時候就會被阻塞,直到擁有鎖的線程釋放了才將其喚醒。對靜態方法而言它就相當於把該類的類型對象給鎖了,即lock(typeof(ClassType));對於實例方法他就相當於把該對象的實例給鎖了,即lock(this)。最開始對它內部調用了lock這個結論存在猜疑,於是用IL編譯了一下,發現方法體的代碼沒啥異樣,查看了一些源碼也好無頭緒,後來發現它的IL方法頭跟普通的方法有區別,多了一個synchronized

於是網上找各種資料,最後發現"junchu25"的博客[1][2]里提到用WinDbg來查看JIT生成的代碼。

調用Attribute的

調用lock的

對於用這個Attribute實現的線程同步連Jeffrey都不推薦使用。

System.Runtime.Remoting.Contexts.SynchronizationAttribute這個Attribute適用於類,在類的定義中加了這個Attribute並繼承與ContextBoundOject的類,它會對類中的所有方法都加上同一個鎖,對比MethodImplAttribute它的範圍更廣,當一個線程調用此類的任何方法時,如果沒有獲得鎖,那麼該線程就會被阻塞。有個說法是它本質上調用了lock,對於這個說法的求證就更不容易,國內的資源少之又少,裡面又涉及到AppDomain,線程上下文,最後核心的就是由SynchronizedServerContextSink這個類去實現的。AppDomain應該要另立篇進行介紹。但是在這裡也要稍微說一下,以前以為記憶體中就是有線程棧與堆記憶體,而這隻是很基本的劃分,堆記憶體還會劃分成若幹個AppDomain,在每個AppDomain中也至少有一個上下文,每個對象都會從屬與一個AppDomain裡面的一個上下文中。跨AppDomain的對象是不能直接訪問的,要麼進行按值封送(相當於深複製一個對象到調用的AppDomain),要麼就按引用封送。對於按引用封送則需要該類繼承MarshalByRefObject。對繼承了這個類的對象進行調用時都不是調用類的本身,而是通過代理的形式進行調用。那麼跨上下文的也需要進行按值封送操作。平常構造的一個對象都是在進程預設AppDomain下的預設上下文中,而使用了SynchronizationAttribute特性的類它的實例是屬於另外的一個上下文中,繼承了ContextBoundObject基類的類進行跨上下文訪問對象時也是通過按引用封送的方式用代理訪問對象,並非訪問到對象本身。至於是否跨上下文訪問對象可以通過的RemotingServices.IsObjectOutOfContext(obj)方法進行判斷。SynchronizedServerContextSink是mscorlib的一個內部類。當線程調用跨上下文的對象時,這個調用會被SynchronizedServerContextSink封裝成WorkItem的對象,該對象也mscorlib的中的一個內部類,SynchronizedServerContextSink就請求SynchronizationAttribute,Attribute根據現在是否有多個WorkItem的執行請求來決定當前處理的這個WorkItem會馬上執行還是放到一個先進先出的WorkItem隊列中按順序執行,這個隊列是SynchronizationAttribute的一個成員,隊列成員入隊出隊時或者Attribute判斷是否馬上執行WorkItem時都需要獲取一個lock的鎖,被鎖的對象也正是這個WorkItem的隊列。這裡面涉及到幾個類的交互,鄙人現在還沒完全看清,以上這個處理過程可能有錯,待分析清楚再進行補充。不過通過這個Attribute實現的線程同步按逼人的直覺也是不推薦使用的,主要是性能方面的損耗,鎖的範圍也比較大。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 比較nor/nand flash NOR NAND介面: RAM-Like,引腳多 引腳少,復用容量: 小 1M 2M 3M 大:128M 256M G讀: 簡單 複雜寫: 發出特定命令 慢 發出特定命令 快價格: 貴 便宜特點: 無位反轉、壞塊 位反轉、壞塊 關鍵重要的程式 大數據、容忍可以出 ...
  • 1 nand flash的操作 目的:讀地址A的數據,把數據B寫到地址A。 問1. 原理圖上NAND FLASH和S3C2440之間只有數據線,怎麼傳輸地址?答1.在DATA0~DATA7上既傳輸數據,又傳輸地址,當ALE為高電平時傳輸的是地址。 問2. 從NAND FLASH晶元手冊可知,要操作N ...
  • 1.桌面右擊新建txt文件複製下麵兩行代碼,修改文件尾碼名為bat保存文件 netsh wlan set hostednetwork mode=allow ssid=zhangxh key=xiaoheng123netsh wlan start hostednetwork 2.右擊bat文件以管理員 ...
  • 1 塊設備的概述 linux支持的兩種重要的設備類型分別是字元設備和塊設備,塊設備可以隨機地以固定大小的塊傳送數據。與字元設備相比,塊設備有以下幾個特殊之處: (1)塊設備可以從數據的任何位置進行訪問 (2)塊數據總是以固定長度進行傳輸,即便請求的這是一個位元組 (3)對塊設備的訪問有大量的緩存。當進 ...
  • 去埠號功能主要用於Apache與IIS等WEB伺服器共存時,去除功能變數名稱後面所帶的埠 本文案例採用我開發的純綠色PHP集成環境PHPWAMP裡面的“去埠”功能模塊。 案例演示: 點擊常用工具,打開“去掉功能變數名稱非80埠”功能即可 彈出的界面菜單如下圖 如下填寫,功能變數名稱填寫格式abc.com,具體如下圖 ...
  • 最近有學生向我咨詢如何同時建立多個不同PHP版本站點,並自定義任意版本,軟體是否可以多開,PHPWAMP如何設置才能與其他的環境同時使用等問題,本文將一一解決。 簡單介紹一下PHPWAMP 你們應該會經常聽到WAMP這詞吧,那麼WAMP是什麼意思? Windows下的Apache+Mysql+PHP ...
  • phpwamp在伺服器搭建網站,php網站在伺服器上的搭建方式,雲伺服器上如何使用PHP綠色集成環境 ...
  • 1.泛型的約束: (1)介面約束; (2)基類約束,基類約束必須放在第一(假如有多個約束); (3)struct/class約束; (4)多個參數類型的約束,每個類型參數都要用where關鍵字; (5)構造器約束,只能是無參構造器,如new(); (6)約束可以由派生類繼承,但必須在派生類中顯式地指 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...