理解Windows窗體和WPF中的跨線程調用

你曾開發過Windows窗體程序,可能會注意到有時事件處理程序將拋出InvalidOperationException異常,信息為“ 線程調用非法:在非創建控件的線程上訪問該控件”。這種Windows窗體應用程序中 線程調用時的一個最為奇怪的行為就是,有些時候它沒什么問題,可有些時候卻會出現問題。在 WPF(Windows Presentation Foundation)中,這個行為有所改變。 WPF線程調用將永遠不會成功。不管怎樣,至少這能讓你在開發過程中更容易地找到問題的所在。

在Windows窗體中,解決方法是首先檢查Control.InvokeRequired屬性,若Control. InvokeRequired屬性為true,那么調用ControlInvoke()。在WPF中,可以使用System.Windows.Threading.Dispatcher中的Invoke()和BeginInvoke()方法。這兩種情況中都發生了很多事情,你也同樣有別的選擇。這兩個API為你做了很多事情,不過在某些情況下仍有可能會失敗。因為這些方法將用來處理線程調用,因此若是沒有正確使用(甚至是正確使用但沒有完全理解其行為)的話,也有可能會導致競爭條件的出現。

無論是Windows窗體還是WPF,問題的成因都很簡單:Windows控件使用的是組件對象模型(Component Object Model,COM)單線程單元(Single-threaded Apartment,STA)模型,因為其底層的控件是單元線程(apartment-threaded)的。此外,很多控件都用消息泵(message pump)來完成操作。因此,這種模型就需要所有調用該控件的方法都和創建該控件的方法位于同一個線程上。Invoke、BeginInvoke和EndInvoke調度方法都需要在正確的線程上調用。兩種模型的底層代碼非常相似,因此這里將以Windows窗體的API為例。不過當調用方法有所區別時,我將同時給出兩個版本。其具體的做法非常復雜,但仍需要深入了解。

首先,我們來看一段簡單的泛型代碼,能夠讓你在遇到此種情況時得到一定的簡化。匿名委托讓僅在一處使用的小方法更加易于編寫。不過,匿名委托卻并不能與接受System.Delegate類型的方法(例如Control.Invoke)配合使用。因此,你需要首先定義一個非抽象的委托類型,隨后在使用Control. Invoke時傳入。

private void OnTick(object sender, EventArgs e)

{

??? Action action = () =>

??????? toolStripStatusLabel1.Text =

??????????? DateTime.Now.ToLongTimeString();

??? if (this.InvokeRequired)

??????? this.Invoke(action);

??? else

??????? action();

}

C# 3.0大大簡化了上述代碼。System.Core.Action委托定義了一類專門的委托類型,用來表示不接受任何參數并返回void的方法。lambda表達式也能夠更加簡單地定義方法體。但若你仍舊需要支持C# 2.0,那么需要編寫如下的代碼。

delegate void Invoker();

private void OnTick20(object sender, EventArgs e)

{

??? Action action = delegate()

??? {

??????? toolStripStatusLabel1.Text =

??????????? DateTime.Now.ToLongTimeString();

??? };

??? if (this.InvokeRequired)

??????? this.Invoke(action);

??? else

??????? action();

}

WPF中,則需要使用控件上的System.Threading.Dispatcher對象來執行封送操作。

private void UpdateTime()

{

??? Action action = () => textBlock1.Text =

??????? DateTime.Now.ToString();

??? if (System.Threading.Thread.CurrentThread !=

??????? textBlock1.Dispatcher.Thread)

??? {

??????? textBlock1.Dispatcher.Invoke

??????????? (System.Windows.Threading.DispatcherPriority.Normal,

??????????? action);

??? }

??? else

??? {

??????? action();

??? }

}

這種做法讓事件處理程序的實際邏輯變得更加模糊,讓代碼難以閱讀和維護。這種做法還需要引入一個委托定義,僅僅用來滿足方法的簽名。

使用一小段泛型代碼即可改善這種情況。下面的這個ControlExtensions靜態類所包含的泛型方法適用于調用不超過兩個參數的委托。再添加一些重載即可支持更多的參數。此外,其中的方法還可使用委托定義來調用目標方法,既可以直接調用,也可以通過Control.Invoke的封送。

public static class ControlExtensions

