本系列學習在.NET中的並發並行編程模式,實戰技巧
內容目錄
函數式編程
一個函數輸出當做另一個函數輸入。有時候一個復雜問題,我們拆分成很多個步驟函數,這些函數組合起來調用解決一個復雜問題。
在C#中不支持函數組合,但可以直接像這樣調用B(A(n)),這也是函數組合,但這不利於閱讀,人們習慣從左往右閱讀,而不是相反的方向。通過創建擴展方法可以任何組合兩個函數,像下面這樣
Func<A,C> Compose<A,B,C>(this Func<A.B> f ,Func<B,C> g)=>(n)=>g(f(n))
上述代碼為泛型委托Func<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">創建了一個擴展Compose的擴展方法,以泛型委托Func<b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">為輸入參數,返回組合后的函數Func<a,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">。創建一個高階函數Compose把不利於閱讀的隱藏起來。
在F#中就非常方便的使用函數組合。舉個例子,將一個列表中數字增加4再乘以3,構建這兩個步驟的函數(當然利用C#linq或F#map可以直接(x+4)*3,這里主要演示兩個功能函數如何組合起來)。
let add4 x=x+4
let mulitply3 x=x*3
let list=[0..10]
let newList=List.map(fun x->mulitply3(add4(x))) list
let newList2=list |>List.map(add4>>mulitply3
在F#中使用>>中綴運算符來使函數組合可以從左到右閱讀,更加精煉、簡潔。
閉包的應用
閉包可以讓函數訪問其所在的外部函數中的參數和變量,即使在其外部函數被返回之后。在js中經常會出現閉包的場景,在C#和F#中,編譯器使用閉包來增加和擴展變量的范圍。
C#在.NET2.0后引入閉包。在lambda和匿名方法中得到充分的使用。像下面的匿名函數引用變量a,訪問和管理變量a的狀態。如果不用閉包,就需要額外創建一個類函數來調用。
string s= "free variable";
Func<string,string> lambda = value=> a + " " + value;
以下載圖片更新窗體PictureBox控件為例:
void UpdateImage(string url)
{
System.Windows.Forms.PictureBox picbox = this.pictureBox1;
var client = new WebClient();
client.DownloadDataCompleted += (o, e) =>
{
if (picbox != null)
{
using (var ms = new MemoryStream(e.Result))
{
picbox.Image = Image.FromStream(ms);
}
}
};
client.DownloadDataAsync(new Uri(url));
//picbox = null;
}
因為是異步下載,UPdateImage方法返回后,圖片還未下載完成,但picbox變量仍然可以使用。這就是變量捕獲。lambda表達式捕獲了局部變量image,因此它仍停留在作用域中。但捕獲的變量值是在運行時確定的,而不是在捕獲時,最后一句如果放開,將不能更新窗體。運行時picbox為null了,在F#中不存在null的概念,所以也不會出現此類錯誤。
多線程環境中的閉包使用。猜測下面的代碼運行結果如何?
for (int i = 1; i < 10; i++)
{
Task.Factory.StartNew(()=>Console.WriteLine("{0}-{1}",
Thread.CurrentThread.ManagedThreadId,i));
}
不會按期望的那樣打印1-9,因為他們共享變量i,調用時i的值可能已經被循環修改了。印證上面說的捕獲的變量值是在運行時確定的。
這種情況就很難搞,給並行編程帶來了頭疼的問題,變量可變,這不廢話嗎,變量不會變就不叫變量了。在C#中解決此類問題的一個方法就是為每個任務創建創建和捕獲一個新的臨時變量,這樣它就能保留捕獲時的值。在F#中不存在這個問題,它的For循環每次創建一個新的不可變值。
記憶化函數緩存
一些函數會頻繁的使用相同的參數去調用。我們可以將用相同的參數調用函數的結果存儲起來,以便下次調用直接返回結果。例如對圖片每個像素做處理,一張圖片可能相同像素的會有很多,通過緩存可以直接返回上次計算結果。
//簡單的函數緩存
public static Func<T, R> Memoize<T, R>(Func<T, R> func) where T : IComparable
{
Dictionary<T, R> cache = new Dictionary<T, R>();
return arg =>
{
if (cache.ContainsKey(arg))
return cache[arg];
return (cache[arg] = func(arg));
};
}
// 線程安全的函數緩存
public static Func<T, R> MemoizeThreadSafe<T, R>(Func<T, R> func) where T : IComparable
{
ConcurrentDictionary<T, R> cache = new ConcurrentDictionary<T, R>();
return arg => cache.GetOrAdd(arg, a => func(a));
}
// 利用延遲提高性能的函數緩存
public static Func<T, R> MemoizeLazyThreadSafe<T, R>(Func<T, R> func) where T : IComparable
{
ConcurrentDictionary<T, Lazy<R>> cache = new ConcurrentDictionary<T, Lazy<R>>();
return arg => cache.GetOrAdd(arg, a => new Lazy<R>(() => func(a))).Value;
}
上述示例代碼中有三個版本的函數記憶化。調用像下面這樣
public static string Greeting(string name)
{
return $"Warm greetings {name}, the time is {DateTime.Now.ToString("hh:mm:ss")}";
}
public static void RunDemoMemoization()
{
var greetingMemoize = Memoize<string, string>(Greeting);
Console.WriteLine(greetingMemoize("Richard"));
Console.WriteLine(greetingMemoize("Paul"));
Console.WriteLine(greetingMemoize("Richard"));
}
線程安全字典ConcurrentDictionary可以保證只向集合里添加一個相同值,但函數求值可能會被執行多次,所以利用.NET4之后的延遲對象加載技術。在真正需要使用對象時候才去實例化(通過訪問延遲對象的Value屬性),而且是線程安全的。
補充一個,以前寫設計模式時,單例模式的創建方式,第四種,延遲+線程安全的單例模式,傳送門,設計模式速查手冊
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy =
new Lazy<Singleton>(() => new Singleton(), true); //#A
public static Singleton Instance => lazy.Value;
private Singleton()
{ }
}
to be contiued!
下集:不可變性
寫給普通:
花有重開日,人無再少年
小時候快樂是本能,長大后快樂是本事