{

??? public static void InvokeIfNeeded(this Control ctl,

??????? Action doit)

? ??{

??????? if (ctl.InvokeRequired)

??????????? ctl.Invoke(doit);

??????? else

??????????? doit();

??? }

??? public static void InvokeIfNeeded<T>(this Control ctl,

??????? Action<T> doit, T args)

??? {

??????? if (ctl.InvokeRequired)

??????????? ctl.Invoke(doit, args);

??????? else

??????????? doit(args);

??? }

}

在多線程環境中使用InvokeIfNeeded能夠很大程度上簡化事件處理程序的代碼。

private void OnTick(object sender, EventArgs e)

{

??? this.InvokeIfNeeded(() => toolStripStatusLabel1.Text =

??????? DateTime.Now.ToLongTimeString());

}

對于WPF控件,也可以創建出一系列類似的擴展。

public static class WPFControlExtensions

{

??? public static void InvokeIfNeeded(

??????? this System.Windows.Threading.DispatcherObject ctl,

??????? Action doit,

??????? System.Windows.Threading.DispatcherPriority priority)

??? {

??????? if (System.Threading.Thread.CurrentThread !=

??????????? ctl.Dispatcher.Thread)

??????? {

??????????? ctl.Dispatcher.Invoke(priority,

??????????????? doit);

??????? }

??????? else

??????? {

??????????? doit();

??????? }

??? }

??? public static void InvokeIfNeeded<T>(

??????? this System.Windows.Threading.DispatcherObject ctl,

??????? Action<T> doit,

??????? T args,

??????? System.Windows.Threading.DispatcherPriority priority)

??? {

??????? if (System.Threading.Thread.CurrentThread !=

??????????? ctl.Dispatcher.Thread)

??????? {

??????????? ctl.Dispatcher.Invoke(priority,

??????????????? doit, args);

??????? }

??????? else

??????? {

??????????? doit(args);

??????? }

??? }

}

WPF版本沒有檢查InvokeRequired,而是檢查了當前線程的標識,并于將要進行控件交互的線程進行比較。DispatcherObject是很多WPF控件的基類,用來為WPF控件處理線程之間的分發操作。注意,在WPF中還可以指定事件處理程序的優先級。這是因為WPF應用程序使用了兩個UI線程。一個線程用來專門處理UI呈現,以便讓UI總是能夠及時呈現出動畫等效果。你可以通過指定優先級來告訴框架哪類操作對于用戶更加重要:要么是UI呈現,要么是處理某些特定的后臺事件。

這段代碼有幾個優勢。雖然使用了匿名委托定義,不過事件處理程序的核心仍位于事件處理程序中。與直接使用Control.IsInvokeRequired或ControlInvoke相比,這種做法更加易讀且易于維護。在ControlExtensions中,使用了泛型方法來檢查InvokeRequired或是比較兩個線程,這也就讓使用者從中解脫了起來。若是代碼僅在單線程應用程序中使用,那么我也不會使用這些方法。不過若是程序最終可能在多線程環境中運行,那么不如使用上面這種更加完善的處理方式。

若想支持C# 2.0,那么還要做一些額外的工作。主要在于無法使用擴展方法和lambda表達式語法。這樣,代碼將變得有些臃腫。

// 定義必要的Action:

public delegate void Action;

public delegate void Action<T>(T arg);

// 3個和4個參數的Action定義省略

public static class ControlExtensions

{

??? public static void InvokeIfNeeded(Control ctl, Action doit)

??? {

??????? if (ctl.InvokeRequired)

??????????? ctl.Invoke(doit);

??????? else

?????????? ?doit();

??? }

??? public static void InvokeIfNeeded<T>( Control ctl,

??????? Action<T> doit, T args)

??? {

??????? if (ctl.InvokeRequired)

??????????? ctl.Invoke(doit, args);

??????? else

??????????? doit(args);

??? }

}

// 其他位置:

private void OnTick20(object sender, EventArgs e)

{

??? ControlExtensions.InvokeIfNeeded(this, delegate()

??? {

??????? toolStripStatusLabel1.Text =

????????? DateTime.Now.ToLongTimeString();

??? });

}

在將這個方法應用到事件處理程序之前,我們來仔細看看InvokeRequired和Control.Invoke所做的工作。這兩個方法并非沒有什么代價,也不建議將這種模式應用到各處。Control.InvokeRequired用來判斷當前代碼是運行于創建該控件的線程之上,還是運行于另一個線程之上。若是運行于另一個線程之上,那么則需要使用封送。大多數情況下,這個屬性的實現還算簡單:只要檢查當前線程的ID,并與創建該控件的線程ID進行比較即可。若二者匹配,那么則無需Invoke,否則就需要Invoke。這個比較并不需要花費太多時間,WPF版本的這類擴展方法也是執行了同樣的檢查。

不過其中還有一些邊緣情況。若需要判斷的控件還沒有被創建,在父控件已創建好,正在創建子控件時就可能發生這個情況。那么此時,雖然C#對象已經存在,不過其底層的窗口句柄仍舊為null。此時也就無法進行比較,因此框架本身將花費一定代價來處理這種情況。框架將沿著控件樹向上尋找,看看是否有上層控件已被創建。若是框架能夠找到一個創建好了的窗體,那么該窗體將作為封送窗體。這是一個非常合理的假設,因為父控件將要負責創建子控件。這種做法可以保證子控件將會與父控件在同一個線程上創建。找到合適的父控件之后,框架即可執行同樣的檢查,比較當前線程的ID和創建該父控件的線程的ID。

不過,若是框架無法找到任何一個已創建的父窗體,那么則需要找到一些其他類型的窗體。若在層次體系中無法找到可用的窗體,那么框架將開始尋找暫存窗體(parking window),暫存窗體讓你不會被某些Win32 API奇怪的行為所干擾。簡而言之,有些對窗體的修改(例如修改某些樣式)需要銷毀并重新創建該窗體。暫存窗體就是用來在父窗體被銷毀并重新創建的過程中用來臨時保存其中的控件的。在這段時間內,UI線程僅運行于暫存窗體中。

WPF中,得益于Dispatcher類的使用,上述很多過程都得到了簡化。每個線程都有一個Dispatcher。在第一次訪問某個控件的Dispatcher時,類庫將察看該線程是否已經擁有了Dispatcher。若已經存在,那么直接返回。如果沒有的話,那么將創建一個新的Dispatcher對象,并關聯在控件及其所在的線程之上。

不過這其中仍舊有可能存在著漏洞和發生失敗。有可能所有的窗體,包括暫存窗體都沒有被創建。在這種情況下,InvokeRequired將返回false,表示無需將調用封送到另一個線程上。這種情況可能會比較危險,因為這個假設可能是錯誤的,但框架也僅能做到如此了。任何需要訪問窗體句柄的方法都無法成功執行,因為現在還沒有任何窗體。此外,封送也自然會失敗。若是框架無法找到任何可以封送的控件,自然也無法將當前調用封送到UI線程上。于是框架選擇了一個可能在稍后出現的失敗,而不是當前會立即出現的失敗。幸運的是,這種情況在實際中非常少見。不過在WPF中,Dispatcher還是包含了額外的代碼來預防這種情況。

總結一下InvokeRequired的相關內容。一旦控件創建完成,那么InvokeRequired的效率將會不錯,且也能保證安全。不過若是目標控件尚未被創建,那么InvokeRequired則可能會耗費比較長的時間。而若是沒有創建好任何控件,那么InvokeRequired則可能要相當長的時間,同時其結論也無法保證正確。但雖然Control.InvokeRequired有可能耗時較長,也比非必要地調用Control.Invoke要高效得多。且在WPF中,很多邊緣情況都得到了優化,性能要比Windows窗體的實現提高不少。

接下來看看Control.Invoke的執行過程。(Control.Invoke的執行非常復雜,因此這里將僅做簡要介紹。)首先,有一個特殊情況是雖然調用了Invoke方法,不過當前線程卻和控件的創建線程一樣。這是個最為簡單的特例,框架將直接調用委托。即當InvokeRequired返回false時仍舊調用Control.Invoke()將會有微小的損耗,不過仍舊是安全的。

在真正需要調用Invoke時會發生一些有趣的情況。Control.Invoke能夠通過將消息發送至目標控件的消息隊列來實現線程調用。Control.Invoke還創建了一個專門的結構,其中包含了調用委托所需要的所有信息,包括所有的參數、調用棧以及委托的目標等。參數均會被預先復制出來,以避免在調用目標委托之前被修改(記住這是在多線程的世界中)。

在創建好這個結構并添加到隊列中之后,Control.Invoke將向目標對象發送一條消息。Control.Invoke隨后將在等待UI線程處理消息并調用委托時組合使用旋轉等待(spin wait)和休眠。這部分的處理包含了一個重要的時間問題。當目標控件開始處理Invoke消息時,它并不會僅僅執行一個委托,而是處理掉隊列中所有的委托。若你使用的是Control.Invoke的同步版本,那么不會看到任何效果。不過若是混合使用了Control.Invoke和Control.BeginInvoke,那么行為將有所不同。這部分內容將在稍后繼續介紹,目前需要了解的是,控件的WndProc將在開始處理消息時處理掉每一個等待中的Invoke消息。對于WPF,可控制的要多一些,因為可以指定異步操作的優先級。你可以讓Dispatcher將消息放在隊列中時給出三種優先級:(1)基于系統或應用程序的當前狀況;(2)使用普通優先級;(3)高優先級。

當然,這些委托中可能會拋出異常,且異常無法線程傳遞。因此框架將把對委托的調用用try/catch包圍起來并捕獲所有的異常。隨后在UI線程完成處理之后,其中發生的異常將被復制到專門的數據結構中,供原線程分析。

在UI線程處理結束之后,Control.Invoke將察看UI線程中拋出的所有異常。如果確有異常發生,那么將在后臺線程中重新拋出。若沒有異常,那么將繼續進行普通的處理。可以看到,調用一個方法的過程并不簡單。

Control.Invoke將在執行封送調用時阻塞后臺線程,雖然實際上在多線程環境中運行,不過仍舊讓人覺得是同步的行為。

不過這可能不是你所期待的。很多時候,你希望讓工作線程觸發一個事件之后繼續進行下面的操作,而不是同步地等待UI。這時則應該使用BeginInvoke。該方法的功能和Control.Invoke基本相同,不過在向目標控件發送消息之后,BeginInvoke將立即返回,而不是等待目標委托完成。BeginInvoke支持發送消息(可能在稍后才會處理)后立即返回到調用線程上。你可以根據需要為ControlExtensions類添加相應的異步方法,以便簡化異步線程UI調用的操作。雖然與前面的那些方法相比,這些方法帶來的優勢不那么明顯,不過為了保持一致,我們還是在ControlExtensions中給出。

public static void QueueInvoke(this Control ctl, Action doit)

{

??? ctl.BeginInvoke(doit);

}

public static void QueueInvoke<T>(this Control ctl,

??? Action<T> doit, T args)

{

??? ctl.BeginInvoke(doit, args);

}

QueueInvoke并沒有在一開始檢查InvokeRequired。這是因為即使當前已經運行于UI線程之上,你仍可能想要異步地調用方法。BeginInvoke()就實現了這個功能。Control.BeginInvoke將消息發送至目標控件,然后返回。隨后目標控件將在其下一次檢查消息隊列時處理該消息。若是在UI線程中調用的BeginInvoke,那么實際上這并不是異步的:當前操作后就會立即執行該調用。

這里我忽略了BeginInvoke所返回的Asynch結果對象。實際上,UI更新很少帶有返回值。這會大大簡化異步處理消息的過程。只需簡單地調用BeginInvoke,然后等待委托在稍后的某個時候執行即可。但編寫委托方法時需要格外小心,因為所有的異常都會在線程封送中被默認捕獲。

在結束這個條目之前,我再來簡單介紹一下控件的WndProc。當WndProc接收到了Invoke消息之后,將執行InvokeQueue中的每一個委托。若是希望按照特定的順序處理事件,且你還混合使用了Invoke和BeginInvoke,那么可能會在時間上出現問題。可以保證的是,使用Control. BeginInvoke或Control.Invoke調用的委托將按照其發出的順序執行。BeginInvoke僅僅會在隊列中添加一個委托。不過稍后的任意一個Control.Invoke調用均會讓控件開始處理隊列中所有的消息,包括先前由BeginInvoke添加的委托。“稍后的某一時間”處理委托意味著你無法控制“稍后的某一事件”到底是何時。“現在”處理委托則意味著應用程序先執行所有等待的異步委托,然后處理當前的這一個。很有可能的是,某個由BeginInvoke發出的異步委托將在Invoke委托調用之前改變了程序的狀態。因此需要小心地編寫代碼,確保在委托中重新檢查程序的狀態,而不是依賴于調用Control.Invoke時傳入的狀態。

簡單舉例,如下版本的事件處理程序很難顯示出那段額外的文字。

private void OnTick(object sender, EventArgs e)

{

??? this.InvokeAsynch(() => toolStripStatusLabel1.Text =

??????? DateTime.Now.ToLongTimeString());

??? toolStripStatusLabel1.Text += "? And set more stuff";

}

這是因為第一個修改會被暫存于隊列中,隨后在開始處理接下來的消息時才會修改文字。而此時,第二條語句已經給標簽添加了額外的文字。

Invoke和InvokeRequired為你默默地做了很多的工作。這些工作都是必需的,因為Windows窗體控件構建于STA模型之上。這個行為在最新的WPF中依舊存在。在所有最新的.NET Framework代碼之下,原有的Win32 API并沒有什么變化。因此這類消息傳遞以及線程封送仍舊可能導致意料之外的行為。你必須對這些方法的工作原理及其行為有著充分的理解。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/388732.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/388732.shtml
英文地址,請注明出處:http://en.pswp.cn/news/388732.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

什么是嵌入式系統

在我們的日常生活中&#xff0c;我們經常使用許多使用嵌入式系統技術設計的電氣和電子電路和套件。計算機&#xff0c;手機&#xff0c;平板&#xff0c;筆記本電腦&#xff0c;數字電子系統以及其他電子和電子設備都是使用嵌入式系統設計的。 什么是嵌入式系統&#xff1f;將硬…

面向數據科學家的實用統計學_數據科學家必知的統計數據

面向數據科學家的實用統計學Beginners usually ignore most foundational statistical knowledge. To understand different models, and various techniques better, these concepts are essential. These work as baseline knowledge for various concepts involved in data …

字符串、指針、引用、數組基礎

1.字符串&#xff1a;字符是由單引號所括住的單個字母、數字或符號。若將單引號改為雙引號&#xff0c;該字符就會變成字符串。它們之間主要的差別是&#xff1a;雙引號的字符串“A”會比單引號的字符串’A’在字符串的最后補上一個結束符’\0’&#xff08;Null字符&#xff0…

suse安裝php,SUSE下安裝LAMP

安裝Apache可以看到編譯安裝Apache出錯&#xff0c;rpm包安裝gcc (首先要安裝GCC)makemake install修改apache端口cd /home/sxit/apache2vi conf/httpd.confListen 8000啟動 apache/home/root/apache2/bin/apachectl start(stop restart)http://localhost:8000安裝一下PHP開發…

自己動手寫事件總線(EventBus)

2019獨角獸企業重金招聘Python工程師標準>>> 本文由云社區發表 事件總線核心邏輯的實現。 <!--more--> EventBus的作用 Android中存在各種通信場景&#xff0c;如Activity之間的跳轉&#xff0c;Activity與Fragment以及其他組件之間的交互&#xff0c;以及在某…

viz::viz3d報錯_我可以在Excel中獲得該Viz嗎?

viz::viz3d報錯Have you ever found yourself in the following situation?您是否遇到以下情況&#xff1f; Your team has been preparing and working tireless hours to create and showcase the end product — an interactive visual dashboard. It’s a culmination of…

php 數組合并字符,PHP將字符串或數組合并到一個數組內方法

本文主要和大家分享PHP將字符串或數組合并到一個數組內方法&#xff0c;有兩種方法&#xff0c;希望希望能幫助到大家。一般寫法&#xff1a;<?php /*** add a string or an array to another array** param array|string $val* param array $array*/function add_val_to_a…

xcode 4 最低的要求是 10.6.6的版本,如果你是 10.6.3的版本,又不想升級的話。可以考慮通過修改版本號的方法進行安裝

xcode 4 最低的要求是 10.6.6的版本&#xff0c;如果你是 10.6.3的版本&#xff0c;又不想升級的話。可以考慮通過修改版本號的方法進行安裝。 一、打開控制臺&#xff1b; 二、使用root用戶&#xff1b; 命令&#xff1a;sudo -s 之后輸入密碼即可 三、編輯 /System/Library/C…

android 調試技巧

1.查看當前堆棧 Call tree new Exception(“print trace”).printStackTrace(); &#xff08;在logcat中打印當前函數調用關系&#xff09; 2.MethodTracing 性能分析與優&#xff08; 函數占用CPU時間&#xff0c; 調用次數&#xff0c; 函數調用關系&#xff09; a) 在程序…

Xml序列化

xml序列化 實現思路 通過程序生成一個xml文件來備份手機短信. 先獲取手機短信的內容 —>通過xml備份.StringBuffer 代碼如下public void click(View view) {StringBuffer sb new StringBuffer();sb.append("<?xml version\"1.0\" encoding\"UTF-8\…

java 添加用戶 數據庫,跟屌絲學DB2 第二課 建立數據庫以及添加用戶

在安裝DB2 之后&#xff0c;就可以在 DB2 環境中創建自己的數據庫。首先考慮數據庫應該使用哪個實例。實例(instance) 提供一個由數據庫管理配置(DBM CFG)文件控制的邏輯層&#xff0c;可以在這里將多個數據庫分組在一起。DBM CFG 文件包含一組 DBM CFG 參數&#xff0c;可以使…

iphone視頻教程

公開課介紹 本課程共28集 翻譯至第15集 網易正在翻譯16-28集 敬請關注 返回公開課首頁 一鍵分享&#xff1a;  網易微博開心網豆瓣網新浪微博搜狐微博騰訊微博郵件 講師介紹 名稱&#xff1a;Alan Cannistraro 課程介紹 如果你對iPhone Development有興趣&#xff0c;以下是入…

在Python中有效使用JSON的4個技巧

Python has two data types that, together, form the perfect tool for working with JSON: dictionaries and lists. Lets explore how to:Python有兩種數據類型&#xff0c;它們一起構成了使用JSON的理想工具&#xff1a; 字典和列表 。 讓我們探索如何&#xff1a; load a…

Vlan中Trunk接口配置

Vlan中Trunk接口配置 參考文獻&#xff1a;HCNA網絡技術實驗指南 模擬器&#xff1a;eNSP 實驗環境&#xff1a; 實驗目的&#xff1a;掌握Trunk端口配置 掌握Trunk端口允許所有Vlan配置方法 掌握Trunk端口允許特定Vlan配置方法 實驗拓撲&#xff1a; 實驗IP地址 &#xff1a;…

django中的admin組件

Admin簡介&#xff1a; Admin:是django的后臺 管理的wed版本 我們現在models.py文件里面建幾張表&#xff1a; class Author(models.Model):nid models.AutoField(primary_keyTrue)namemodels.CharField( max_length32)agemodels.IntegerField()# 與AuthorDetail建立一對一的關…

虛擬主機創建虛擬lan_創建虛擬背景應用

虛擬主機創建虛擬lanThis is the Part 2 of the MediaPipe Series I am writing.這是我正在編寫的MediaPipe系列的第2部分。 Previously, we saw how to get started with MediaPipe and use it with your own tflite model. If you haven’t read it yet, check it out here.…

.net程序員安全注意代碼及服務器配置

概述 本人.net架構師&#xff0c;軟件行業為金融資訊以及股票交易類的軟件產品設計開發。由于長時間被黑客攻擊以及騷擾。從事高量客戶訪問的服務器解決架構設計以及程序員編寫指導工作。特此總結一些.net程序員在代碼編寫安全以及服務器設置安全常用到的知識。希望能給對大家…

文件的讀寫及其相關

將軟件布置在第三方電腦上會出現無法提前指定絕對路徑的情況&#xff0c;這回影響到后續的文件讀寫&#xff1b;json文件是數據交換的一種基本方法&#xff0c;為了減少重復造輪子&#xff0c;經行標準化代碼。關于路徑&#xff1a; import os workspaceos.getcwd() pathos.pat…

接口測試框架2

現在市面上做接口測試的工具很多&#xff0c;比如Postman&#xff0c;soapUI, JMeter, Python unittest等等&#xff0c;各種不同的測試工具擁有不同的特色。但市面上的接口測試工具都存在一個問題就是無法完全吻合的去適用沒一個項目&#xff0c;比如數據的處理&#xff0c;加…

python 傳不定量參數_Python中的定量金融

python 傳不定量參數The first quantitative class for vanilla finance and quantitative finance majors alike has to do with the time value of money. Essentially, it’s a semester-long course driving notions like $100 today is worth more than $100 a year from …