close

Week 1

第一個禮拜真的是很戰鬥的戰鬥營阿!

除了學習繼承、多型等在物件導向程式語言中,很重要的概念之外

也學習介面、抽象類別、override

而我們以小組形式製作遊戲小專題

我們這組是做電子寵物機

運用上課所學的繼承與多型,去生成各種不同種的動物

是個有趣但也高壓的一個禮拜

不過太空人我,很喜歡寫程式

也熱愛debug後的成就感

所以挺喜歡這個禮拜的生活喔~~

(第一個禮拜的電子寵物機成果)

電子寵物

Week 2

這周學了匿名內部類別、內部類別、lambda

在實作lambda的時候超痛苦的

因為不熟悉

也因為lambda是在程式執行時才會實現

所以我一度很崩潰不知自己到底錯在哪

後來才發現是回傳值我寫boolean

但在做比較時我回傳int

(蠻愚蠢的我知道QQ)

不過也因為這個蠢問題讓我在下次使用lambda時

更加注意細節

算是一個又痛苦又苦惱的一周

(本來這周也有小遊戲專題不過延了一個禮拜,下周再分享吧XD)

Week 3

這周都是講解很重要的概念!!!!!

包括怎麼自己實現 ArrayList && LinkedList 的功能

(還有重要的資結 EX: Stack, Queue, Hash, Map, 排序)

雖然java有內建的 ArrayList && LinkedList

不過真的自己實做一次會比較了解他們的邏輯

我覺得是很好的學習方式!!

而這禮拜的小遊戲專題是RPG

分享一下成果吧

擷取

Week 4

這周大量練習遊戲邏輯、畫面、輸入

對我來說最大的困難點在於如何拆分class

總覺得要自己去想那些東西可以獨立出一個class變得很抽象

而其中,在存取圖片上,我也沒想過可以用內部、抽象類別去做階層的概念

這部分還需要多多練習了!

遇到最蠢的是我一直叫不到想要的資料夾

debug半天,才發現我根本沒設接口給他QQ

123

Week 5

序列化

FileOutputStream跟FileWriter功能一樣,只是FileWriter是用來寫文字,FileOutputStream可以把一些stream串流{也就是圖、文等等換成電腦懂得程式語言(010101)}傳出去

87465

如何序列化

bvheigfbia

序列化條件:所有屬性都要可以被序列化,自己寫的參考資料型態(object)就都要 implements serializable才可使用

7

12

很大的限制!!沒泛型所以要強轉,不然根本不知道你讀來的是甚麼類型的檔案

uygdsfda

Week 6

泛型

Node後面給予<T>型態的限制很重要

78946521

Week 7 & Week 8

製作遊戲專題

投影片3

投影片4

投影片5

投影片6

投影片7

投影片8

Week 9

C# 資料結構

參考資料:http://people.cs.aau.dk/~normark/oop-csharp/html/notes/collections_themes-non-generic-collections-sect.html

泛型非泛型:https://www.itread01.com/p/607670.html

List


依索引存取項目
https://www.dotnetperls.com/list

底層運作

建構子 (官方文件)

 
public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T> { ... private T[] _items; //array形式 ... static readonly T[] _emptyArray = new T[0]; ... public List() { _items = _emptyArray; //預設空array } }

實現


IEnumerable<T>

 
IEnumerator<T> GetEnumerator ( )

IEnumerator<T>

 
T Current bool MoveNext( ) void Reset ( )

這個介面主要是為了實現列舉器的功能,讓我們可以遍歷整個資料集合,能夠將資料集合中的成員 , 逐一取出並回傳,常見的例子是foreach的使用。

ICollection<T>

 
bool Contains(T element) void Add(T element) bool Remove(T element) void Clear() void CopyTo(T[] targetArray, int startIndex) int Count bool IsReadOnly

實現此介面可以完善集合的基本運用,包含判斷元素是否存在,新增或刪除元素,以及清空所有元素等等。

 IList<T>

 
T this[int index] int IndexOf(T element) void Insert(int index, T element) void RemoveAt(int index)

此介面最大的目的在於"索引"功能,透過索引位置,可以直接訪問該索引的的元素,也可以插入或刪除指定位置的元素,但是,再插入與刪除時,需要使用Array.Copy()來創建新的陣列並複製過去,效能上比較沒那麼好。如果插入刪除的位置越後面,就會越不耗能(不用移動那麼多)。
EX:

 
public void Insert(int index, T item) { // Note that insertions at the end are legal. if ((uint) index > (uint)_size) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert); } Contract.EndContractBlock(); if (_size == _items.Length) EnsureCapacity(_size + 1); if (index < _size) { Array.Copy(_items, index, _items, index + 1, _size - index); } // 創建+複製 //如何實現(為什麼慢) _items[index] = item; _size++; _version++; }

總體而言,List<T> 的使用上類似Java中的ArrayList<T> ,其內部也是使用Array實現,其資料特性是有順序並以連續記憶體空間的方式儲存,當新增元素發現容量不足時,會自動擴增容量,因此在使用上會更加方便。也因為泛型的緣故,而不需要在存取時替物件轉型,從而減少效能。

如何使用

建構子
List(), List(IEnumerable<T>), List(int)

 
Via a collection initializer: new List<T> {t1, t2, ..., tn}

取得元素 O(1)
this[int], GetRange(int, int)
測量(當前資料量、當前容量)
Count, Capacity
新增元素…//當容量不夠時,會再新增元素前,將容量變成當前兩倍大
Add(T), AddRange(IEnumerable<T>), Insert(int, T), InsertRange(int, IEnumerable<T>)
刪除元素
Remove(T), RemoveAll(Predicate<T>),  RemoveAt(int), RemoveRange(int, int), Clear()
排序
Reverse(),  Reverse(int, int),
Sort(), Sort(Comparison<T>), Sort(IComparer<T>),
搜尋 O(N)
BinarySearch(T),
Find(Predicate<T>), FindAll(Predicate<T>), FindIndex(Predicate<T>),
FindLast(Predicate<T>), FindLastIndex(Predicate<T>), IndexOf(T), LastIndexOf(T)
布林查詢 //遍歷方式
Contains(T), Exists(Predicate<T>), TrueForAll(Predicate<T>)

使用時機

優點:
1.存取快速:只需要 O(1) 時間對 Array 的資料做存取。
2.比 Linked list 為節省記憶體空間,因為 linked list 需要多一個指標 pointer 來記錄指定節點的記憶體位置。
3.比起一般的ArrayList,因為省去裝箱、拆箱的步驟,儲存型別為安全。

缺點:
若時常在新增、刪除資料,要時常調整陣列的大小,還會花費 O(N) 的時間在搬動資料(把資料從舊的陣列移動到新的陣列)。

適用時機:
1.快速存取資料時
2.已知資料的數量,如此便不用經常改變陣列大小
3.要求記憶體空間的使用越少越好。
例如:簡易、有順序、有明確數量的客戶名單建置

Dictionary

儲存項目為成對的索引鍵/值 (Key/Value),以供依據索引鍵快速查詢

底層運作

實現

ICollection<KeyValuePair<TKey,TValue>>  IDictionary<TKey,TValue> IEnumerable<KeyValuePair<TKey,TValue>>  IEnumerable<T> IReadOnlyCollection<KeyValuePair<TKey,TValue>>  IReadOnlyDictionary<TKey,TValue>
 ICollection   IDictionary IEnumerable IDeserializationCallback  ISerializable

IDictionary<TKey,TValue> 

 
 
void Add(K key, V value) // only possible if key is not already present bool Remove(K key) bool ContainsKey(K key) bool TryGetValue(K key, out V value)

是索引鍵/值組之泛型集合的基底介面
每個元素都是儲存在物件中的索引鍵/值組 KeyValuePair<TKey,TValue>

IReadOnlyCollection<T>

 
 
IEnumerator GetEnumerator() // 傳回逐一查看集合的列舉值

表示項目的強型別、唯讀集合

IReadOnlyDictionary<TKey,TValue> 

 
bool ContainsKey(TKey) IEnumerator GetEnumerator() bool TryGetValue(TKey, TValue)

代表索引鍵/值組的泛型唯讀集合

IDeserializationCallback 
反序列化 (Deserialization) 完成時告知類別

ISerializable 
允許物件控制它自己的序列化 (Serialization) 和還原序列化 (Deserialization)

類型參數


TKey 字典中索引鍵的類型
TValue 字典中值的類型

邏輯結構



使用索引鍵來抓取值相當快速,接近 O (1) ,因為Dictionary<TKey,TValue> 類別會實作為雜湊表

如何使用

建構子

Dictionary<TKey,TValue>():
初始化 Dictionary<TKey,TValue> 類別的新執行個體,這個執行個體是空白的、具有預設的初始容量,並使用索引鍵類型的預設相等比較子。

 
IDictionary<Key, Value> dic1 = new Dictionary<Key,Value>();

Dictionary<TKey,TValue>(IDictionary<TKey,TValue>):
初始化 Dictionary<TKey,TValue> 類別的新執行個體,其中包含從指定的 IDictionary<TKey,TValue> 複製的項目,並使用索引鍵類型的預設相等比較子。

 
IDictionary<Key, Value> dic2 = new Dictionary<Key,Value>(dic1);

Dictionary<TKey,TValue>(IDictionary<TKey,TValue>, IEqualityComparer<TKey>):
初始化 Dictionary<TKey,TValue> 類別的新執行個體,其中包含從指定的 IDictionary<TKey,TValue> 複製的項目,並使用指定的 IEqualityComparer<T>

 
Dictionary<string, string> copy = new Dictionary<string, string>(openWith,StringComparer.CurrentCultureIgnoreCase);

Dictionary<TKey,TValue>(IEnumerable<KeyValuePair<TKey,TValue>>):
初始化 Dictionary<TKey,TValue> 類別的新執行個體,這個類別包含從指定的 IEnumerable<T> 所複製項目。

 
IDictionary<Key, Value> dic3 = new Dictionary<Key,Value>(IEnumerable<KeyValuePair<TKey,TValue>>);

方法

  • Add(TKey, TValue):
    將指定的索引鍵和值加入字典。

  • Remove(TKey):
    將具有指定索引鍵的值從 Dictionary<TKey,TValue> 中移除。

  • Remove(TKey, TValue):
    從 Dictionary<TKey,TValue> 中移除具有指定索引鍵的值,並將該項目複製到 value 參數。

  • Clear():
    從 Dictionary<TKey,TValue> 移除所有索引鍵和值。

  • ContainsKey(TKey):
    判斷 Dictionary<TKey,TValue> 是否包含特定索引鍵。

  • ContainsValue(TValue):
    判斷 Dictionary<TKey,TValue> 是否包含特定值。

  • TryAdd(TKey, TValue):
    嘗試將指定的索引鍵和值新增至字典。

  • TryGetValue(TKey, TValue):
    取得與指定索引鍵關聯的值。

  • GetEnumerator():
    傳回在 Dictionary<TKey,TValue> 中逐一查看的列舉值。

  • GetObjectData(SerializationInfo, StreamingContext):
    實作 ISerializable 介面,並傳回序列化 Dictionary<TKey,TValue> 執行個體所需的資料。

  • OnDeserialization(Object):
    實作 ISerializable 介面,並於還原序列化完成時引發還原序列化事件。

  • EnsureCapacity(Int32):
    確保字典最多可以保留指定的項目數量,但不必進一步擴充其支援儲存體。

  • TrimExcess():
    將此字典容量設定為一開始若使用所有項目初始化的應有容量。

  • TrimExcess(Int32):
    將此字典容量設定為最多可以保留的指定項目數量,但不必進一步擴充其支援儲存體。

使用時機

查詢元素的時間複雜度接近 O(1) ,可用來做資料快取,提升整體效率。
例如: 擁有 Key 和 Value 為一組的資料, e.g. 簡易銀行資料 - 人名 & 帳戶

 
IDictionary<Person, BankAccount> bankMap = new Dictionary<Person,BankAccount>(); bankMap.Add(person1, bankAcount1); bankMap[person1] = new BankAcount(bankAcount2); bankMap[new Person("Maria")] = ba4; bankMap.Remove(person1);

ref:
https://flyingsnow-hu.github.io/brokenslips/unity/2017/11/28/dictionary.html
Microsoft
https://www.dotnetperls.com/
http://people.cs.aau.dk/~normark/oop-csharp/html/notes/theme-index.html

Queue

物件先進先出 (FIFO) 的集合

底層運作

直接實現 IEnumerable<T> 

實現介面

IEnumerable<T>
同前面敘述

IEnumerable
同前面敘述

IReadOnlyCollection<T>
提供泛型唯讀集合的基底類別

ICollection
ICollection實現了計算集合元素數目的功能,但沒有提供更改集合的功能

補充

比較 ICollection<T>  ICollection

ICollection<T>是可以統計集合中物件的標準介面。該介面可以
Count確定集合的大小
Contains集合是否包含某個元素
ToArray複製集合到另外一個數組
集合是否是IsReadOnly,如果一個集合是可編輯的
可以呼叫Add Remove Clear 等方法操作集合中的元素
這也是為何Queue和Stack不需實現ICollection<T>

如何使用

 
 
//3 constructor Queue<T> number = new Queue<T>(); Queue<T> queue = new Queue<T>(IEnumerable<T> collection); Queue<T> queue = new Queue(int capacity); //Attribute Count 傳回Queue集合物件中的元素數量 //function Enqueue()增加並移除 Dequeue()取出並移除 Peek()取出不移除 ToArray()取陣列(例如:複製陣列,可用第二種建構子取得) Count 傳回Queue集合物件中的元素數量 Contains()檢查queue是否包含某一個特定項目 Clear()清除queue所有的項目 TrimToSize()調整queue的大小到實際儲存項目的大小

使用時機

先進先出 (FIFO) 地使用項目
例如:演唱會門票訂票系統,先到的人先買到票
例如:網路連線

Stack

表示物件的後進先出 (LIFO) 集合。

底層運作

同Queue

實作

IEnumerable<T> IReadOnlyCollection<T> ICollection IEnumerable

如何使用

 
//3 constructor Stack<T> number = new Stack<T>(); Stack<T> queue = new Stack<T>(IEnumerable<T> collection); Stack<T> queue = new Stack(int capacity); //Attribute Count 傳回Stack集合物件中的元素數量 //function Push() 增加並移除 Pop()取出並移除 Peek()取出不移除 ToArray()取陣列(例如:複製陣列,可用第二種建構子取得) Contains()檢查Stack是否包含某一個特定項目 Clear()清除Stack所有的項目 TrimToSize()調整Stack的大小到實際儲存項目的大小

使用時機

後進先出 (LIFO) 地使用資料
例如:瀏覽網頁時,上一頁下一頁的資料紀錄

LinkedList

循序存取項目

底層運作

 
 
Next - getter Previous - getter List - getter Value - getter and setter

Node為一個單位,串接資料,使用上必須先有LinkedListNode<T>類的建構,才能有後續操作。Node屬性包含value(data),和下一個Node,值得一提的是,C#的LinkedListNode(T)還有前一個Node的屬性,屬於雙向鏈結。

實現

IEnumerable<T>、ICollection<T>

如何使用

 
()屬性 Count Node數量 First 第一個Node Last 最後一個 Node ()方法 新增類 AddFirst(T) //新增資料在開頭,會自動建立Node AddLast(LinkedListNode<T>) //新增資料在尾端 AddAfter(LinkedListNode<T>, T) //新增資料在現有的指定Node後面 訪問類 Find(T) //遍歷整個集合,回傳第一個符合指定值的Node //實作 public LinkedListNode<T> Find(T value) { LinkedListNode<T> node = head; EqualityComparer<T> c = EqualityComparer<T>.Default; if (node != null) { if (value != null) { do { if (c.Equals(node.item, value)) { return node; } node = node.next; } while (node != head); } else { do { if (node.item == null) { return node; } node = node.next; } while (node != head); } } return null; } FindLast(T) //遍歷整個集合,回傳最後一個符合指定值的Node 刪除類 Clear() //移除所有Node Remove(T) //遍歷集合移除第一次出現的指定值

總的來說,LinkedList雖然沒有索引的概念,因此若是要訪問指定元素則需要遍歷整個集合,但是在插入與刪除功能最為方便。

使用時機

優點:
1.新增/刪除資料比 Array 簡單,只要對要新增刪除的相關節點們調整指標即可,不需要像 Array 搬動其餘元素。
2.linked list 的資料數量能動態的向記憶體要空間或釋放空間,不像 Array 會有 “重新定義陣列大小” 的問題。

缺點:
1.因為 linked list 不能直接透過索引值找到某節點(ex: 像陣列可以透過 array[index] 找到要的元素),若要找到特定節點,需要從頭開始找起,搜尋的時間複雜度為 O(N)。
2.需要額外的記憶體空間來儲存指標(next 、 previous)。

適合情況:
1.無法預期資料數量時,使用linked list就沒有resize的問題。
2.需要頻繁地新增/刪除資料時。
3.不需要快速存取查詢資料時。
4.需要頭尾兩端增加元素時
具體例子!

SortedList

SortedList<K,V> 表示索引 (key/value) 鍵/值組配對的集合,這個集合按索引鍵排序,而且可以按索引鍵和索引存取,元素加入的當下就依據關聯的 IComparer<T> 被排序。
如果元素的型別由自己定義,則需自行實作比較的方法

和 SortedDictionary<TKey,TValue> 類似, 都是排序後的 Dictionary

  • 比起 SortedDictionary<TKey,TValue> 使用較少記憶體
  • 未排序的資料在插入和刪除時 SortedDictionary<TKey,TValue> 的速度較快
其他比較 SortedList<K,V> SortedDictionary<TKey,TValue>
Underlying structure 2 arrays BST
Contiguous storage Y N
Data access Key, Index Key

底層運作

使用 BinarySearch 方法進行查詢

實作

ICollection<KeyValuePair<TKey,TValue>> IDictionary<TKey,TValue>  IEnumerable<KeyValuePair<TKey,TValue>> IEnumerable<T>  IReadOnlyCollection<KeyValuePair<TKey,TValue>>  IReadOnlyDictionary<TKey,TValue> ICollection IDictionary IEnumerable

如何使用

使用時機

  • 排序的集合
  • 跟 Hashtable 比起來效能較差, 但可以透過 key & index 訪問 value 所以彈性較高
  • 會在建立 Collection 時就先排序好資料, 所以在建立時是很耗效能在排序清單上, 但是如果有少量資料異動或新增時它會自己排序效能就會很好, SortedList 適用在大部份是靜態也很少被異動的情況
  • 避免使用 SortedList 去排序 String, 會有額外的成本產生

HashSet

數學函式的集合 (無序集合)

  • 以數學集合 (Set) 模型為基礎,類似存取 Dictionary<TKey, TValue> 或 Hashtable 集合的索引鍵。簡而言之可以將 HashSet<T>類別視為沒有值的Dictionary<TKey, TValue>集合
  • 不會排序,也無法包含重複的項目
  • 提供許多數學集合 (Set) 運算,例如集合 (Set) 相加 (結合) 以及集合 (Set) 相減。

底層運作

實作介面

ICollection<T> IEnumerable<T> IReadOnlyCollection<T> IEnumerable

ISet<T>
此介面提供了執行集合的方法,HashSet<T> SortedSet<T> 集合都會執行此介面

IReadOnlySet<T>
為集合提供唯讀抽象屬性

IDeserializationCallback
還原序列化

ISerializable
允許物件控制它自己的序列化和還原序列化

優勢

  1. 提升檢索的性能:HashSet<T>的Contains方法時間複雜度是O(1),List<T>的Contains方法時間複雜度是O(n),後者數據量越大速度越慢,而HashSet<T>不受數據量的影響
  2. 減少內存浪費:HashSet每條數據只保存一項,並不採用Key-Value的方式,與Hashtable和Dictionary相比,可避免造成內存浪費

劣勢

  1. List<T>相比,不能使用下標來訪問元素,如:list[1] 。

  2. Dictionary<TKey, TValue>相比,不能通過鍵值來訪問元素,例如:dic[key]

如何使用

 
//constructor HashSet<T> hashSet = new HashSet<T>(); //function Add(T) Clear(T) Contains(T) UnionWith() //聯集 ExceptWith() //差集 IntersectWith() //交集 SymmetricExceptWith() //補集

使用時機

例如:存儲關鍵字的集合,運行的時候通過其Contains方法檢查輸入字符串,比使用List<T>快上許多
例如: 奇偶聯集, 搞小圈圈的時候可以用差集, 出國旅遊的地點決定使用交集
例如: Steam推薦和您相似的玩家也喜歡… 先使用交集確定是否是相近的玩家, 再用差集找出差異點後推薦予玩家

SortedSet

數學函式的集合(已排序)

底層運作

同HashSet,實作ISet<T>、ICollection<T>等等,集合內的元素必須是唯一的,但在底層資料結構的儲存上,HashSet是以只有Key的hash-table方式去儲存,而SortedSet則是red-black tree。

紅黑樹的原理

兩種資料儲存的方式,最大的差異就在,新增、刪除、搜尋等基本操作時,所花費的時間複雜度不同,HashSet是O(1);SortedSet是O(logN),在資料量龐大時,效能差異就會越趨顯著。

如何使用

同HashSet

 
//constructor SortedSet<T>() //T必須為有實作IComarable之類別,升冪排序。 SortedSet<T>(IComparer<T>) //自己提供T的比較器 //實作 public SortedSet(IComparer<T> comparer) { if (comparer == null) { this.comparer = Comparer<T>.Default; } else { this.comparer = comparer; } } //新增的方法 GetViewBetween() Max() Min()

使用時機

同HashSet,但想排序資料的時候,例如遊戲名稱(A-Z)

 

Week10

Extend

SQL中最基本的儲存單位,由8頁連續的Page組成,在記憶體中佔64KB空間。

SQL Server 有兩種Extend類型︰

https://i.imgur.com/71AeuH5.gif

uniform的範圍將由一個物件所擁有;範圍中的所有八個頁面只能被擁有的物件所使用。 mixed的範圍最多可被八個物件所共用。 範圍中的 8 個分頁都可以由不同的物件所擁有。

建立Table時,系統會先配置mixed的Page給Table用,當資料多到該Extend存不下時,會再給予一個uniform的空間儲存。

Page

構成Extend的單位,每一頁page固定大小為8KB。

https://i.imgur.com/5m3wF6c.png

Page Header:

每個分頁的開頭為 96 個位元組的Page Header,用來儲存與分頁有關的系統資訊。 此資訊包括頁碼、分頁類型、分頁上可用空間的數量,以及擁有分頁的物件配置單位識別碼。

Body:

每頁Page最多儲存8060Bytes的資料量,只要插入或更新操作把行的總大小增加到超過8,060位元組的限制,SQL Server就自動執行,把最大的變長列移動到ROW_OVERFLOW_DATA分配單元中的頁面中,只把存在於IN_ROW_DATA分配單元中原始頁面上的24位元組的指標保留下來,該指標指向ROW_OVERFLOW_DATA 頁面。如果後續的更新操作導致該行減少到小於8,060位元組,那麼SQL Server自動把列移回到原始資料頁中。

Offeset Row(資料列位移資料表):

從頁面結尾開始,而且每個資料表分別為分頁上每一列包含一個項目。 每個資料列位移項目,都記錄資料列第一個位元組距離分頁開頭多遠。 因此,資料列位移資料表的功能是協助 SQL Server 以非常快速的速度在分頁上找出資料列。 資料列位移資料表的項目順序與分頁中的資料列順序是相反的。

page種類

Data Page:

基本資料儲存頁

Index Page:

索引儲存頁

Text and Image:

儲存圖片和Text檔

GAM,SGAM (Share)Global Allocation Map:

整體配置對應 (GAM) GAM 分頁可記錄已配置哪些範圍。 每個 GAM 可涵蓋 64,000 個範圍,或接近 4 GB 的資料。 GAM 以 1 個位元來表示所涵蓋期間的每個範圍。 若位元為 1,代表範圍可用;若位元為 0,代表範圍已配置。

共用的整體配置對應 (SGAM) SGAM 分頁可記錄哪些範圍目前當作混合範圍使用,並至少擁有一個未使用的分頁。 若位元為 1,則表示該範圍目前當作混合範圍使用,並具有可用分頁。 若位元為 0,則表示該範圍不是當作混合範圍使用,或者它是混合範圍,但所有分頁都在使用中。

PFS (Page Free Space):

頁面可用空間 (PFS) 以byteMap儲存page資訊,一頁PFS可以記錄8088頁的資訊。

  • bits 0-2: how much free space is on the page
    • 0x00 is empty
    • 0x01 is 1 to 50% full
    • 0x02 is 51 to 80% full
    • 0x03 is 81 to 95% full
    • 0x04 is 96 to 100% full
  • bit 3 (0x08): is there one or more ghost records on the page?(準刪除清除程序是單一執行緒背景程序,會將已標示為要刪除的記錄從頁面刪除。)
  • bit 4 (0x10): is the page an IAM page?
  • bit 5 (0x20): is the page a mixed-page?
  • bit 6 (0x40): is the page allocated?
  • Bit 7 is unused

當範圍配置給物件之後,SQL Server Database Engine 會使用 PFS 分頁,來記錄範圍中哪些分頁是已配置或可用的。 當 SQL Server Database Engine 必須配置新分頁時,就會使用此資訊。 分頁中的可用空間量只會針對heap與 Text/Image 分頁而保留。 當 SQL Server Database Engine 必須尋找具有可用空間的分頁來存放新插入的資料列時,就會用到它。 因為插入新資料列的點由索引鍵值所設定,所以使用索引時不需追蹤分頁可用空間。

IAM (Index Allocation Map):

紀錄物件有使用到的Extend,最高紀錄4GB範圍,PageHeader紀錄該IAM頁的負責範圍,Body的資料每一bit都代表一個Extend。每個物件都有一個以上的IAM,透過bitMap紀錄該物件有使用到那些Extend。

通常和PFS會一起存放在SQL server的記憶體中,方便快速找到資料。

Bulk Changed Map:

大量複製變更對應 (BCM) 這種對應會追蹤自從上個 BACKUP LOG 陳述式之後由大量記錄作業修改過的範圍。 若某個範圍的位元為 1,代表該範圍自從上個 BACKUP LOG 陳述式之後已被大量記錄作業所修改。 若位元為 0,代表該範圍並未被大量記錄作業所修改。 雖然 BCM 分頁出現在所有資料庫中,但它們只適用於資料庫正在使用大量複製復原模式時。 在此復原模式中,當執行 BACKUP LOG 時,備份處理序會掃描 BCM 來找出修改過的範圍。 接著將這些範圍記錄到記錄備份中。 若資料庫是由資料庫備份和一系列的交易記錄檔備份來還原,這將可復原大量記錄作業。 對於使用簡單復原模式的資料庫,BCM 分頁並不適用,因為並沒有記錄下大量記錄作業。 它們也不適用於使用完整復原模式的資料庫,因為該復原模式會將大量記錄作業視為完整記錄作業。

Differential Changed Map:

差異式變更對應 (DCM) 這種對應會追蹤自從上個 BACKUP DATABASE 陳述式之後發生變更的範圍。 若某個範圍的位元為 1,代表該範圍自從上個 BACKUP DATABASE 陳述式之後已經修改。 若位元為 0,代表該範圍並未修改過。 差異備份只讀取 DCM 分頁,找出哪些範圍經過修改。 這可大幅減少差異式備份必須掃描的分頁數目。 差異式備份的執行時間長度,將與上次執行 BACKUP DATABASE 陳述式之後修改過的範圍數目成正比,而不是與資料庫的整體大小成正比。

Table架構

架構:Table→ 分割槽Partition(1 or 多) → 分割槽Partition(一個堆 or 多B樹) → 三種配置單位(IAM)

  • 每張表有一個對應的物件ID,並且包含一個或多個分割槽
  • 每個分割槽會有一個堆或者多個B樹
  • 堆積或索引的每個資料分割都會至少包含一個 IN_ROW_DATA 配置單位。 資料分割也可能會包含 LOB_DATA 或 ROW_OVERFLOW_DATA 配置單位。
  • 3種配置單位都有自己的IAM(Index Allocation Map),分配記錄以下三種空間位置:
    1. IN_ROW_DATA保存堆積或索引的資料分割。
    2. LOB_DATA保存大型物件 (LOB) 資料類型,例如 XML、VARBINARY(max) 與 VARCHAR(max)。如果LOB資料或長度超過8060位元組的記錄,則可能有另外的LOB配置單位和ROW_OVERFLOW_DATA(行溢位)配置單位
    3. ROW_OVERFLOW_DATA保存儲存於 VARCHAR、NVARCHAR、VARBINARY 或 SQL_VARIANT 資料行中,超過 8,060 個位元組資料列大小限制的可變長度資料。
  • SQL SERVER 資料空間都是以頁(Page)為一個單位,存放索引的空間就叫索引頁,存放資料的空間就叫資料頁,資料頁或者是索引頁,可以存的大小是 8060 byte

總結: 資料有clustered index,則存B-tree,沒有就存heap,在已有clustered index的table刪除index,會使資料移至heap,反之移至B-tree。每個配置單位可以有很多資料頁,資料頁根據表是否有索引(堆),索引是聚集還是非聚集來區分

https://i.imgur.com/9U7OdqY.gif

管理物件所使用的空間

  • IAM 可以存 4 GB

,mmma.JPG

  • IAM 頁面是視需要配置給每個配置單位多少容量,而且在檔案中的位置是隨機的。 系統檢視指向配置單位的第一個 IAM 分頁。 該配置單位的所有 IAM 分頁都會連結成一條 IAM 鏈結。

jfwioejgf.JPG

何謂堆?

  1. 堆(不含聚集索引的表)
  2. 堆結構中只有data page跟IAM page,沒有索引頁。
  3. 堆中的data page沒有層次結構,都是葉子節點
  4. data page之間沒有雙向連結串列

堆只有一個分割槽,分割槽都有index_id= 0,其下面的每個分配單元都有一個連線指向Index Allocation Map頁(IAM)

SQL Server 使用 IAM 頁在堆中移動。堆內的資料頁和行沒有任何特定的順序,也不連結在一起。資料頁之間唯一的邏輯連線是記錄在 IAM 頁內的資訊

5aa40ce8a4a0aba8ff986211c7f15824.jpg

使用時機

適合使用在Log資料表、Event資料表、稽核資料表….一直新增的資料,但比較少查詢或更新的表

優點

因為插入資料不需要考慮排序,插入資料比索引表來的快

缺點-forwarding pointer

Heap資料表更新欄位資料,可能會造成forwarding pointer因為原本page(8KB)塞不下更新後資料就會先把資料搬到ROW_OVERFLOW_DATA上並在原本的page建立一個類似指標東西指向它

假如有個掃描需求

  1. 讀取要讀Page1發現有些資料在其他(Page2,Page3)
  2. 所以到forwarding pointer (Page2,Page3)搜索資料
  3. 搜尋完Page1接者搜尋Page2,Page3

因為Page1資料forwarding pointer到其他Page導致Scan資料時多了2個page read,如果forwarding pointer數量一多對於讀的效能

HT0bui0.png

何謂 B-tree?

https://www.youtube.com/watch?v=NI9wYuVIYcA(2.20)

擷取.JPG

B-tree, B代表Balanced與Balanced Binary Search Tree不同, 只是一種自平衡的樹,並保持數據有序

資料結構能讓查找數據、順序訪問、插入數據及刪除的動作,都在O(logN)

一個節點可以擁有2個以上的子節點

使用時機

常被應用在資料庫和文件系統的實現上

讀取需很多準備時間、一次讀取一連串資料

優點

適用於讀寫相對大的數據塊的存儲系統,例如磁碟

減少定位記錄時所經歷的中間過程,從而加快存取速度

為何使用 B-tree 而非自平衡二元搜尋樹?

自平衡二元搜尋樹:Balanced Binary Search Tree,Red-Black Tree

目的:減少硬碟讀取次數

987465.JPG

索引

  • What is an index in SQL
    1. Clustered index is actually related to how the data is stored, there is only one of them possible per table. You can open the book at 'Hilditch, David' and find all the information for all of the 'Hilditch's right next to each other
    2. Non-clustered index is different in that you can have many of them and they then point at the data in the clustered index
    • For example phone book

      clustered index keyed on (lastname, firstname)

      non-clustered index keyed on (town, address)

      Imagine if you had to search through the phone book for all the people who live in 'London' - with only the clustered index you would have to search every single item in the phone book since the key on the clustered index is on (lastname, firstname) and as a result the people living in London are scattered randomly throughout the index.

SQL 提供二種索引:【叢集索引】與【非叢集索引】,這二種索引的存放都是採用 B-Tree 架構。 如果沒有任何索引,資料直接以 Heap 架構存放。

索引頁(index page)與資料頁(data page)

sql-cluster-index.png

索引頁有三種:非叢集索引的葉子節點,叢集索引的非葉子節點,非叢集索引的非葉子節點

資料頁:聚集索引的葉子節點

叢集索引(**Clustered index)**是指資料庫中的數據順序與鍵值的索引順序相同。一個表所對應的叢集索引只能有一個。與非叢集索引相比,叢集索引有著更快的檢索速度

fb362329606a6f4f85e3d5d86b745bfd.jpg

非聚集索引(Non-clustered index)

此類的索引單純存放著指標資料,而該指標會對應到的是 Clustered-Index 或是 Heaper 內實體資料所存放的位置。

ba453796f844686baa335357edfaf40c.jpg

決定聚集索引欄位的考量

  1. 欄位數盡可能越少越好
  2. 欄位大小盡可能越少越好
  3. 常被用來 ORDER BY 或是 GROUP BY 的欄位若索引鍵值常用於ORDER BY 與GROUP BY中時,因為實體資料已經排序好了,系統不會再進行排序的動作,因此會增加執行的速度。

不適合當聚集索引的欄位

  1. 更新頻率過高的欄位因為Clustered Index每次更新時都會對實體資料進行排序,如果資料量較大的資料表,系統會在排序上會多花不少時間處理,所以並不建議以此類的欄位當索引鍵。
  2. 過多或過大的欄位所組成鍵值因為過多或過大都會造成系統在進行排序時的負擔。
  3. 獨特性過高的欄位因為實體資料是經過排序存入到硬碟中,若欄位中每筆記錄都沒甚麼順序性,則無法有效利用到此排序索引

補充Types of Database Files

  • Primary data file.

包含資料庫的啟動資訊,並指向資料庫中的其他檔案。 每個資料庫都有一個主要資料檔案。

建議主要資料檔的副檔名設為 .mdf。

  • Secondary data file.

選擇性的使用者定義資料檔案。 可將每個檔案放在不同的磁碟機上,以將資料分散在多個磁碟上。

建議次要資料檔的副檔名為 .ndf。

  • Log file.

記錄包含用來復原資料庫的資訊。 每個資料庫至少要有一個記錄檔。

建議交易記錄檔的副檔名為 .ldf。

補充Types of Checkpoint

Checkpoint.png

Automatic Checkpoint

Indirect Checkpoint

Manual Checkpoint

Internal Checkpoint

NoSQL- Redis

Remote Dictionary Server,是快速的開源記憶體內鍵值資料存放區。

為一個數據結構服務器(data structures server)支援網路、可永續性的鍵值對儲存資料庫,屬於非關聯式資料庫 (NoSQL),是目前最流行的鍵值對儲存資料庫

外圍由一個鍵、值對應的Dictionary 為基礎

20201229201917803.png

與其他NoSQL不同在於:Redis中值的類型不僅限於字串,還支援如下抽象資料類型:

  • String
  • List
  • Hash
  • Set
  • Sorted Set

值的類型決定了值本身支援的操作。

Redis支援不同無序、有序的列表,無序、有序的集合間的交集、併集等進階伺服器端原子操作。

20201229140229711.png

hash衝突

Redis 解決雜湊衝突的方式和JAVA中的hash表解決方式一樣,也是鏈式雜湊。指同一個雜湊桶中的多個元素用一個鍊結串列來儲存

2020122920124886.png

如何使用

Redis的所有功能就是將數據以其固有的幾種資料結構保存,並提供給用戶操作這幾種結構的接口

  • Strings
  • Hashs
  • Lists
  • Sets
  • Sorted Sets
  • 支援pub/sub訊息訂閱機制,可以用來進行訊息訂閱與通知

特色

  • 為了滿足高性能,Redis採用記憶體 (in-memory) 資料集 (dataset)。
  • 提供持久化:RDB方式(5分鐘一次)、AOF方式(1s一次)

使用時機

  1. Redis使用最佳方式是全部資料都儲存在記憶體
  2. 當需要除key/value之外的更多資料類型支援時,使用Redis更合適

缺點

  1. 只能使用單執行緒,效能受限於CPU效能
  2. 資料庫是用硬碟儲存,Redis 是存在記憶體,以儲存成本來說,資料庫會便宜許多

應用情境

  • 當儲存資料的時候,可以新增一個 Expire time 的參數,當這個時間一到之後,這個 key 就會自動被清除。舉例來說,短網址的 expire 可以設定成 7 天,當某個網址 7 天內都沒有被任何使用者訪問的話,就會自動被刪除。

  • 緩存

    Redis提供了鍵過期功能,因為靈活的鍵淘汰策略,Redis用在緩存的場合非常多。

  • 排行榜

    很多網站都有排行榜應用的,如月銷量榜單、商品的新排行榜等。Redis提供的有序集合數據類能實現各種複雜的排行榜應用。

  • 社交網絡

    點贊、關注/被關注、共同好友等是社交網站的基本功能,社交網站的訪問量通常來說比較大,而且傳統的SQL不適合存儲這種類型的數據,Redis提供的hash、set等數據結構能很方便的的實現這些功能

  • Week 11

  • 委派

    委派是一種類型,代表具有特定參數清單及傳回型別的方法參考。 當您具現化委派時,可以將其執行個體與任何具有相容簽章和傳回型別的方法產生關聯。委派是方法的傳遞,我們可以把方法交給委派,由委派來執行我們的方法,可使用泛型。

  • 宣告方式
  • // Declare a delegate.
    delegate void Del(string str);
    
    // Declare a method with the same signature as the delegate.
    static void Notify(string name)
    {
        Console.WriteLine($"Notification received for: {name}");
    }
    // Create an instance of the delegate.
    Del del1 = new Del(Notify);
    // C# 2.0 provides a simpler way to declare an instance of Del.
    Del del2 = Notify;
    // Instantiate Del by using an anonymous method.
    Del del3 = delegate(string name)
        { Console.WriteLine($"Notification received for: {name}"); };
    // Instantiate Del by using a lambda expression.
    Del del4 = name =>  { Console.WriteLine($"Notification received for: {name}"); };
    
    
  • 委派完全為物件導向
  • public class Program
    {
        public delegate string Reverse(string s);
    
        static string ReverseString(string s)
        {
            return new string(s.Reverse().ToArray());
        }
    
        static void Main(string[] args)
        {
            Reverse rev = ReverseString;
    
            Console.WriteLine(rev("a string"));
        }
    }
    
    
  • 委派允許將方法當做參數傳遞,也可用於定義回呼方法
  • public delegate void Del(string message);
    
    // Create a method for a delegate.
    public static void DelegateMethod(string message)
    {
        Console.WriteLine(message);
    }
    
    // Instantiate the delegate.
    Del handler = DelegateMethod;
    
    public static void MethodWithCallback(int param1, int param2, Del callback)
    {
        callback("The number is: " + (param1 + param2).ToString());
    }
    
    MethodWithCallback(1, 2, handler);
    
    //The number is: 3
    
    
  • 可將委派鏈結在一起,在單一事件上呼叫多個方法(多點傳送委派)當叫用 allMethodsDelegate 時,會依序呼叫所有三個方法。 參考參數會依序傳入這三個方法中的每一個,而且任一方法所做的任何變更,下一個方法都看得到。 當任一方法擲回未在該方法內攔截到例外狀況時,該例外狀況會傳遞至委派的呼叫端,且不會呼叫引動過程清單中的任何後續方法。 如果委派具有傳回值和 (或) 輸出參數,則它會傳回所叫用之最後一個方法的傳回值與參數。
  • public class MethodClass
    {
        public void Method1(string message) { }
        public void Method2(string message) { }
    }
    
    var obj = new MethodClass();
    Del d1 = obj.Method1;
    Del d2 = obj.Method2;
    Del d3 = DelegateMethod;
    
    //Both types of assignment are valid.
    Del allMethodsDelegate = d1 + d2;
    allMethodsDelegate += d3;
    
    //remove Method1
    allMethodsDelegate -= d1;
    
    // copy AllMethodsDelegate while removing d2
    Del oneMethodDelegate = allMethodsDelegate - d2;
    
    

    在委派中使用變異數

  • 共變數DogsHandler 所傳回的資料類型是 Dogs 類型,該類型衍生自定義於委派中的 Mammals 類型。
  • class Mammals {}
    class Dogs : Mammals {}
    
    class Program
    {
        // Define the delegate.
        public delegate Mammals HandlerMethod();
    
        public static Mammals MammalsHandler()
        {
            return null;
        }
    
        public static Dogs DogsHandler()
        {
            return null;
        }
    
        static void Test()
        {
            HandlerMethod handlerMammals = MammalsHandler;
    
            // Covariance enables this assignment.
            HandlerMethod handlerDogs = DogsHandler;
        }
    }
    
    
  • 反變數使用參數類型為委派簽章參數類型的基底類型方法來使用委派,EventArgs 同時是 KeyEventArgs和 MouseEventArgs 的基底類型
  • //KeyEventHandler 委派會定義 Button.KeyDown 事件的簽章。 其簽章為:
    public delegate void KeyEventHandler(object sender, KeyEventArgs e)
    
    //MouseEventHandler 委派會定義 Button.MouseClick 事件的簽章。 其簽章為:
    public delegate void MouseEventHandler(object sender, MouseEventArgs e)
    
    // Event handler that accepts a parameter of the EventArgs type.
    private void MultiHandler(object sender, System.EventArgs e)
    {
        label1.Text = System.DateTime.Now.ToString();
    }
    
    public Form1()
    {
        InitializeComponent();
    
        // You can use a method that has an EventArgs parameter,
        // although the event expects the KeyEventArgs parameter.
        this.button1.KeyDown += this.MultiHandler;
    
        // You can use the same method
        // for an event that expects the MouseEventArgs parameter.
        this.button1.MouseClick += this.MultiHandler;
    
    }
    
    

    FUNC<>與ACTION<>

    FUNC<>與ACTION<>本質都是委派(Delegate)。

    //Func 方法簽章
    public delegate TResult Func<out TResult>();
    //action 方法簽章
    public delegate void Action<in T>(T obj);
    
    
  • Action 所代表的是不傳回值的涵式(也就是void),<T1,T2> 代表傳入類型 若不傳入直接宣告Action即可
  • namespace ConsoleApp1
    {
        class Program
        {
            static Action<int,int> Calculate;
            static void Main()
            {
                Calculate = Add;
                Calculate(6, 5); //傳入型態為2個int 會執行 Add(5,6)
                //num1 + num2 = 11
                Calculate = Sub;
                Calculate(6, 5); //會執行 Sub(5,6)
                //num1 - num2 = 1
            }
    
            public static void Add(int num1, int num2)
            {
                Console.WriteLine($"num1 + num2 = {num1 + num2}");
            }
            public static void Sub(int num1, int num2)
            {
                Console.WriteLine($"num1 - num2 = {num1 - num2}");
            }
        }
    }
    
    
  • Func 則是會傳回值,我們會將回傳型態放在最後一項,<T1,T2,TResult> T1,T2代表傳入類型 TResult為回傳類型
  • namespace ConsoleApp1
    {
        class Program
        {
            private static Func<int,int,string> Calculate;
            static void Main()
            {
                Calculate = Add;
                Console.WriteLine(Calculate(6, 5)); //傳入型態為2個int 會執行 Add(5,6)
                //num1 + num2 = 11
                Calculate = Sub;
                Console.WriteLine(Calculate(6, 5)); //會執行 Sub(5,6)
                //num1 - num2 = 1
            }
    
            public static string Add(int num1, int num2)
            {
                return $"num1 + num2 = {num1 + num2}";
            }
            public static string Sub(int num1, int num2)
            {
                return $"num1 - num2 = {num1 - num2}";
            }
        }
    }
    
    

    Lambda 匿名函式

    λ(lambda)運算式其實是一種委派,不過他是匿名的委派,其特色在於無須定義函式名稱以及使用Lambda運算子 => 將左側的輸入參數與右側的Lambda主體分隔開來。

    Lambda 可以是下列兩種形式的任一個:

    以運算式作為主體的運算式 Lambda:

    委派的邏輯只需要一行就可以寫完,不需要用大括號把邏輯包起來

    (input-parameters) => expression
    
    

    以陳述式區塊作為主體的陳述式 Lambda:

    陳述式 Lambda 看起來就像是運算式 Lambda,不同之處在於,陳述式會包含於大括號內

    (input-parameters) => { <sequence-of-statements> }
    
    

    任何 Lambda 運算式可轉換成委派型別,Lambda 型別推斷的一般規則如下所示:

  • Lambda 必須包含與委派類型相同數目的參數。
  • Lambda 中的每個輸入參數都必須能夠隱含轉換為其對應的委派參數。
  • Lambda 的傳回值 (如果有的話) 必須能夠隱含轉換為委派的傳回類型。
  • Lambda 運算式中的擷取外部變數和變數範圍

    在定義Lambda的範圍內使用到外部變數會被擷取儲存加以使用,因此Lambda可以在運算時參考到外部變數,但是外部變數必須確實指派同時擁有權限才能用於Lambda。

    int y = 5;
    
    Func<int, int> func = x => x + y;
    
    Console.WriteLine(func(2));
    
    //Output=7
    
    

    下列規則適用於 Lambda 運算式中的變數範圍:

  • 在參考的變數變成可進行垃圾收集之前,將不會進行垃圾收集
  • Lambda 運算式無法直接從封入方法中抓取 in、ref 或 out 參數
  • Lambda 運算式中的 return 陳述式不會造成封入方法傳回
  • Lambda 運算式不能包含 goto、break 或 continue 語句
  • IEnumerable & IQueryable 的使用時機點, 什麼時候該用哪個?

    IEnumerable介面

    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }
    
    

    繼承此介面還要實現IEnumerator

    public interface IEnumerator
    {
        bool MoveNext();    //找下一筆資料
    
        object Current { get; }    //取得目前位置的資料
    
        void Reset();    //回到第一筆資料前面
    }
    
    

    .Net 對「是否可被列舉」這件事的定義上,就是認定是否具備取得「列舉器」(Enumerator) 的能力。所以具有可被列舉的特性, 就必須要繼承IEnumerable或IEnumerable<T>

    IQueryable

    繼承IEnumerable,兩者主要差別在於IQueryable多了IQueryProvider和Expression,可以產生操作資料庫的語法。

    public interface IQueryable : IEnumerable
    {
        Type ElementType { get; }    //資料類型
        Expression Expression { get; }    //程式語法(將CODE轉成物件)
        IQueryProvider Provider { get; }    //產生資料庫查詢語法
    }
    
    public interface IQueryable<out T> : IEnumerable<T>, IQueryable
    {
    }
    
    public interface IQueryProvider
    {
        IQueryable CreateQuery(Expression expression);    //產生SQL查詢語法
        IQueryable<TElement> CreateQuery<TElement>(Expression expression);
        object Execute(Expression expression);    //執行產生的語法
        TResult Execute<TResult>(Expression expression);
    }
    
    

    IEnumerable與IQueryable差異比較

    **IEnumerable:**取出當前符合條件的所有資料後,在記憶體中進行後續的資料篩選

    **IQueryable:**會保存所有查詢條件於Query Expression中,直到資料被列舉成實體資料的當下,才將最終的Query Expression透過Query Provider轉換為實際執行的SQL語法,從DB取出符合條件資料。

    以下兩行程式執行結果相同,但執行過程差異極大:

    var result = Table.AsEnumerable().Where(x=> x.ID == 1).ToList();
    var result = Table.AsQueryable().Where(x=> x.ID == 1).ToList();
    
    

    IEnumerable會在ToList的時候先取出Table的所有資料存到記憶體中,再去從記憶體中的資料下條件式(where)取出也就是假如Table有10萬筆資料,他會先把10萬筆存到記憶體中,再去篩選ID=1得到的結果

    IQueryable會在ToList的時候,會包含WHERE條件式一起送給SQL SERVER取要得資料也就是只把SQL SERVER篩選過後,符合條件的資料傳給記憶體

    以下比較不同順序使用Where查詢之後,IQueryProvider的輸出差別:

    //db為:DbSet<T>類別,實現IQueryable介面
    // part1
    var query1 = db.Orders.Where(x => x.OrderId != 5);
    var query2 = query1.AsEnumerable();
    var query3 = query2.AsQueryable();
    var list = query3.Where(x => x.CreateDate < DateTime.Now).ToList();
    
    // part2
    var query1 = db.Orders.Where(x => x.OrderId != 5);
    var query2 = query1.AsEnumerable();
    var query3 = query2.Where(x=>x.CreateDate < DateTime.Now).AsQueryable();
    var list = query3.ToList();
    
    

    IQueryProvider 輸出的SQL指令:

    /*part 1*/
    SELECT [x].[OrderId], [x].[CreateDate]
    FROM [Orders] AS [x]
    WHERE ([x].[OrderId] <> 5) AND ([x].[CreateDate] < GETDATE());
    
    /*part 2*/
    SELECT [x].[OrderId], [x].[CreateDate]
    FROM [Orders] AS [x]
    WHERE [x].[OrderId] <> 5;
    
    

    不同點在assign給query3時

    part1 query2透過AsQueryable()轉為IQueryable, 在指定給list時呼叫IQueryable的Where, 呼叫ToList()時會執行 GetEnumerator(),

    part2 query2呼叫Where(此為IEnumerable的方法, 此時會直接發請求給db取得資料放至記憶體, 再加入x.CreateDate < DateTime.Now的條件), 再轉為IQueryable

    適用時機:

    IEnumerable: 處理記憶體內的資料 ex. List / ArrayIQueryable: 處理遠端來源的資料 ex. Database / Service

    EX:全台灣店家資料查詢服務,常被查詢的地區先用IQueryable載入記憶體,搜尋請求時再IEnumerable細項查詢,不常被查詢的有請求再IQueryable從SQL找就好

    Expression

    簡單來說,用途就是把程式碼轉為物件處理。把程式碼轉換成樹狀結構的運算式資料之後,便可以對這些運算式進行修改,然後等到要執行運算式時,只要呼叫 Expression<T> 的 Compile 方法,就能夠將運算式資料結構逆向轉為程式碼(或匿名函式Lambda),並呼叫之。

    例如有段lambda如下:

    Func<int, int> fn = n => n * n;
    Console.WriteLine(fn(10));    // 印出 100.
    
    

    轉換成Expression就是:

    Expression<Func<int, int>> expr = n => n * n;  // 將運算式轉換成樹狀資料結構
    Func<int, int> fn = expr.Compile();  // 將樹狀結構逆向轉為程式碼,並存入委派物件.
    Console.WriteLine(fn(10));    // 印出 100.
    
    

    直接以Expression實現就是:

    ParameterExpression pe = Expression.Parameter(typeof(int), "n");    //int變數n
    Expression<Func<int, int>> expr =    //委派
        Expression<Func<int, int>>.Lambda<Func<int, int>>(    //Lambda
            Expression.Multiply(pe, pe),    //二元運算子-乘號,建立 BinaryExpression 的物件實體。
    //這是一種工廠方法(factory method)  的寫法──你無法利用 new 來建立 BinaryExpression 實體,而必須藉由特定的工廠方法來建立物件實體。
            new ParameterExpression[] { pe }    //回傳值
        );
    Console.WriteLine(expr.Compile()(10));  // 印出 100.
    
    

    結構如下圖:

    https://i.imgur.com/wNtFFC7.jpg

    實際應用參考: https://www.tpisoftware.com/tpu/articleDetails/2229

    延遲執行 Deferred Execution

    顧名思義,就是在真正需要取用查詢結果的時候,才去執行查詢表示式。

    一般的情況,程式執行到哪一行,運算式應該就會立刻被執行,延遲執行的意義,就是等到實際驅動點時,才會執行原本該執行的程式。

    延遲執行是 LINQ 的重點技術之一,對於像是會存取資料庫的 Framework 或指令,如果在指令下的當下就執行的話,有可能會在下個指令存取之前就跑了,這樣可能會有時間差,或是下一個指令無法反應到結果上的問題,因爲 LINQ 把查詢的創建與查詢的執行解耦,這讓才可以像創建 SQL 查詢那樣,分成多個步驟來創建 LINQ 查詢

    誰是查詢的發動者?

    int[] arr1 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    var query = from i in (from i in arr1
                           where i % 2 == 0
                           select i)
                where i > 5
                select i;
    
    foreach (var item in query.Where((i) => i % 2 == 0))
    {
        Console.WriteLine(item);
    }
    
    

    實作 IEnumerable & IQueryable 來測試

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace DefrredExecutionPrototype
    {
        public interface IQueryable<T>{
            T First();
            T Last();
            IEnumerable<T> Where(Func<T, bool> WhereClause);
            IEnumerable<T> ToList();
        }
    }
    
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace DefrredExecutionPrototype
    {
        public class Queryable<T> : IQueryable<T>{
            private IEnumerable<T> _list = null;
    
            public Queryable(IEnumerable<T> InitialList)
            {
                this._list = InitialList;
            }
    
            public T First()
            {
                IEnumerator<T> cursor = this._list.GetEnumerator();
                int offset = 0;
    
                if (this._list.Count() == 0)
                    return default(T);
    
                while (cursor.MoveNext())
                {
                    if (offset == 0)
                        return cursor.Current;
                }
    
                return default(T);
            }
    
            public T Last()
            {
                IEnumerator<T> cursor = this._list.GetEnumerator();
                int offset = 0;
    
                while (cursor.MoveNext())
                {
                    if (offset == this._list.Count() - 1)
                        return cursor.Current;
    
                    offset++;
                }
    
                return default(T);
            }
    
            public IEnumerable<T> Where(Func<T, bool> WhereClause)
            {
                if (WhereClause == null)
                    throw new ArgumentNullException("WhereClause", "WHERE clause cannot be null.");
    
                IEnumerator<T> cursor = this._list.GetEnumerator();
                List<T> itemStore = new List<T>();
    
                if (this._list.Count() == 0)
                    throw new InvalidOperationException("List cannot empty.");
    
                return this.WhereImpl(WhereClause);
            }
    
            private IEnumerable<T> WhereImpl(Func<T, bool> WhereClause)
            {
                foreach (var item in this._list)
                {
                    Console.WriteLine("Invoke query item: {0}", item);
    
                    if (WhereClause(item))
                        yield return item;
                }
            }
    
            public IEnumerable<T> ToList()
            {
                IEnumerator<T> cursor = this._list.GetEnumerator();
                List<T> itemStore = new List<T>();
    
                if (this._list.Count() == 0)
                    return new List<T>();
    
                while (cursor.MoveNext())
                {
                    itemStore.Add(cursor.Current);
                }
    
                return itemStore;
            }
        }
    }
    
    

    實際使用

    List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
    IQueryable<int> query = new Queryable<int>(list);
    
    Console.WriteLine("first: {0}, last: {1}", query.First(), query.Last());
    
    foreach (var item in query.Where((i) => i % 2 == 0))
    {
        Console.WriteLine(item);
    }
    
    Console.ReadLine();
    
    

    https://i.imgur.com/0Ck4Hy4.png

    LINQ 在呼叫 select 時,還是沒有引發查詢動作,真正引發查詢動作會是在 foreach,也就是 IEnumerator<T>.MoveNext(),而這個方法會被由編譯器產生的狀態機類別處理,因此開發人員通常不會感覺到

    延遲執行的運作範例

    private static void Main(string[] args)
    {
        var people = GetPeople();
        var names = people
            .Where(person => person.Age > 18)
            .Select(person => person.Name);
    
        foreach (var name in names)
        {
            Console.WriteLine(name);
        }
    }
    
    private static IEnumerable<Person> GetPeople()
    {
        yield return new Person { Id = 1, Name = "John", Age = 36 };
        yield return new Person { Id = 2, Name = "Bill", Age = 6 };
        yield return new Person { Id = 3, Name = "Steve", Age = 23 };
    }
    
    

    利用偵錯看執行順序

  • 第一行的 var people = GetPeople();不會先跳 GetPeople() 功能而是會跳到下一行的var names = people.Where().Select();

    https://i.imgur.com/kAs9Kml.png

    因為 GetPeople() 中有 yield 關鍵字,因此 GetPeople() 實際執行的時間點,會在某個 iterator 的 MoveNext() 中,需要實際用到 GetPeople() 中的值時,才會執行。

  • 下一步則到 foreach(var name in names) 這一行還是不會跳到 GetPeople() 功能

    https://i.imgur.com/DTDi1tl.png

    原因與 GetPeople() 沒有被馬上執行相同,因為 Where() 的方法內容中有 yield 關鍵字,而 Select() 方法內容中,也有 yield 關鍵字。因此這一行並不會馬上執行

  • 逐步偵錯的過程中,會先停在 foreach (var name in names) 的 in 上,接下來進去 Select() 的方法,再接著進到Where(),然後才到 GetPeople()

    https://i.imgur.com/nVFAECj.png

    foreach 的 in ,指的是 names 的 GetEnumerator() 後的 MoveNext() , names 要 MoveNext() 就得從 names 來源開始執行

    names 是怎麼來的? names 是 people.Where().Select() 的結果而來的

    所以要巡覽 names ,需要先知道 Select() 的結果是什麼。因為延遲執行,所以 names 的 MoveNext() 時才會呼叫 Select()

    https://i.imgur.com/zTeAW4M.png

    這時候 Select() 的 IEnumerable<TSource> source 怎麼來的? Select() 方法中的 source 是 people.Where() 的結果。所以,當我們在 Select() 方法中,呼叫 source.GetEnumerator().MoveNext() 時,就會實際去執行 Where() 的方法內容,因為需要取得 Where() 的結果了,就會進入 GetPeoploe() 的 yield return new Person ,並取得第一個 Person

    https://i.imgur.com/9KakvLH.png

  • 接下來就依照順序來查詢

    https://i.imgur.com/34JrZYE.png

  • 實際的執行順序

    https://i.imgur.com/n2hnleJ.png

    延遲執行的實現

    是因為使用 yield return 的 IEnumerable<T> 列舉進行迭代,例如:

     public static IEnumerable<string> getString()
            {
                for (int i = 0; i < 10; i++)
                {
                    yield return "s" + i;
                }
            }
    
    

    根據微軟文件:

    在陳述式中使用 yield 內容關鍵字時,您會指出關鍵字所在的方法、運算子或 get 存取子是迭代器。 如果使用 yield 定義迭代器,當您為自訂集合類型實作 IEnumerator<T> 和 IEnumerable 模式時,就不需要明確的額外類別。

  • 如果一個區塊(block)中有 yield 陳述式,此區塊就叫做 Iterator Block
  • 一個方法的區塊如果是 Iterator Block,則它的回傳值會是 IEnumerable、IEnumerator。
  • 從迭代器方法傳回的序列,可以透過使用 foreach 陳述式或 LINQ 查詢來取用。 foreach 迴圈的每個反覆項目都會呼叫 Iterator 方法。 當 Iterator 方法中到達 yield return 陳述式時,就會傳回 expression 並保留程式碼中目前的位置。 下一次呼叫 Iterator 函式時,便會從這個位置重新開始執行。

    實務上會遇見的問題

    https://i.imgur.com/BrrYwxX.png

    因為延遲的因素,不會正常報錯

    https://i.imgur.com/KutEilF.png

    LINQ 有延遲執行的特性,在 Select() 的當下程式還不會執行,要等 特定函式執行才會開始跑迴圈

    LINQ 運算子種類

    立即執行運算子 Immediately execution

  • 返回單個元素或者標量值的查詢運算子,如First、Count、Sum等。
  • 下面這些轉換運算子:ToArray、ToList、ToDictionary、ToLookup。
  • 上面兩種運算符會被立即執行,因爲他們的返回值類型沒有提供延遲執行的機制。

    延後執行運算子 Deferred Execution

    分成兩類:資料流和非資料流(Streaming or Non-Streaming)

  • 資料流: 每次列舉時,只取出一個項目 e.g. GroupBy, OrderBy, Reverse…
  • 非資料流: 必須先把所有資料都取出來才能產生結果 e.g. Select, Union, Where…
  • 詳細運算子資料: https://reurl.cc/px2XgZ

    效能討論

    LINQ 延遲查詢特性,會在 foreach 每次都查詢一次,所以如果需要用到 foreach 的話,先 ToList 後再去 foreach 效能比較好,因為並不會每次都去重新查詢。當然ToList轉換一定也有他的效能消耗,所以要判斷在哪邊使用,例如在迴圈中使用會消耗額外效能

    什麼是Iterator設計模式?

    一、設計模式 design pattern 起源

    什麼是設計模式 ? (By Wiki)

    在軟體工程中,設計模式(design Pattern)是對軟體設計中普遍存在(反覆出現)的各種問題,所提出的解決方案。這個術語是由埃里希·伽瑪(Erich Gamma)等人在1990年代從建築設計領域引入到電腦科學的。

    設計模式並不直接用來完成程式碼的編寫,而是描述在各種不同情況下,要怎麼解決問題的一種方案。物件導向設計模式通常以類別或物件來描述其中的關係和相互作用,但不涉及用來完成應用程式的特定類別或物件。設計模式能使不穩定依賴於相對穩定、具體依賴於相對抽象,避免會引起麻煩的緊耦合,以增強軟體設計面對並適應變化的能力。

    簡言之:奠基在物件導向原則,結合前人經驗,解決軟體設計上常見的問題。

    例子: 單例模式(Singleton Pattern)解決單一實體的需求、創建者模式(Builder Pattern)解決建構分離的需求等等

    二、迭代器Iterator

    可以讓你巡覽集合內所有元素的類別

    foreach (V v in x)
    {
    embedded_statement
    }
    
    

    為什麼需要?

    可維護性!!

    1.讓巡覽和計算邏輯分工

    2.使用者隔離於內部結構

    for (int currentNum = 1; currentNum <= maxNum; currentNum++)
    {
        Console.WriteLine($"{currentNum} ");
    }
    
    
    for (int currentNum = 1; currentNum <= maxNum; currentNum++)
    {    if(currentNum % 2 == 0)
        {
            Console.WriteLine($"{currentNum} ");
        }
    }
    
    

    不符合單一職責原則(Single-responsibility principle)

    透過迭代器將巡覽功能拉出來

    https://i.imgur.com/nbcL6YA.png

    三、Iterator Pattern如何使用

    以C#為例

    IEnumrable

    public interface IEnumerable
    
    {
    
        IEnumerator GetEnumerator();
    
    }
    
    

    IEnumrator

    public interface IEnumerator
    
    {
    
        bool MoveNext();
    
        object Current { get; }
    
        void Reset();
    
    }
    
    

    我們就可以透過此介面實作Iterator,來達到我們想要的巡覽效果 舉例來說

    private class integersInterator : IEnumerator
    {
        private int _maxNum;
        private int _divide;
        private int currentNum = 1;
    
        public integersInterator(int maxNum, int divide)
        {
            _maxNum = maxNum;
            _divide = divide;
        }
    
        public object Current { get; private set; }
    
        public bool MoveNext() ///實作如何巡覽的條件
        {
            do
            {
                if (currentNum % _divide == 0)
                {
                    Current = currentNum;
                    return true;
                }
                currentNum++;
            } while (currentNum <= _maxNum);
            return false;
        }
    
        public void Reset()
        {
            currentNum = 1;
        }
    }
    
    

    我們可以自己決定集合內部該用什麼方式來巡覽

    所以當我們在使用foreach的時候,其實程式本身是在做下面這件事

    foreach (V v in x) embedded_statement
    它會被擴充為:
    
    {
        E e = ((C)(x)).GetEnumerator();
        try {
            while (e.MoveNext()) {
                V v = (V)(T)e.Current;
                embedded_statement
            }
        }
        finally {
            ... // Dispose e
        }
    }
    
    

    四、Enumerable 在LINQ

    LINQ to Objects

    LINQ to Objects 表示要透過 LINQ 與集合型態的物件進行互動。而這些物件都是存在於記憶體當中。

    前面介紹過的IEnumerable,Provider就是利用這個介面去對任何要查詢的物件做處理,只要你的物件有實作IEnumerable這個物件,你就可以使用LINQ的Enumrable類別的方法。

    System.Linq.Enumerable <方法> Aggregate<TSource,TAccumulate,TResult>(IEnumerable<TSource>, TAccumulate, Func<TAccumulate,TSource,TAccumulate>, Func<TAccumulate,TResult>) 將累加函式套用到序列上。 使用指定的值做為初始累加值,並使用指定的函式來選取結果值。 All<TSource>(IEnumerable<TSource>, Func<TSource,Boolean>) 判斷序列的所有項目是否全都符合條件。 Any<TSource>(IEnumerable<TSource>) 判斷序列是否包含任何項目。 Append<TSource>(IEnumerable<TSource>, TSource) 將值附加在序列結尾。 AsEnumerable<TSource>(IEnumerable<TSource>) 傳回 IEnumerable<T> 類型的輸入。 Average(IEnumerable<Decimal>) 計算 Decimal 值序列的平均值。 Contains<TSource>(IEnumerable<TSource>, TSource) 使用預設的相等比較子 (Comparer) 來判斷序列是否包含指定的項目。 . . .

    五、補充語法糖 Yield

    Yield 就是 .Net 中用來實作 iterator(迭代器) 設計模式的語法糖

    yield實現的功能

    只要在實作IEnumerable<T>的類別中加入Yield return和 Yield break的邏輯

    yield return:

    先看下面的程式碼,通過yield return實現了類似用foreach遍歷陣列的功能,說明yield return也是用來實現迭代器的功能的。

    sing static System.Console;
    using System.Collections.Generic;
    
    class Program
    {
        //一個返回型別為IEnumerable<int>,其中包含三個yield return
        public static IEnumerable<int> enumerableFuc()
        {
            yield return 1;
            yield return 2;
            yield return 3;
        }
    
        static void Main(string[] args)
        {
            //通過foreach迴圈迭代此函式
            foreach(int item in enumerableFuc())
            {
                WriteLine(item);
            }
            ReadKey();
        }
    }
    
    輸出結果:
    1
    2
    3
    
    

    yield break:

    再看下面的程式碼,只輸出了1,2,沒有輸出3,說明這個迭代器被yield break停掉了,所以yield break是用來終止迭代的。

    using static System.Console;
    using System.Collections.Generic;
    class Program
    {
        //一個返回型別為IEnumerable<int>,其中包含三個yield return
        public static IEnumerable<int> enumerableFuc()
        {
            yield return 1;
            yield return 2;
            yield break;
            yield return 3;
        }
    
        static void Main(string[] args)
        {
            //通過foreach迴圈迭代此函式
            foreach(int item in enumerableFuc())
            {
                WriteLine(item);
            }
            ReadKey();
        }
    }
    
    輸出結果:
    1
    2
    
    

    https://raw.githubusercontent.com/peterhpchen/DigDeeperLINQ/develop/image/05_yield/yieldBreakPoint.gif

    遇到yield return就先暫停+回傳 再次觸發時會從原本的地方繼續執行

    原理

    自動實作IEnumrator

    編譯器自動產生的 IEnumerator 衍生類別

    [CompilerGenerated]
    private sealed class <YieldReturnSample3>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
    {
        // Fields
        private int <>1__state;
        private int <>2__current;
        public int <>3__end;
        public int <>3__start;
        private int <>l__initialThreadId;
        public int <current>5__1;
        public bool <match>5__2;
        public int end;
        public int start;
    
        // Methods
        [DebuggerHidden]
        public <YieldReturnSample3>d__0(int <>1__state)
        {
            this.<>1__state = <>1__state;
            this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
        }
    
        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<current>5__1 = 1;
                    while (this.<current>5__1 <= 100)
                    {
                        this.<match>5__2 = false;
                        if ((this.<current>5__1 % 2) == 0)
                        {
                            this.<match>5__2 = true;
                        }
                        if ((this.<current>5__1 % 3) == 0)
                        {
                            this.<match>5__2 = true;
                        }
                        if (!this.<match>5__2)
                        {
                            goto Label_0098;
                        }
                        this.<>2__current = this.<current>5__1;
                        this.<>1__state = 1;
                        return true;
                    Label_0090:
                        this.<>1__state = -1;
                    Label_0098:
                        this.<current>5__1++;
                    }
                    break;
    
                case 1:
                    goto Label_0090;
            }
            return false;
        }
    
        [DebuggerHidden]
        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
            Program.<YieldReturnSample3>d__0 d__;
            if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
            {
                this.<>1__state = 0;
                d__ = this;
            }
            else
            {
                d__ = new Program.<YieldReturnSample3>d__0(0);
            }
            d__.start = this.<>3__start;
            d__.end = this.<>3__end;
            return d__;
        }
    
        [DebuggerHidden]
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
        }
    
        [DebuggerHidden]
        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }
    
        void IDisposable.Dispose()
        {
        }
    
        // Properties
        int IEnumerator<int>.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }
    
        object IEnumerator.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }
    }
    

    查詢方法的執行順序

    一、標準查詢運算子執行時間

  • “標準查詢運算子”是組成查詢語言(LINQ) 的方法,大多數這些方法都在序列上運行
  • 序列是一個物件,其型別實作了IEnumerable<T>介面或 IQueryable<T> 介面
  • 各個標準查詢運算子在執行時間上有所不同,具體情況取決於回傳單一值還是值序列
  • 二、執行順序

    回傳單一值的方法(例如Average和Sum)會立即執行,回傳序列的方法會在遍歷的時候執行

    擷取.JPG

  • 立即執行

    立即執行意味著程式執行到哪一行,運算式就會立刻被執行

    下面兩種查詢運算子是立即執行,其他的運算子都是延遲執行:

    • 返回單個元素或者標量值的查詢運算子,如First、Count等。

    • 轉換運算子:ToArray、ToList、ToDictionary

    • 舉例:返回單個元素Start With

      • Start With(String)
      string[] StrDB = { "Puma", "BlueShop", "Microsoft", "Linq", "Ling", "Luma" };
      
      IEnumerable<string> StrSelete = from str in StrDB
                                      where str.StartsWith("Pu")
                                      orderby str
                                      select str;
      
      • Contains、Average、Sum、Count、MAX、MIN大家所熟知的子句也都是立即執行
    • 舉例:轉換運算子To Array

      List<string> names_list = new List<string> { "Puma", "BlueShop", "Microsoft", "Linq", "Ling", "Luma" };
      string[] takenames_arry = names_list.ToArray();
      
      string[] takenames_arry2 = (from name in names_list
                                  select name).Take(4).ToArray();
      foreach (var name in takenames_arry2)
      {
          Console.WriteLine(name);
      }
      
  • 延遲執行(1. 資料流 Streaming 2. 非資料流 Non-Streaming)

    • 不在代碼中宣告查詢的位置執行運算
    • 查詢變數進行列舉操作時才執行運算,例如使用 foreach
    • 多次列舉查詢變數,則每次結果可能都不同
    • 回傳型別為IEnumerable<T>或IOrderedEnumerable<TElement>的標準查詢運算子幾乎都以延遲方式執行
    static void TestDeferredExecution()
            {
    var numbers = new List<int>();
                numbers.Add(1);
                IEnumerable<int> query = numbers.Select(n => n * 10);   // Build query
     
                numbers.Add(2);                 // Add an extra element after the query
                foreach (int n in query)
                    Console.Write(n + "|");     // 10|20|
            }
    
    1. 資料流 Streaming

      • 運算子不需要在生成元素前讀取所有源資料

      • 在執行時,資料流運算子一邊讀取每個源元素,一邊對該源元素執行運算在可行時生成元素,資料流運算子將持續讀取源元素直到可以生成結果元素

      • 舉例:Order by

        class Pet
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }
        
        public static void OrderByEx1()
        {
            Pet[] pets = { new Pet { Name="Barley", Age=8 },
                           new Pet { Name="Boots", Age=4 },
                           new Pet { Name="Whiskers", Age=1 } };
        
            IEnumerable<Pet> query = pets.OrderBy(pet => pet.Age);
        
            foreach (Pet pet in query)
            {
                Console.WriteLine("{0} - {1}", pet.Name, pet.Age);
            }
        }
        
        /*
         This code produces the following output:
        
         Whiskers - 1
         Boots - 4
         Barley - 8
        */
        
    2. 非資料流 Non-Streaming

      • 排序和分組運算子屬于此類

      • 在執行時,非資料流查詢運算子讀取所有源資料,將其放入資料結構中,執行運算然后生成結果元素

      • 舉例:Concat

        class Pet
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }
        
        static Pet[] GetCats()
        {
            Pet[] cats = { new Pet { Name="Barley", Age=8 },
                           new Pet { Name="Boots", Age=4 },
                           new Pet { Name="Whiskers", Age=1 } };
            return cats;
        }
        
        static Pet[] GetDogs()
        {
            Pet[] dogs = { new Pet { Name="Bounder", Age=3 },
                           new Pet { Name="Snoopy", Age=14 },
                           new Pet { Name="Fido", Age=9 } };
            return dogs;
        }
        
        public static void ConcatEx1()
        {
            Pet[] cats = GetCats();
            Pet[] dogs = GetDogs();
        
            IEnumerable<string> query =
                cats.Select(cat => cat.Name).Concat(dogs.Select(dog => dog.Name));
        
            foreach (string name in query)
            {
                Console.WriteLine(name);
            }
        }
        
        // This code produces the following output:
        //
        // Barley
        // Boots
        // Whiskers
        // Bounder
        // Snoopy
        // Fido
        

  • Race Condition, Deadlock 介紹

    零、從何而來?

    Program

    Process

    Thread

    一、競爭條件(同步存取)

    多個任務取用相同資源

    競爭條件舉例:帳戶只有1000,但可以買下7-11

    處理競爭條件方法

  • 上鎖
  • 單一查詢 ATOMIC
  • 互斥鎖(Mutex)(睡眠等待)(鎖定呼叫端)

    當一個線程嘗試獲取互斥鎖,如果互斥鎖已經被占用則該線程會被掛起進入睡眠狀態,直到被喚醒。線程被掛起時,CPU會將該線程當前的處理狀態保存到內存中,等到喚醒時從內存中讀取上次的處理狀態,這個CPU切換線程處理狀態的過程被稱為「上下文切換」

    適合情境:生命週期長(減少上下文切換)且穩定的Code(避免accidentally release 不正常解鎖)

    using System;
    using System.Threading;
    
    public class Program
    {
        private static Mutex mut = new Mutex();
    
        public static void Main()
        {
            for (int i = 0; i < 2; i++)
            {
                UseResource();
            }
        }
    
        // This method represents a resource that must be synchronized
        // so that only one thread at a time can enter.
        private static void UseResource()
        {
            // Wait until it is safe to enter.
            mut.WaitOne();
    
            Console.WriteLine("{0} has entered the protected area",
                Thread.CurrentThread.Name);
    
            // Place code to access non-reentrant resources here.
    
            // Simulate some work.
            Thread.Sleep(500);
    
            Console.WriteLine("{0} is leaving the protected area\\r\\n",
                Thread.CurrentThread.Name);
    
            // Release the Mutex.
            mut.ReleaseMutex();
        }
    }
    

    C#:Mutex只能互斥執行緒間的呼叫,但是不能互斥本執行緒的重複呼叫

    臨界區(Critical Section)(是一個協議或方法)

    三大條件

  • 互斥鎖(Mutex)

    任一時間點,只允許一個 process 進入他自已的 critical section 內活動。

  • 進程(Progress)

    • 不想進入 critical section 的 process 不可以阻礙其它 process 進入 critical section,即不可參與進入 critical section 的決策過程。
    • 必須在有限的時間從想進入 critical section 的 process 中,挑選其中一個 process 進入 critical section,隱含No Deadlock
      • tips: 有空位時讓「想進的人」「馬上」進入。
  • 有限等待(Bounded Wait)

    (no starvation)

  • 讀寫鎖(ReadWriteLock) 欠實作

    讀取鎖可以多執行緒取用,寫入鎖不可以被多執行緒使用,同時也是一種混和鎖

    C# Lock(鎖定被呼叫端)

    同步處理多個執行緒的活動。

    請使用 MutexManualResetEventAutoResetEvent 和 Monitor

    Mutex(互斥鎖):

    必須由同一個Thread解鎖,可以避免優先權倒置

    Semaphore(號誌):

  • Binary Semaphore
  • Semaphore
  • Recursive Mutex
  • 限制同時存取的Thread數量、使用Signal的up,down達到notification的功能,主要用在同步多執行緒

    P(減少)V(增加)操作

    造成死結、飢餓(一直拿不到需要的資源)

    優先權倒置問題:有三個行程(其優先權從高到低分別為T1、T2、T3),有一個臨界資源CS(T1與T3會用到)。這時,T3先執行,取得了臨界資源CS。然後T2打斷T3。接著T1打斷T2,但由於CS已被T3取得,因此T1被阻塞,這樣T2獲得時間片。直到T2執行完畢後,T3接著執行,其釋放CS後,T1才能取得CS並執行。這時,我們看T1與T2,雖然T1優先權比T2高,但實際上T2優先於T1執行。這稱之為優先權逆轉。

    二、死結

    預防

    a.禁止被搶佔:不得被其他行程中斷 b.等待時可持有資源 c.資源互斥:資源不得同時被使用 d.循環等待:兩個行程互等

    補.活結:兩執行緒互相禮讓,導致仍無法執行

  • 簡易死結實現(執行緒完成前佔有資源)

    class Program
        {
    				//A,B必定要是參考型別,且static
            static StringBuilder A = new StringBuilder("A");
            static StringBuilder B = new StringBuilder("B");
    
            public static void Main(String[] args)
            {
                Thread thread1 = new Thread(new ThreadStart(AB));
                Thread thread2 = new Thread(new ThreadStart(BA));
                thread1.Start();
                thread2.Start();
            }
    
            public static void AB()
            {
                Console.WriteLine("AB start");
                lock (A)
                {
                    Console.WriteLine("AB.lock(A)");
                    lock (B)
                    {
                        Console.WriteLine("AB.lock(B)");
                        Console.WriteLine("AB release B");
                    }
                    Console.WriteLine("AB release A");
                }
            }
    
            public static void BA()
            {
                lock (B)
                {
                    Console.WriteLine("BA.lock(B)");
                    lock (A)
                    {
                        Console.WriteLine("BA.lock(A)");
                        Console.WriteLine("BA release A");
                    }
                    Console.WriteLine("BA release B");
                }
            }
        }
    
    //情況一
    // AB start
    // AB.lock(A)
    // BA.lock(B)
    
    //情況二
    // BA.lock(B)
    // AB start
    // BA.lock(A)
    // BA release A
    // BA release B
    // AB.lock(A)
    // AB.lock(B)
    // AB release B
    // AB release A
    
  • 檢測死結方法

  • 自旋鎖頭(Spin Lock) (自旋等待)

    自旋鎖不會觸發睡眠狀態,而是持續檢查鎖是否可用。

    適合情境:用在持有鎖的線程很快就釋放,並且線程競爭不激烈的狀態

    class Program
        {
            static void Main(string[] args)
            {
                var count = 0;
                var taskList = new Task[10];
                Stopwatch sp = new Stopwatch();
                sp.Start();
    
                // 不要意外复制。每个实例都是独立的。
                SpinLock _spinLock = new SpinLock();
                for (int i = 0; i < taskList.Length; i++)
                {
                    taskList[i] = Task.Run(() =>
                    {
                        bool _lock = false;
                        for (int j = 0; j < 10_000_000; j++)
                        {
                            _spinLock.Enter(ref _lock);
                            count++;
                            _spinLock.Exit();
                            _lock = false;
                        }
                    });
                }
    
                sp.Stop();
                Task.WaitAll(taskList);
                Console.WriteLine($"完成! 耗时:{sp.ElapsedTicks}");
                Console.WriteLine($"结果:{count}");
            }
        }
    

    遞迴互斥鎖(Recursive Mutex)

    監視器**(Monitor) (鎖定被呼叫端)**

    using System;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace LinqPractice
    {
        class Program
        {
            public static void Main()
            {
                MyClass myClass = new MyClass();
                bool acquiredLock = false;
                try
                {
                    Monitor.TryEnter(myClass, 500, ref acquiredLock);
                    if (acquiredLock)
                    {
    
                        // Code that accesses resources that are protected by the lock.
                    }
                    else
                    {
    
                        // Code to deal with the fact that the lock was not acquired.
                    }
                }
                finally
                {
                    if (acquiredLock)
                    {
                        Monitor.Exit(myClass);
                    }
                }
            }
        }
        internal class MyClass { }
    }
    

    先使用自旋鎖

    混和鎖**(AutoResetEvent)**

    先使用SpinWait再做Mutex

Week12

一、控制反轉IOC&依賴注入DI 留言

前言

五個物件導向的設計原則 S.O.L.I.D.

https://i.imgur.com/vrAyJa6.png

依賴反轉原則 (Dependency-Inversion Principle, DIP)

方法應該依賴抽象而非實例

依賴

 /// <summary>
    /// 河水
    /// </summary>
    public class RiverWater
    {
        public void GiveNutrition()
        {
            Console.WriteLine("我是河水,我給小魚提供營養。");
        }
    }
    /// <summary>
    /// 魚
    /// </summary>
    public class Fish
    {
        private RiverWater riverWater;
        public void Live()
        {
            riverWater = new RiverWater();
            riverWater.GiveNutrition();
        }
    }

魚(高階模組)依賴河水(低階模組)

目的

『 解除高階模組 (Caller 呼叫者) 與低階模組 (Callee 被呼叫者)的耦合關係,使高階模組不再直接依賴低階模組,依賴關係被顛倒(反轉),從而使得低階模組依賴於高階模組的需求抽象。』

原則

  1. 高階的模組不應該依賴於低階的模組,兩者都應該依賴於抽象介面。
  2. 抽象不應該依賴細節;細節應該依賴抽象。

高階模組 → 低階模組====>高階模組 (提出需求)→ 抽象介面 ← (實現功能)低階模組

public interface Water
    {
        public void GiveNutrition();
    }

    /// <summary>
    /// 河水
    /// </summary>
    public class RiverWater : Water
    {
        public override void GiveNutrition()
        {
            Console.WriteLine("河水-提供營養。");
        }
    }
    /// <summary>
    /// 井水
    /// </summary>
    public class WellWater : Water
    {
        public override void GiveNutrition()
        {
            Console.WriteLine("井水-提供營養。");
        }
    }

Water是一個抽象介面

 /// <summary>
    /// 魚
    /// </summary>
    public class Fish
    {
        private Water water;
        public Fish()
        {
            water = new LakeWater();
        }
        public void Live()
        {
            Console.WriteLine("個人生活靠:");
            warter.GiveNutrition();
        }
    }

魚提出需求 => 水(介面) <= 河水、井水、海水實現具體功能

什麼是控制反轉(IOC)?

倒轉依賴物件實例的『控制流程』。

上面的例子可以看到,魚還是控制了水的實現,仍有某種程度的依賴因此將水的實現反轉到外部來做

/// <summary>
    /// 魚
    /// </summary>
    public class Fish
    {
        private Water water;
        public Fish(Water _water)
        {
            water = _water;
        }
        public void Live()
        {
            Console.WriteLine("個人生活靠:");
            water.GiveNutrition();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Water water = new RiverWater();
            Fish fish = new Fish(water);
            fish.Live();
            Console.ReadKey();
        }
    }

  1. 原本Fish類控制著Water類
  2. 建立權力移轉給Program的Main方法
  3. Fish只關心提供的功能

Program 就是一個 IOC容器

為什麼需要?

  • 解耦性
    • 避免動一髮牽全身
    • 易於測試
      • 只需要改動單一物件
    • 快速開發
      • 新增功能很快

如何達成IOC

1.依賴注入(Dependency Injection) 2.依賴尋找(Dependency Lookup)

前者是被動的接收物件,後者是主動索取相應類型的物件

什麼是依賴注入(DI)

https://i.imgur.com/AGGmcVI.png

將所需的依賴實例(外部對象、資源、常數),注入到更高階的模組中。

無須關注依賴的資源從何而來、由誰實現、如何實現的

實現DI方式

  • 建構元注入 (Constructor Injection)
public class Fish
    {
        private Water water;
        public Fish(Water _water)
        {
            water = _water;
        }
        public void Live()
        {
            Console.WriteLine("個人生活靠:");
            water.GiveNutrition();
        }
    }

優:一開始便確定了依賴關係缺:後期無法更動

  • 設值方法注入 (Setter Injection)
public class Fish
    {
        private Water water;
        public Fish()
        {
        }
        public void setWater(Water _water)
        {
            water = _water;
        }
        public void Live()
        {
            if (water != null)
            {
                Console.WriteLine("個人生活靠:");
                water.GiveNutrition();
            }
        }
    }

優點:靈活缺點:要多判斷依賴項可能為null的時候

  • 介面注入 (Interface Injection)
 public class Fish : ISetWater
    {
        private Water water;
        public Fish()
        {
        }
        public void Live()
        {
            if (water != null)
            {
                Console.WriteLine("個人生活靠:");
                water.GiveNutrition();
            }
        }

        public void SetWater(Water _water)
        {
            water = _water;
        }
    }

    public interface ISetWater
    {
        void SetWater(Water water);
    }

優點:比前者更靈活,只有需要此依賴的類別才需實現該介面

結論

https://i.imgur.com/gM2IS62.png

透過IOC/DI來符合DIP

高階模組,依賴於抽象,而非低階模組。

但要使用抽象的具體產品(低階模組)時,

1.不用也不需要知道是哪種具體產品2.不再自己實例具體產品,而是服務容器(IOC)會提供給他 。

實務與例子

用車子例子說明概念

控制反轉

控制反轉(Inversion of Control)是一種是物件導向程式設計中的一種設計原則,用來減低計算機程式碼之間的耦合度。其基本思想是:藉助於“第三方”實現具有依賴關係的物件之間的解耦

從圖中可以看到,軟體中的物件就像齒輪一樣,協同工作,但是互相耦合,一個零件不能正常工作,整個系統就崩潰了。這是一個強耦合的系統。現在,伴隨著工業級應用的規模越來越龐大,物件之間的依賴關係也越來越複雜,耦合度過高的系統必然會出現牽一髮而動全身的情形。

https://i.iter01.com/images/b92b812c2cc0386ec43d55ff541ce4bad1dbfe0964c42606e732cbb8f72a5315.png

為了解決物件間耦合度過高的問題,軟體專家Michael Mattson提出了IoC理論,用來實現物件之間的“解耦”。

https://i.iter01.com/images/1dea41d6cd7bace5bc4262921093c2feacbfc19b65ce36f975882cf9e8f0206d.png

由於引進了中間位置的IOC容器,使得A、B、C、D這4個物件沒有了耦合關係,齒輪之間的傳動全部依靠“第三方”了,全部物件的控制權全部上繳給IOC容器,IOC容器成了整個系統的關鍵核心

通過前後的對比,我們不難看出來:物件A獲得依賴物件B的過程,由主動行為變為了被動行為,控制權顛倒過來了,這就是“控制反轉”這個名稱的由來。

控制反轉不只是軟體工程的理論,在生活中我們也有用到這種思想。再舉一個現實生活的

依賴注入

依賴注入是一種設計模式,可以作為控制反轉的一種實現方式。依賴注入就是將例項變數傳入到一個物件中去(Dependency injection means giving an object its instance variables)。

什麼是依賴

如果在 Class A 中,有 Class B 的例項,則稱 Class A 對 Class B 有一個依賴。例如下面類 Human 中用到一個 Father 物件,我們就說類 Human 對類 Father 有一個依賴。

public class Human {
    ...
    Father father;
    ...
    public Human() {
        father = new Father();
    }
}

仔細看這段程式碼我們會發現存在一些問題,如果現在要改變 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 程式碼,因為 father 的初始化被寫死在了 Human 的建構函式中;

上面將依賴在建構函式中直接初始化是一種 Hard init 方式,弊端在於兩個類不夠獨立,不方便測試。我們還有另外一種 Init 方式,如下:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}

上面程式碼中,我們將 father 物件作為建構函式的一個引數傳入。在呼叫 Human 的構造方法之前外部就已經初始化好了 Father 物件。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴注入。

控制反轉和依賴注入的關係

控制反轉是一種思想,依賴注入是一種設計模式,IoC 框架使用依賴注入作為實現控制反轉的方式,但是控制反轉還有其他的實現方式,例如說ServiceLocator,所以不能將控制反轉和依賴注入等同。通過IoC框架,類A依賴類B的強耦合關係可以在執行時通過容器建立,也就是說把建立B例項的工作移交給容器,類A只管使用就可以。

依賴倒置原則(Dependency Inversion Principle )

依賴倒置原則把原本的高層建築依賴底層建築“倒置”過來,變成底層建築依賴高層建築。高層建築決定需要什麼,底層去實現這樣的需求,但是高層並不用管底層是怎麼實現的。這樣就不會出現“牽一髮動全身”的情況。

假設我們設計一輛汽車:先設計輪子,然後根據輪子大小設計底盤,接著根據底盤設計車身,最後根據車身設計好整個汽車。這裡就出現了一個“依賴”關係:汽車依賴車身,車身依賴底盤,底盤依賴輪子。

https://i.iter01.com/images/d1fdef719d548e7b2e20fb344fe1c2ff985e15002d7f76df56e6258a2f5f90c2.png

這樣的設計看起來沒問題,但是可維護性卻很低。假設設計完工之後,我們把車子的輪子設計都改大一碼。因為我們是根據輪子的尺寸設計的底盤,輪子的尺寸一改,底盤的設計就得修改;同樣因為我們是根據底盤設計的車身,那麼車身也得改,同理汽車設計也得改——整個設計幾乎都得改!

我們現在換一種思路。我們先設計汽車的大概樣子,然後根據汽車的樣子來設計車身,根據車身來設計底盤,最後根據底盤來設計輪子。這時候,依賴關係就倒置過來了:輪子依賴底盤, 底盤依賴車身, 車身依賴汽車。

https://i.iter01.com/images/b99d351ccffe1d2c8052a1846314d02b74077328c19998dc8a28c2981c2e6ef2.png

這時候要改動輪子的設計,我們就只需要改動輪子的設計,而不需要動底盤,車身,汽車的設計了。

https://i.iter01.com/images/29868d447201e2a63757be5237ee880fe0975f04d785d18f81d03943f30224fb.png

為了理解這幾個概念,我們還是用上面汽車的例子。只不過這次換成程式碼。我們先定義四個Class,車,車身,底盤,輪胎。然後初始化這輛車,最後跑這輛車。程式碼結構如下:

https://i.iter01.com/images/3c97dab902b06fca361698ced0ccd7240161b91028bdfc2ea23f67b3e451d216.png

假設我們需要改動一下輪胎(Tire)類,把它的尺寸變成動態的,而不是一直都是30。我們需要這樣改:

https://i.iter01.com/images/c60e7cde22c63302a602e9d837ff9b56a2246f2edbd2655132e0a95e00379739.png

由於我們修改了輪胎的定義,為了讓整個程式正常執行,我們需要做以下改動:

https://i.iter01.com/images/8f6791c7b2c403cf6b2d09e299fa7796b76e68cf47947fd651e35ff42c4644a7.png

由此我們可以看到,僅僅是為了修改輪胎的建構函式,這種設計卻需要修改整個上層所有類的建構函式

所以我們需要進行控制反轉(IoC),及上層控制下層,而不是下層控制著上層。我們用依賴注入(Dependency Injection)這種方式來實現控制反轉。

https://i.iter01.com/images/85cbfae6adc394a330a4fe3852c9e4dd4b0cb78a6022fd0b8268105e9f0626c9.png

這裡我們再把輪胎尺寸變成動態的,同樣為了讓整個系統順利執行,我們需要做如下修改:這裡我只需要修改輪胎類就行了,不用修改其他任何上層類。這顯然是更容易維護的程式碼。不僅如此,在實際的工程中,這種設計模式還有利於不同組的協同合作和單元測試

控制反轉容器(IoC Container)

那什麼是控制反轉容器(IoC Container)呢?其實上面的例子中,對車類進行初始化的那段程式碼發生的地方,就是控制反轉容器。

https://i.iter01.com/images/dc12ac566979c2804dfc930aa6947a3c0673da160a884b46b7b30547c78027f5.png

因為採用了依賴注入,在初始化的過程中就不可避免的會寫大量的new。這裡IoC容器就解決了這個問題。這個容器可以自動對你的程式碼進行初始化,你只需要維護一個Configuration(可以是xml可以是一段程式碼),而不用每次初始化一輛車都要親手去寫那一大段初始化的程式碼。這是引入IoC Container的第一個好處。

IoC Container的第二個好處是:我們在建立例項的時候不需要了解其中的細節。在上面的例子中,我們自己手動建立一個車instance時候,是從底層往上層new的:

https://i.iter01.com/images/6d0292e87108c045b04fc9b34416339fca6d601309ad15fa2e48ef8e56ce7959.png

https://i.iter01.com/images/fee76746d82171387cb923c96bb88ae80c2307c1c8e758dcfff03f866ffa269b.png

這裡IoC Container可以直接隱藏具體的建立例項的細節,在我們來看它就像一個工廠:

https://i.iter01.com/images/b666762cd52e9c671ed821fe197dc79293471dfa4ab25d3dda317835f19567b8.png

我們就像是工廠的客戶。我們只需要向工廠請求一個Car例項,然後它就給我們按照Config建立了一個Car例項。我們完全不用管這個Car例項是怎麼一步一步被建立出來。

.NET CORE DI容器介紹

參考網址:https://blog.johnwu.cc/article/ironman-day04-asp-net-core-dependency-injection.html

ASP.NET Core 透過 DI 容器,切斷這些相依關係,實例的產生不會是在使用方(指上例 UserController 建構子的 new),而是在 DI 容器。DI 容器的註冊方式也很簡單,在 Startup.ConfigureServices 註冊。如下:

Startup.cs

// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }
}

services 就是一個 DI 容器。此例把 MVC 的服務註冊到 DI 容器,等到需要用到 MVC 服務時,才從 DI 容器取得物件實例。

基本上要注入到 Service 的類別沒什麼限制,除了靜態類別。以下範例程式就只是一般的 Class 繼承 Interface:Sample : ISample

public interface ISample
{
    int Id { get; }
}

public class Sample : ISample{
    private static int _counter;
    private int _id;

    public Sample()
    {
        _id = ++_counter;
    }

    public int Id => _id;
}

要注入的 Service 需要在 Startup.ConfigureServices 中註冊實做類別。如下:

// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddScoped<ISample, Sample>();
    }
}

第一個泛型為注入的類型建議用 Interface 來包裝,這樣在才能把相依關係拆除。第二個泛型為實做的類別

DI 運作方式

ASP.NET Core 的 DI 是採用 Constructor Injection,也就是說會把實例化的物件從建構子傳入。如果要取用 DI 容器內的物件,只要在建構子加入相對的 Interface 即可。例如:

Controllers\HomeController.cs

public class HomeController : Controller{
    private readonly ISample _sample;

    public HomeController(ISample sample)
    {
        _sample = sample;
    }

    public string Index() {
        return $"[ISample]\\r\\n"+ $"Id: {_sample.Id}\\r\\n"+ $"HashCode: {_sample.GetHashCode()}\\r\\n"+ $"Tpye: {_sample.GetType()}";
    }
}

輸出內容如下:

[ISample]
Id: 1
HashCode: 14145203
Tpye: MyWebsite.Sample

ASP.NET Core 實例化 Controller 時,發現建構子有 ISample 這個類型的參數,就把 Sample 的實例注入給該 Controller。

每個 Request 都會把 Controller 實例化,所以 DI 容器會從建構子注入 ISample 的實例,把 sample 存到欄位 _sample 中,就能確保 Action 能夠使用到被注入進來的 ISample 實例。

注入實例過程,情境如下:

https://i.imgur.com/AuZaplR.png

Service 生命週期註冊在 DI 容器的 Service 有分三種生命週期:

Transient每次注入時,都重新 new 一個新的實例。Scoped每個 Request 都重新 new 一個新的實例,同一個 Request 不管經過多少個 Pipeline 都是用同一個實例。上例所使用的就是 Scoped。Singleton被實例化後就不會消失,程式運行期間只會有一個實例。小改一下 Sample 類別的範例程式:

public interface ISample
{
    int Id { get; }
}

public interface ISampleTransient : ISample{
}

public interface ISampleScoped : ISample{
}

public interface ISampleSingleton : ISample{
}

public class Sample : ISampleTransient, ISampleScoped, ISampleSingleton{
    private static int _counter;
    private int _id;

    public Sample()
    {
        _id = ++_counter;
    }

    public int Id => _id;
}

在 Startup.ConfigureServices 中註冊三種不同生命週期的服務。如下:

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<ISampleTransient, Sample>();
        services.AddScoped<ISampleScoped, Sample>();
        services.AddSingleton<ISampleSingleton, Sample>();
        // Singleton 也可以用以下方法註冊
        // services.AddSingleton<ISampleSingleton>(new Sample());
    }
}

Service Injection只要是透過 WebHost 產生實例的類別,都可以在建構子定義型態注入。

所以 Controller、View、Filter、Middleware 或自訂的 Service 等都可以被注入。此篇只用 Controller、View、Service 做為範例。

Controller在 HomeController 中注入上例的三個 Services:

Controllers\HomeController.cs

public class HomeController : Controller{
    private readonly ISample _transient;
    private readonly ISample _scoped;
    private readonly ISample _singleton;

    public HomeController(
        ISampleTransient transient,
        ISampleScoped scoped,
        ISampleSingleton singleton)
    {
        _transient = transient;
        _scoped = scoped;
        _singleton = singleton;
    }

    public IActionResult Index() {
        ViewBag.TransientId = _transient.Id;
        ViewBag.TransientHashCode = _transient.GetHashCode();

        ViewBag.ScopedId = _scoped.Id;
        ViewBag.ScopedHashCode = _scoped.GetHashCode();

        ViewBag.SingletonId = _singleton.Id;
        ViewBag.SingletonHashCode = _singleton.GetHashCode();
        return View();
    }
}

輸出內容如下:

https://i.imgur.com/aeVsR76.png

從左到又打開頁面三次,可以發現 Singleton 的 Id 及 HashCode 都是一樣的,此例還看不太出來 Transient 及 Scoped 的差異。Service 實例產生方式:

https://i.imgur.com/chrAgfr.gif

圖例說明:

A 為 Singleton 物件實例一但實例化,就會一直存在於 DI 容器中。B 為 Scoped 物件實例每次 Request 就會產生新的實例在 DI 容器中,讓同 Request 週期的使用方,拿到同一個實例。C 為 Transient 物件實例只要跟 DI 容器請求這個類型,就會取得新的實例。

ViewView 注入 Service 的方式,直接在 *.cshtml 使用 @inject:

Views\Home\Index.cshtml

@using MyWebsite

@inject ISampleTransient transient
@inject ISampleScoped scoped
@inject ISampleSingleton singleton

<table border="1">
    <tr><td colspan="3">Cotroller</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
    <tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
    <tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>
<hr />
<table border="1">
    <tr><td colspan="3">View</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@transient.Id</td><td>@transient.GetHashCode()</td></tr>
    <tr><td>Scoped</td><td>@scoped.Id</td><td>@scoped.GetHashCode()</td></tr>
    <tr><td>Singleton</td><td>@singleton.Id</td><td>@singleton.GetHashCode()</td></tr>
</table>

輸出內容如下:

https://i.imgur.com/XIiedlU.png

從左到又打開頁面三次,Singleton 的 Id 及 HashCode 如前例是一樣的。Transient 及 Scoped 的差異在這次就有明顯差異,Scoped 在同一次 Request 的 Id 及 HashCode 都是一樣的,如紅綠籃框。

什麼是Polling、long polling、WebSocket

Polling

輪詢Polling)是一種CPU決策如何提供週邊裝置服務的方式,又稱「程式控制輸入輸出」(Programmed I/O)。輪詢法的概念是:由CPU定時發出詢問,依序詢問每一個週邊裝置是否需要其服務,有即給予服務,服務結束後再問下一個週邊,接著不斷週而復始。

COMET(字面翻譯:彗星)

Comet is a web application model in which a long-held HTTPS request allows a web server to push data to a browser, without the browser explicitly requesting it

Comet 並不是一種制式的協定 , 這是一種概念, Comet 主要用意是要讓 Browser 得到即時的資訊並且做到雙向互動 , 但我們都知道 , Web Browser 是要 Client 端主動要求某個網址 , Web Server 才會送資料來。

polling vs long polling

https://dotblogsfile.blob.core.windows.net/user/regionbbs/1109/ASP.NETLongPolling_12ACA/image_thumb.png

改善方式

  1. 架構(流程)
  2. 方法

https://www.yasssssblog.com/assets/img/polling.3cbcf3dc.png

 

AJAX(Asynchronous JavaScript and XML)

最大優點,就是能在不更新整個頁面的前提下維護資料。這使得Web應用程式更為迅捷地回應使用者動作,並避免了在網路上傳送那些沒有改變的資訊。

備註:可用JSON、可同步

<aside> 💡 以上皆基於HTTP協定,屬於半雙工

</aside>

雙工duplex

  • 半雙工(half-duplex)

    • Comet
      • Polling
      • Long Polling
  • 全雙工(full-duplex)

    • WebSocket

https://img-blog.csdnimg.cn/20181030105652259.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zNzY0MTgzMg==,size_16,color_FFFFFF,t_70

Long Polling(長時間輪詢)

一、什麼是 Long Polling

  • Long Polling是 Comet 演化過後的方式
  • 目前 Facebook、Plurk 實現動態更新的方法
  • 單向的 Server 送資料給 Client,若要傳資料給 Server,仍要另開新的連線 (Request)
  • 比 Polling 更有效率可以等到 Timeout 或拿到新資料的時候在重新發送,相對之下減少很多網路資源的浪費

如何傳遞資料

RP4R7bK3qr.jpg

Long Polling 的作法

  • Client side 發送長時間等待的 Request 給 Server side
  • Server 收到後 Response 給 Client ,並斷開連線
  • Client 收到 Response 後,執行 Callback ,再次發送 Request 給 Sevrer
  • 若等待的時間到了也沒有新資料,就會送個回應給Client ,告知資料沒有更新

例如

(functionpolling() {
    $.ajax({
        url: "<http://server>",
        type: "post",
        dataType: "json",
        timeout: 30000,   //等待三十秒後重複發送 AJAX 請求到 Server 端
        success:function(data) {
            /* Do something */
        },
        complete:function() {
            /* Polling here. */
            polling();
        }
    });
})();

Server 端需支援 non-blocking IO

非 non-blocking IO 的 Server 不行的原因:

  • 如果後端收到之後並沒有斷開連線,那麼前端就只會每 30 秒斷線重連,也就是一般的 Polling

  • 例如

    假定送一個 AJAX 給 Server side, 事情做完之後,丟一個 Response 給 Client , 一樣會觸發 complete 條件。但當Server 沒放開連線,只能等 Client timeout, 並且再次發送 Request 才能繼續動作,而這時候 Server 的資料到底有沒有完成,根本不知道,所以 non-blocking IO 的 Server 多少能避開這種問題

優點

長輪詢和短輪詢比起來,明顯減少了很多不必要的http請求次數,相比之下節約了資源

缺點

連接掛起也會導致資源的浪費

二、補充Comet

Client 發出一個請求 Request 後,Server 不將此連線中斷,伺服器保持連線的狀態,持續讓 Server 端 Response 資料回來,這樣的作法其實就是把 Polling 做在 Server 端

用 AJAX 實作 Comet

在 Server 查詢完畢後、利用 flush() 顯示結果、再使用 sleep() 暫停執行,依這樣的方式做無窮迴圈。這樣的作法可將 Browser 的 Request 減到最低、但 Server 端仍得用無窮迴圈一直做查詢(可以說是 Server 端的 Polling)

while (TRUE) // 無窮迴圈
{
    $wait = rand(1, 3);
    flush(); // 輸出結果,有人會另外加上 ob_flush()
    $num = rand(10, 100); // 一樣是亂數、可以想成是 ,memcache 或資料庫的查詢。
    echo "Server said $num.\\r\\n";
    sleep($wait); // 等待一陣子
}

優點

不結束連線,解決 Polling 造成頻寬浪費的問題

缺點

此作法會把傳統的 Web Server 連線給佔住,所以須配合 non-Blocking IO 的 Web Server 才能運作

兩種實作方式

  • 長時間輪詢 Long polling
  • 串流 Streaming

三、補充setTimeout() 與 setInterval()

setTimeout()與setInterval()特性

綁定在瀏覽器 window 的一個方法,可以透過  setInterval  /setTTimeout 指定一段程式碼或函式在多少毫秒(ms)後執行,並回傳此定時器的編號

可以透過 clearInterval  / clearInterval 取消程式碼的執行

setTimeout()與setInterval()差異

  setTimeout() setInterval()
執行次數 執行一次 自動重複執行
  1. setTimeout()

    時間基本上是符合所設定的 100 ms

    var startTime=new Date();
    var func = function(){
    console.log('start: ' + (new Date()-startTime));
    for(var i=0; i<1000000000; i++){};
    console.log('end: ' + (new Date()-startTime));
    setTimeout(func,100);
    };
    setTimeout(func,100);
    // start: 2515
    // end: 3457
    // start: 3558
    // end: 4503
    // start: 4604
    // end: 5543
    
  2. setInterval() 一開始就標定了執行時間點,當所註冊的函式(func)超過執行時間點,結束時則會馬上觸發(func),因此並不會是固定的 100 ms

    var startTime=new Date();
    var func = function(){
    console.log('start: ' + (new Date()-startTime));
    for(var i=0; i<1000000000; i++){}
    console.log('end: ' + (new Date()-startTime));
    };
    setInterval(func,100);
    // start: 2520
    // end: 3465
    // start: 3466
    // end: 4409
    // start: 4409
    // end: 5350
    // start: 5351
    // end: 6291
    // start: 6292
    

四、補充AJAX(wiki)

一套綜合了多項技術的瀏覽器端網頁開發技術

AJAX應用可以僅向伺服器傳送並取回必須的資料,並在客戶端採用JavaScript處理來自伺服器的回應

很多的處理工作可以在發出請求的客戶端機器上完成,因此Web伺服器的負荷也減少了

傳統Web應用與AJAX的差別

傳統的Web應用允許使用者端填寫表單(form),當送出表單時就向網頁伺服器傳送一個請求。伺服器接收並處理傳來的表單,然後送回一個新的網頁,但這個做法浪費了許多頻寬,因為在前後兩個頁面中的大部分HTML碼往往是相同的

WebSocket

WebSocket是一種網路傳輸協定,為了解決單向請求的問題而產生,可在單個TCP連接上進行全雙工通訊,位於OSI模型的應用層。

WebSocket使得客戶端和伺服器之間的資料交換變得更加簡單,允許伺服器端主動向客戶端推播資料。在WebSocket API中,瀏覽器和伺服器只需要完成一次交握,兩者之間就可以建立永續性的連接,並進行雙向資料傳輸。容易實作一個兼具可擴充性與即時性的網頁應用程式。

上面提到的即時性更新網頁的技術都有使用到 HTTP 的請求與回應,所以在網路上傳輸的資料中,一定會包含 HTTP 的表頭資訊,而這些資料其實不是必要的,多傳輸這些資料反而會造成網路延遲上升。

Untitled

連線

瀏覽器與伺服器之間若要建立一條 WebSocket 連線,在一開始的交握(handshake)階段中,要先從 HTTP 協定升級為 WebSocket 協定

Untitled

瀏覽器送出:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: <http://example.com>

伺服器回應:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

使用

設定中介軟體

在 Startup 類別的 Configure 方法中新增 WebSocket 中介軟體:

app.UseWebSockets();

接受 WebSocket 要求

在要求生命週期的後半部某處 (例如,在 Configure 方法或動作方法的後半部),檢查它是否為 WebSocket 請求並接受

下列範例取自 Configure 方法的後半部:

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
            {
                await Echo(context, webSocket);
            }
        }
        else
        {
            context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
        }
    }
    else
    {
        await next();
    }

});

傳送和接收訊息

AcceptWebSocketAsync 方法可將 TCP 連線升級為 WebSocket 連線,並提供 WebSocket 物件。 請使用 WebSocket 物件來傳送和接收訊息。 WebSocket 要求的程式碼會將 WebSocket 物件傳遞給 Echo 方法。 程式碼會收到一則訊息,並立即傳送回相同的訊息。 在用戶端關閉連線之前,訊息會在迴圈中傳送和接收:

private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

WebSocket 類別

屬性

Untitled

方法

Untitled

WebSocket 和 Polling 的比較

網路頻寬(bandwidth)的比較

WebSocket 的架構所使用的網路頻寬比傳統的 Polling 小非常多

Untitled

網路延遲(latency)的比較

由於 WebSocket 在通訊協定上的改進,所以在網路延遲也會比傳統 Polling 所使用的 HTTP 小很多。

Untitled

WebSocket Pros

  • 是一種事件導向的協議,可用於真正的即時溝通,不像 HTTP 需要不斷的請求更新,WebSocket 可以立即發送更新
  • 建立的是永續的性的連接,可解決延遲問題
  • Webockets 通常不用 XMLHttpRequest,也不會每次都傳送 header,減少很多傳到伺服器的資料流量
  • 與 HTTP 協議有良好的兼容性,默認端口也是 80 與 443 port,並且在握手階段採用 HTTP 協議,因此不容易被屏蔽,能通過各種 HTTP 代理服務器
  • 雙向溝通,server 和 client 端都可以彼此獨立的互傳訊息

WebSocket Cons

  • 斷線時不會自行回復,需要自行解決這個問題
  • 2011 前的瀏覽器不支援 WebSocket connections

補充應用

利用延遲執行(將 setTimeout 與 setInterval 事件放進 task queue)特性,我們可以應用在程式碼執行的順序(比如等 innerHTML 執行完才 document.getElementById)

setTimeout(function() {
console.log("想最後執行的一段程式碼(getElementById)");
}, 0);
function a(x) {
console.log("a() 開始 innerHTML");
b(x);
console.log("a() 結束 innerHTML");
}
 
function b(y) {
console.log("b() 開始");
console.log("傳入的值" + y);
console.log("b() 結束");
}
console.log("程式碼開始");
a(42);
console.log("程式碼結束");

補充串流(Streaming)

串流(streaming)是讓伺服器在接收到瀏覽器所送出 HTTP 請求後,立即產生一個回應瀏覽器的連線,並且讓這個連線持續一段時間不要中斷,而伺服器在這段時間內如果有新的資料,就可以透過這個連線將資料馬上傳送給瀏覽器。

這個方式雖然不錯,但是由於他是建立在 HTTP 協定上的一種傳輸機制,所以有可能會因為代理伺服器(proxy)或防火牆(firewall)將其中的資料存放在緩衝區中,造成資料回應上的延遲,因此許多使用串流的 Comet 實作會在偵測到有代理伺服器的狀況時,改用長時間輪詢的方式處理。另外透過 TLS(SSL)的連線也可以避免緩衝區的問題,但是這個方式除了設定麻煩之外,也會造成伺服器額外的負擔。

以上這些即時性更新網頁的技術都有使用到 HTTP 的請求與回應,所以在網路上傳輸的資料中,一定會包含 HTTP 的header資訊,而這些資料其實不是必要的,多傳輸這些資料反而會造成網路延遲上升。

如果是全雙工的連線的話,只需要一條就可以同時處理上傳與下載的資料傳輸,而如果以半雙工的 HTTP 協定要達到全雙工的效果,大部份的實作方式都會開啟兩條連線,一條負責上傳、另一條負責下載,但是這樣的做法不但讓整個系統更加複雜、難以維護,而且伺服器的負擔也會增加。

下圖中這個架構是一個使用半雙工 HTTP 協定的 Comet 應用程式架構,而後端搭配一個 messaging broker 以 publish/subscribe 的方式提供及時的資料。

https://blog.gtwang.org/wp-content/uploads/2015/03/comet-apps.jpg


複雜的 Comet 網頁應用程式架構


這個狀況在你要擴充系統的規模時會更糟糕,使用 HTTP 來實作雙向的資料傳輸是一件很麻煩的事情,在維護很容易出問題,擴充也會有困難,縱使你的使用者感覺這樣即時性的網頁應用程式很好用,但是使用這樣的架構同時會讓你的伺服器與網路承受很大的工作負載量。

  • Long Polling 長時間輪詢Long Polling 的原理是瀏覽器發出一個 Request,而伺服器讓這個 Request 持續開啟一段時間,若在這時間間隔內伺服器有資料就會回傳給客戶端,如果沒有則超時後伺服器會關上 Request。瀏覽器收到回應後,才會再重新發出一個 Request。與 Polling 的不同之處就在於它是比較有效率的、可以等到 timeout 或拿到資料時再重新發、因此減少不必要的流量浪費。但是,這種情況下當傳送的訊息相當龐大時,可能會造成傳送不完全,使得控制失靈。

    https://media.techtarget.com/tss/static/articles/content/WhatistheAsynchronousWeb/HttpLongPolling.gif

  • Streaming 串流Streaming 的原理則是讓伺服器與客戶端建立起一條持續的連線,為了使連線不中斷,伺服器每隔一段時間會發送 Response 給客戶端,確保連線不中斷,在 Streaming 中使用隱藏的 iframe tag,伺服器將資料傳入 iframe,交給其中的 javascript 去執行頁面的更新。使用 Streaming 有一些缺點,由於他是建立在 HTTP 協定上的一種傳輸機制,訊息會被包裝起來,所以可能會因為 代理伺服器(proxy) 或 防火牆(firewall) 將其中的資料存放在 緩衝區(Buffer) 中,造成傳送上的延遲,因此許多使用串流的 Comet 實作會在偵測到有代理伺服器的狀況時,改用長時間輪詢的方式處理。

    https://media.techtarget.com/tss/static/articles/content/WhatistheAsynchronousWeb/HttpStreaming.gif

    使用 Streaming 有一些缺點,由於他是建立在 HTTP 協定上的一種傳輸機制,訊息會被包裝起來,所以可能會因為 代理伺服器(proxy) 或 防火牆(firewall) 將其中的資料存放在 緩衝區(Buffer) 中,造成傳送上的延遲,因此許多使用串流的 Comet 實作會在偵測到有代理伺服器的狀況時,改用長時間輪詢的方式處理

    整合polling → long polling → web socket

    Comet 在英文中也就是彗星的意思,顧名思義發出的 Request 會像彗星的尾巴般,將 Request 拉長,讓伺服器可以想傳資料就傳,不需要等客戶端先送請求伺服器再回傳,讓伺服器實時地將更新的資訊傳送到用戶端

    Comet 的最大瓶頸在於控制連線生命週期上需要反覆發出請求,花了太多 Effort,而導致效能會較差,WebSocket 卻解決了這個問題,讓效能不會卡在連線生命週期,增強了資料傳輸的效率,接著我們來看 WebSocket 是如何解決這些問題

    HTML5利用了新的協定建立了雙向的通道:當通道建立起來之後,Browser 可以隨時丟訊息給 Server、Server 可以隨時丟訊息給瀏覽器。唯一的小缺點就是可能有一些瀏覽器相容性的問題必須要解決,IE10 以前的版本並不支援。

  • Week13

  • Thread

    執行緒(英語:thread)是作業系統能夠進行運算排程的最小單位。大部分情況下,它被包含在行程之中,是行程中的實際運作單位。

    為了提高 CPU 的使用率,將某些需要耗時較多的任務或是大量 I/O 操作 (I/O處理速度很慢),採用多執行緒可以適當地提高程式的執行效率。

    在介紹多執行緒之前,先來說明多執行緒的相關概念。

  • 程式 (Program) : 指尚未被 Load 到記憶體的 Code
  • 程序 (Process) : 指正在執行的程式,Operating System (OS) 會分配其所需要的資源,至少存在一個或多個執行緒,主要包含: Code, Data, Heap, Stack
  • Code (Text Section) 儲存程序執行的代碼
    Data 可分為 global variable 跟 static variable
    Stack 儲存暫時性的資料 (local variable, function)
    Heap 動態配置記憶體空間給變數或函式
    
    
  • 執行緒 (Thread) : 指 OS 分配 CPU 進行運算的基本單位,存在於 Process中。一旦CPU開始執行程式,就會至少有一個Thread運作
  • 多執行緒 (Multithreading) : 指將一個程序中的任務分配給不同的執行緒,各個執行緒平行運作,不互相影響,並且在同一個程序的執行緒共享記憶體 (shared memory)
  • ❗ 不同的程序間沒辦法共享記憶體,若有需要互相通訊,唯有依賴特別的設計才能擁有共享記憶體

    使用多執行緒有什麼優點跟缺點呢~~

    🔹 優點:

  • 提高 CPU 的使用效率
  • 當一個執行緒必須停下來等待與服務器連接或是需要佔據長時間處理的程序,可以放在後台處理,其他執行緒還是可以繼續運作,可以提高處理效能
  • 🔹 缺點:

  • 若有大量的執行緒,就會影響其效能,因為 OS 需要在它們之間做切換 (Context Switch)
  • 更多的執行緒需要更多的記憶體空間
  • 因為資料是多個執行緒共享的,因此有可能會發生 Race Condition 的狀況
  • 上下文切換 (Context Switch) : 指當 CPU 要從一個執行緒切換至另一個執行緒時,需要先儲存當前執行緒的狀態,再讀回將要執行的執行緒狀態。在切換的過程中,需要花費一些時間

    發生時機 : 因為一顆 CPU 同時只能處理一項程序,OS 就會利用時間輪轉的方式,讓使用者感覺這些程序都是同時運作。當CPU 認為某執行緒執行夠久的時候,就會發出一個中斷 (Interrupt) 訊號,切換至另一個執行緒去運作

  • 競爭危害 (Race Condition) : 指當一個 Thread 修改動作執行到一半時被切換,而另一個 Thread 也正好執行修改同一個地方時,可能會導致記憶體洩漏 (memory leak)

  • 條件競爭(Race Condition) : 因為程序有時間排程的問題 (發生 Context Switch),在多個執行緒的情況下,當兩個 Thread 同時修改一個資料時,可能造成變數的值錯誤,形成不可預期的結果,便造成了 Race Condition, 解決 Race Condition 的方法就是使用 Critical Section

  • 臨界區段 (Critical Section) : 指存取共享資源的程式碼區域,而這些共享資源不能被多個執行緒存取,也就是指這些要受保護的程式區段稱為 Critical Section。

    ❗ 當執行緒進入臨界區段時,必須使用一些同步機制在臨界區段的進入點與離開點實現,以確保這些執行緒的安全。

  • 同步機制 (Synchronized) : 互斥鎖 (Mutex), 訊號量 (Semaphore), 條件變數 (Condition Variable), 原子變數 (Atomic), 隊列 (Queue), 事件 (Event)

    ⚠ 但是當這些鎖定機制用得不好時,有可能會產生 Deadlock 的狀況,所以要特別注意使用

  • 死結、死鎖 (Deadlock) : 當兩個以上的 Thread,都在互相等待雙方停止執行並釋放資源,造成循環等待的情況

  • 建立執行緒

  •  
  • class Program
    {
        static void Main(string[] args)
        {
            Program mProgram = new Program();
            mProgram.ProvideMission();
            Console.ReadKey();
        }
        //指派任務
        private void ProvideMission()
        {
            //指定委派物件
            ThreadStart MissionA_Tg = new ThreadStart(MissionA);
            //建立A任務執行緒
            Thread MissionA_Thread = new Thread(MissionA_Tg);
            MissionA_Thread.Name = "Mission A";
    
            //指定委派物件
            ThreadStart MissionB_Tg = new ThreadStart(MissionB);
            //建立B任務執行緒
            Thread MissionB_Thread = new Thread(MissionB_Tg);
            MissionB_Thread.Name = "Mission B";
    
            //啟動執行緒
            MissionA_Thread.Start();
            MissionB_Thread.Start();
            NormalProcess();
    
        }
        //任務A
        private void MissionA()
        {
            for (int i = 0; i < 1000; i++)
            {
                if (i % 2 != 0)
                {
                    Console.WriteLine("A" + i);
                }
            }
        }
        //任務B
        private void MissionB()
        {
            for (int i = 0; i < 1000; i++)
            {
                if (i % 2 == 0)
                {
                    Console.WriteLine("B" + i);
                }
            }
        }
        //Normal
        private void NormalProcess()
        {
            Console.WriteLine("This is normal process ========================");
        }
    }
    
    

    2.可代入參數

    class Program
    {
        static void Main(string[] args)
        {
            Program mProgram = new Program();
            mProgram.ProvideMission();
            Console.ReadKey();
        }
        //指派任務
        private void ProvideMission()
        {
            //指定委派物件
            ParameterizedThreadStart MissionA_Tg = new ParameterizedThreadStart(MissionA);
            //建立A任務執行緒
            Thread MissionA_Thread = new Thread(MissionA_Tg);
            MissionA_Thread.Name = "Mission A";
    
            //指定委派物件
            ParameterizedThreadStart MissionB_Tg = new ParameterizedThreadStart(MissionB);
            //建立B任務執行緒
            Thread MissionB_Thread = new Thread(MissionB_Tg);
            MissionB_Thread.Name = "Mission B";
    
            //啟動執行緒
            MissionA_Thread.Start(10);
            MissionB_Thread.Start(10);
            NormalProcess();
    
        }
        //任務A
        private void MissionA(object value)
        {
            int num = Convert.ToInt32(value);
            for (int i = 0; i < num; i++)
            {
                if (i % 2 != 0)
                {
                    Console.WriteLine("A" + i);
                    Thread.Sleep(1000);
                }
            }
        }
        //任務B
        private void MissionB(object value)
        {
            int num = Convert.ToInt32(value);
            for (int i = 0; i < num; i++)
            {
                if (i % 2 == 0)
                {
                    Console.WriteLine("B" + i);
                    Thread.Sleep(2000);
                }
            }
        }
        //Normal
        private void NormalProcess()
        {
            Console.WriteLine("This is normal process ========================");
        }
    }
    
    

    3.Sleep

    class Program
    {
        static void Main(string[] args)
        {
            Program mProgram = new Program();
            mProgram.ProvideMission();
            Console.ReadKey();
        }
        //指派任務
        private void ProvideMission()
        {
            //指定委派物件
            ParameterizedThreadStart MissionA_Tg = new ParameterizedThreadStart(MissionA);
            //建立A任務執行緒
            Thread MissionA_Thread = new Thread(MissionA_Tg);
            MissionA_Thread.Name = "Mission A";
    
            //指定委派物件
            ParameterizedThreadStart MissionB_Tg = new ParameterizedThreadStart(MissionB);
            //建立B任務執行緒
            Thread MissionB_Thread = new Thread(MissionB_Tg);
            MissionB_Thread.Name = "Mission B";
    
            //啟動執行緒
            MissionA_Thread.Start(10);
            MissionB_Thread.Start(10);
            NormalProcess();
    
        }
        //任務A
        private void MissionA(object value)
        {
            int num = Convert.ToInt32(value);
            for (int i = 0; i < num; i++)
            {
                if (i % 2 != 0)
                {
                    Console.WriteLine("A" + i);
                    Thread.Sleep(1000);
                }
            }
        }
        //任務B
        private void MissionB(object value)
        {
            int num = Convert.ToInt32(value);
            for (int i = 0; i < num; i++)
            {
                if (i % 2 == 0)
                {
                    Console.WriteLine("B" + i);
                    Thread.Sleep(2000);
                }
            }
        }
        //Normal
        private void NormalProcess()
        {
            Console.WriteLine("This is normal process ========================");
        }
    }
    
    

    4.Join

    通常我們認知的Join是連結的意思,但是在C# Thread.Join 的意思是可以封鎖指定的執行緒,直到執行緒執行完畢主要用於控制順序,等封鎖的執行緒執行完畢,主處理序才會繼續往下走

    class Program
    {
        private Thread MissionA_Thread;
        private Thread MissionB_Thread;
        static void Main(string[] args)
        {
            Program mProgram = new Program();
            mProgram.ProvideMission();
            Console.WriteLine("Blocks the calling MissionA thread");
            mProgram.MissionA_Thread.Join();
            Console.WriteLine("MissionA thread terminates");
            Console.ReadKey();
        }
        //指派任務
        private void ProvideMission()
        {
            //指定委派物件
            ParameterizedThreadStart MissionA_Tg = new ParameterizedThreadStart(MissionA);
            //建立A任務執行緒
            MissionA_Thread = new Thread(MissionA_Tg);
            MissionA_Thread.Name = "Mission A";
    
            //指定委派物件
            ParameterizedThreadStart MissionB_Tg = new ParameterizedThreadStart(MissionB);
            //建立B任務執行緒
            MissionB_Thread = new Thread(MissionB_Tg);
            MissionB_Thread.Name = "Mission B";
    
            //啟動執行緒
            MissionA_Thread.Start(10);
            MissionB_Thread.Start(10);
            NormalProcess();
    
        }
        //任務A
        private void MissionA(object value)
        {
            int num = Convert.ToInt32(value);
            for (int i = 0; i < num; i++)
            {
                if (i % 2 != 0)
                {
                    Console.WriteLine("A" + i);
                    Thread.Sleep(1000);
                }
            }
        }
        //任務B
        private void MissionB(object value)
        {
            int num = Convert.ToInt32(value);
            for (int i = 0; i < num; i++)
            {
                if (i % 2 == 0)
                {
                    Console.WriteLine("B" + i);
                    Thread.Sleep(2000);
                }
            }
        }
        //Normal
        private void NormalProcess()
        {
            Console.WriteLine("This is normal process ========================");
        }
    }
    
    

    5.Interrupt

    class Program
    {
        private Thread MissionA_Thread;
        private Thread MissionB_Thread;
        static void Main(string[] args)
        {
            Program mProgram = new Program();
            mProgram.ProvideMission();
    
            Console.ReadKey();
        }
        //指派任務
        private void ProvideMission()
        {
            //指定委派物件
            ParameterizedThreadStart MissionA_Tg = new ParameterizedThreadStart(MissionA);
            //建立A任務執行緒
            MissionA_Thread = new Thread(MissionA_Tg);
            MissionA_Thread.Name = "Mission A";
    
            //指定委派物件
            ParameterizedThreadStart MissionB_Tg = new ParameterizedThreadStart(MissionB);
            //建立B任務執行緒
            MissionB_Thread = new Thread(MissionB_Tg);
            MissionB_Thread.Name = "Mission B";
    
            //啟動執行緒
            MissionA_Thread.Start(10);
            MissionB_Thread.Start(10);
    
            //暫停主執行緒
            MissionB_Thread.Join();
    
            NormalProcess();
    
        }
        //任務A
        private void MissionA(object value)
        {
            try
            {
                int num = Convert.ToInt32(value);
                for (int i = 0; i < num; i++)
                {
                    if (i == 5)
                    {
                        //中斷執行緒
                        MissionA_Thread.Interrupt();
                    }
                    Console.WriteLine("A" + i);
                    Thread.Sleep(1000);
                }
            }catch (ThreadInterruptedException)
            {
                //
                Console.WriteLine(Thread.CurrentThread.Name);
            }
        }
        //任務B
        private void MissionB(object value)
        {
            try {
                int num = Convert.ToInt32(value);
                for (int i = 0; i < num; i++)
                {
                    if (i > 6)
                    {
                        //中斷執行緒
                        MissionB_Thread.Interrupt();
                    }
                    Console.WriteLine("B" + i);
                    Thread.Sleep(2000);
                }
            }
            catch (ThreadInterruptedException)
            {
                //捕捉中斷執行緒拋出的例外
                Console.WriteLine(Thread.CurrentThread.Name+"Is Stop");
            }
        }
        //Normal
        private void NormalProcess()
        {
            Console.WriteLine("This is normal process ========================");
        }
    }
    
    

    6.Abort

    if(MissionA_Thread.IsAlve)
    {
        MissionA_Thread.Abort();
    }
    
    

    Task

    Untitled

    Thread

  • 非同步操作
  • 啟動的執行緒不好判斷執行緒的執行情況
  • 預設為前臺執行緒,主程式必須等執行緒跑完才會關閉
  • 沒有很好的api區控制
  • Threadpool

  • 非同步操作
  • Thread的集合
  • 啟動的執行緒不好判斷執行緒的執行情況
  • 任務多的時候全域性佇列會存在競爭而消耗資源,耗記憶體
  • 預設為後臺執行緒
  • 沒有很好的api區控制
  • 使用 Thread 類別缺了兩項基本功能:難以得知非同步工作何時完成

    Task出現的目的

    以工作為基礎的(task-based)概念,並將相關類別放在 System.Threading.Tasks 命名空間裡。這些類別形成了一組 API,統稱為 Task Parallel Library(TPL)

    Task

  • 非同步操作
  • Task提供豐富的API來管理、控制執行緒,解決ThreadPool、Thread判斷執行緒的執行情況
  • Task依賴多核的CPU,效能遠超 Thread 和 ThreadPool ,但單核的CPU三者的效能沒什麼差別。
  • Task是在 ThreadPool 的基礎上進行一層封裝,背後的實現也是使用 ThreadPool 的執行緒,但它的效能優於 ThreadPool ,因為它使用的不是執行緒池的全域性佇列,而是使用的本地佇列,使執行緒之間的資源競爭減少
  • Task不等於Thread,只是微軟預設實現ThreadPoolTaskScheduler是依賴於執行緒池的,因為該類的可訪問性為internal,所以我們在實際編碼中無法直接在程式碼中new這麼一個Scheduler出來,只能透過 TaskScheduler.Default 間接的來使用
  • 對比ThreadPool & Task

    static void Main(string[] args)
    {    // 寫法 1 - .NET 2 開始提供
        ThreadPool.QueueUserWorkItem(state => MyTask());
    
        //.NET 4 開始提供 Task 類別。
        var t = new Task(MyTask);   // 等同於 new Task(new Action(MyTask));
        t.Start();
    
        Console.ReadLine();
    }
    
    static void MyTask()
    {
        Console.WriteLine("工作執行緒 #{0}", Thread.CurrentThread.ManagedThreadId);
    }
    
    

    new Task 的方式來建立 Task 物件──代表一項工作;建立好的工作可以在稍後需要的時候才呼叫 Task 物件的 Start 方法來開始執行工作。這表示我們甚至可以先建立好 Task 物件,然後把它當作參數傳遞給其他函式,並由其他函式在適當時機起始工作

    TaskFactory 類別 & Run方法

    目的:一次完成建立工作和起始工作的程序

    static void Main(string[] args)
    {
        //可以用靜態方法直接建立並開始執行工作。
        Task.Factory.StartNew(MyTask(1));
    
        //Task 類別新增了靜態方法 Run。
        Task.Run(() => MyTask(2));
    
        Console.ReadLine();
    }
    
    static void MyTask(int num)
    {
        Console.WriteLine("工作執行緒 #{0}", Thread.CurrentThread.ManagedThreadId);
    }
    
    

    透過 Task.Factory 屬性取得 TaskFactory 類別的執行實體

    public new static TaskFactory<TResult> Factory { get { return s_Factory; } }
    
    

    TaskFactory

    Task自帶的執行緒工廠,方便隨時建立Task

    TaskFactory 有多達 16 種多載版本,提供更細緻的控制

    /* Constructors */
    public TaskFactory()
                : this(default(CancellationToken), TaskCreationOptions.None, TaskContinuationOptions.None, null)
            {
            }
    
    public TaskFactory(CancellationToken cancellationToken)
                : this(cancellationToken, TaskCreationOptions.None, TaskContinuationOptions.None, null)
            {
            }
    
    public TaskFactory(TaskScheduler scheduler) // null means to use TaskScheduler.Current
                : this(default(CancellationToken), TaskCreationOptions.None, TaskContinuationOptions.None, scheduler)
            {
            }
    
    public TaskFactory(TaskCreationOptions creationOptions, TaskContinuationOptions continuationOptions)
                : this(default(CancellationToken), creationOptions, continuationOptions, null)
            {
            }
    
    public TaskFactory(CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskContinuationOptions continuationOptions, TaskScheduler scheduler)
            {
                TaskFactory.CheckMultiTaskContinuationOptions(continuationOptions);
                TaskFactory.CheckCreationOptions(creationOptions);
    
                m_defaultCancellationToken = cancellationToken;
                m_defaultScheduler = scheduler;
                m_defaultCreationOptions = creationOptions;
                m_defaultContinuationOptions = continuationOptions;
            }
    
    

    StartNew 多載方法

    public Task<TResult> StartNew(Func<TResult> function)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                Task currTask = Task.InternalCurrent;
                return Task<TResult>.StartNew(currTask, function, m_defaultCancellationToken,
                    m_defaultCreationOptions, InternalTaskOptions.None, GetDefaultScheduler(currTask), ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<TResult> function, CancellationToken cancellationToken)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                Task currTask = Task.InternalCurrent;
                return Task<TResult>.StartNew(currTask, function, cancellationToken,
                    m_defaultCreationOptions, InternalTaskOptions.None, GetDefaultScheduler(currTask), ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<TResult> function, TaskCreationOptions creationOptions)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                Task currTask = Task.InternalCurrent;
                return Task<TResult>.StartNew(currTask, function, m_defaultCancellationToken,
                    creationOptions, InternalTaskOptions.None, GetDefaultScheduler(currTask), ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<TResult> function, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                return Task<TResult>.StartNew(
                    Task.InternalCurrentIfAttached(creationOptions), function, cancellationToken,
                    creationOptions, InternalTaskOptions.None, scheduler, ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<Object, TResult> function, Object state)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                Task currTask = Task.InternalCurrent;
                return Task<TResult>.StartNew(currTask, function, state, m_defaultCancellationToken,
                    m_defaultCreationOptions, InternalTaskOptions.None, GetDefaultScheduler(currTask), ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<Object, TResult> function, Object state, CancellationToken cancellationToken)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                Task currTask = Task.InternalCurrent;
                return Task<TResult>.StartNew(currTask, function, state, cancellationToken,
                    m_defaultCreationOptions, InternalTaskOptions.None, GetDefaultScheduler(currTask), ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<Object, TResult> function, Object state, TaskCreationOptions creationOptions)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                Task currTask = Task.InternalCurrent;
                return Task<TResult>.StartNew(currTask, function, state, m_defaultCancellationToken,
                    creationOptions, InternalTaskOptions.None, GetDefaultScheduler(currTask), ref stackMark);
            }
    
    public Task<TResult> StartNew(Func<Object, TResult> function, Object state, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler)
            {
                StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
                return Task<TResult>.StartNew(Task.InternalCurrentIfAttached(creationOptions), function, state, cancellationToken,
                    creationOptions, InternalTaskOptions.None, scheduler, ref stackMark);
            }
    
    

    範例

    開啟新任務的方法:Task.Run() 或 Task.Factory.StartNew()

    Task啟動的是後臺執行緒

    如果要主執行緒中等待後臺執行緒執行完畢,可以呼叫Wait方法

    Console.WriteLine("主執行緒啟動");
    
    Task task = Task.Factory.StartNew(() => {
    Thread.Sleep(1500);
    Console.WriteLine("task啟動");
    });
    
    Task task = Task.Run(() => {
    Thread.Sleep(1500);
    Console.WriteLine("task啟動");
    });
    
    Thread.Sleep(300);
    task.Wait();
    Console.WriteLine("主執行緒結束");
    
    

    https://i.imgur.com/6T2YveF.png

    Task 支援的 TPL

    基於Action 委派
    
    Task透過CancellationToken支援一種執行緒的取消機制
    
    Task支援Delay操作
    
    Task提供了更完善的異常處理機制
    
    Task自帶**TaskFactory** ,方便隨時建立Task
    
    Task支援Wait WaitAny WaitAll
    
    Task支援 WhenAny WhenAll
    
    Task支援 ContinueWith,節省執行緒開銷
    
    Task支援 Yield操作
    
    Task透過 TaskScheduler 可以支援執行緒佇列
    
    Task還可以配合 async 和 await 關鍵字,寫出更優雅的多執行緒程式
    
    

    async/await

    async用來修飾方法,表明這個方法是非同步的

    宣告的方法的返回型別必須為:void,Task或Task<TResult>

    await必須用來修飾Task或Task<TResult>,而且只能出現在已經用async關鍵字修飾的非同步方法中

    Console.WriteLine("-------主執行緒啟動-------");
                Task<int> task = GetStrLengthAsync();
                Console.WriteLine("主執行緒繼續執行");
                Console.WriteLine("Task返回的值" + task.Result);
                Console.WriteLine("-------主執行緒結束-------");
            }
            static async Task<int> GetStrLengthAsync()
            {
                Console.WriteLine("GetStrLengthAsync方法開始執行");
                //此處返回的<string>中的字串型別,而不是Task<string>
                string str = await GetString();
                Console.WriteLine("GetStrLengthAsync方法執行結束");
                return str.Length;
            }
            static Task<string> GetString()
            {
                //Console.WriteLine("GetString方法開始執行")
                return Task<string>.Run(() =>
                {
                    Thread.Sleep(2000);
                    return "GetString的返回值";
                });
    
    

    main函式呼叫GetStrLengthAsync方法後,在await之前,都是同步執行的,直到遇到await關鍵字,main函式才返回繼續執行

    https://i.imgur.com/C7YSJOm.png

    遇到await關鍵字的時候程式自動開啟了一個後臺執行緒去執行GetString方法嗎?

    在遇到await關鍵字後,沒有繼續執行GetStrLengthAsync方法後面的操作,也沒有馬上反回到main函式中,而是執行了GetString的第一行,以此可以判斷await這裡並沒有開啟新的執行緒去執行GetString方法,而是以同步的方式讓GetString方法執行,等到執行到GetString方法中的Task<string>.Run()的時候才由Task開啟了後臺執行緒!

    https://i.imgur.com/AeUMhdL.png

    總結

    static void Main(string[] args)
    {
        string url = "<https://www.huanlintalk.com/>";
        var task = new Task<int>(GetContentLength, url);    // 建立工作。
    
        task.Start();   // 起始工作。
    
        task.Wait();    // 等待工作。
    
        int length = task.Result;   // 取得結果。
        Console.WriteLine("Content length: {0}", length);
    }
    
    static int GetContentLength(object state)
    {
        var client = new System.Net.Http.HttpClient();
        var url = state as string;
        return client.GetStringAsync(url).Result.Length;
    }
    
    

    Timer

    .NET 包含四個類別的 Timer

    System.Timers.Timer:

    定期引發事件。 類別的目的是要在多執行緒環境中做為伺服器架構或服務元件使用;它沒有使用者介面,而且在執行時間看不到。

    System.Threading.Timer:

    定期線上程集區執行緒上執行單一回呼方法。 當計時器具現化且無法變更時,會定義回呼方法。 就像 System.Timers.Timer 類別一樣,這個類別的目的是要在多執行緒環境中做為伺服器型或服務元件使用; 它沒有使用者介面,在執行時間看不到。

    System.Windows.Forms.Timer ( 僅限 .NET Framework) :

    定期引發事件的 Windows Forms 元件。 該元件沒有使用者介面,是專為用於單一執行緒環境所設計。

    System.Web.UI.Timer ( 僅限 .NET Framework) :

    定期執行非同步或同步網頁回傳的 ASP.NET 元件。

    System.Timers.Timer

  • Timer元件是以伺服器為基礎的計時器,會在 Interval 屬性中的毫秒數之後,在您的應用程式中引發事件 Elapsed。
  • 當 AutoReset 設定為時 false , Timer 物件只會在經過 Interval 之後引發事件一次 。 若要持續以所定義的間隔定期引發事件,請將設 AutoReset 為 true ,這是預設值。
  • public class Timer : System.ComponentModel.Component, System.ComponentModel.ISupportInitialize
    
    
    using System;
    using System.Timers;
    
    public class Example
    {
       private static System.Timers.Timer aTimer;
    
       public static void Main()
       {
          SetTimer();
    
          Console.WriteLine("\\nPress the Enter key to exit the application...\\n");
          Console.WriteLine("The application started at {0:HH:mm:ss.fff}", DateTime.Now);
          Console.ReadLine();
          aTimer.Stop();
          aTimer.Dispose();
    
          Console.WriteLine("Terminating the application...");
       }
    
       private static void SetTimer()
       {
            // Create a timer with a two second interval.
            aTimer = new System.Timers.Timer(2000);
            // Hook up the Elapsed event for the timer.
            aTimer.Elapsed += OnTimedEvent;
            aTimer.AutoReset = true;
            aTimer.Enabled = true;
        }
    
        private static void OnTimedEvent(Object source, ElapsedEventArgs e)
        {
            Console.WriteLine("The Elapsed event was raised at {0:HH:mm:ss.fff}",
                              e.SignalTime);
        }
    }
    // The example displays output like the following:
    //       Press the Enter key to exit the application...
    //
    //       The application started at 09:40:29.068
    //       The Elapsed event was raised at 09:40:31.084
    //       The Elapsed event was raised at 09:40:33.100
    //       The Elapsed event was raised at 09:40:35.100
    //       The Elapsed event was raised at 09:40:37.116
    //       The Elapsed event was raised at 09:40:39.116
    //       The Elapsed event was raised at 09:40:41.117
    //       The Elapsed event was raised at 09:40:43.132
    //       The Elapsed event was raised at 09:40:45.133
    //       The Elapsed event was raised at 09:40:47.148
    //
    //       Terminating the application...
    
    

    System.Threading.Timer

    System.Threading.Timer 類別可讓您依指定的時間間隔持續呼叫委派。建立物件時,會定義回呼方法的 TimerCallback 委派、傳遞至回呼的選擇性狀態物件、第一個回呼引動過程之前延遲的時間量 dueTime,以及回呼引動過程之間的時間間隔 period 。最常用的構造形式:

    Timer(TimerCallback callback, object state , int duetime , int period)
    
    

    callback: 回撥方法必須是TimerCallback委託形式的:

    void TimerCallback(Object state)
    
    

    state:要傳入的物件引用,可以為null

    duetime:回呼首次被呼叫之前的時間,如果被設定為Timeout.Infinite則會停止計時

    period:兩次回呼之間的時間間隔

    可使用 change()改變 duTime 和 period

    public bool Change (int dueTime, int period);
    
    
    using System;
    using System.Threading;
    
    class TimerExample
    {
        static void Main()
        {
            // Create an AutoResetEvent to signal the timeout threshold in the
            // timer callback has been reached.
            var autoEvent = new AutoResetEvent(false);
    
            var statusChecker = new StatusChecker(10);
    
            // Create a timer that invokes CheckStatus after one second,
            // and every 1/4 second thereafter.
            Console.WriteLine("{0:h:mm:ss.fff} Creating timer.\\n",
                              DateTime.Now);
            var stateTimer = new Timer(statusChecker.CheckStatus,
                                       autoEvent, 1000, 250);
    
            // When autoEvent signals, change the period to every half second.
            autoEvent.WaitOne();
            stateTimer.Change(0, 500);
            Console.WriteLine("\\nChanging period to .5 seconds.\\n");
    
            // When autoEvent signals the second time, dispose of the timer.
            autoEvent.WaitOne();
            stateTimer.Dispose();
            Console.WriteLine("\\nDestroying timer.");
        }
    }
    
    class StatusChecker
    {
        private int invokeCount;
        private int  maxCount;
    
        public StatusChecker(int count)
        {
            invokeCount  = 0;
            maxCount = count;
        }
    
        // This method is called by the timer delegate.
        public void CheckStatus(Object stateInfo)
        {
            AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
            Console.WriteLine("{0} Checking status {1,2}.",
                DateTime.Now.ToString("h:mm:ss.fff"),
                (++invokeCount).ToString());
    
            if(invokeCount == maxCount)
            {
                // Reset the counter and signal the waiting thread.
                invokeCount = 0;
                autoEvent.Set();
            }
        }
    }
    // The example displays output like the following:
    //       11:59:54.202 Creating timer.
    //
    //       11:59:55.217 Checking status  1.
    //       11:59:55.466 Checking status  2.
    //       11:59:55.716 Checking status  3.
    //       11:59:55.968 Checking status  4.
    //       11:59:56.218 Checking status  5.
    //       11:59:56.470 Checking status  6.
    //       11:59:56.722 Checking status  7.
    //       11:59:56.972 Checking status  8.
    //       11:59:57.223 Checking status  9.
    //       11:59:57.473 Checking status 10.
    //
    //       Changing period to .5 seconds.
    //
    //       11:59:57.474 Checking status  1.
    //       11:59:57.976 Checking status  2.
    //       11:59:58.476 Checking status  3.
    //       11:59:58.977 Checking status  4.
    //       11:59:59.477 Checking status  5.
    //       11:59:59.977 Checking status  6.
    //       12:00:00.478 Checking status  7.
    //       12:00:00.980 Checking status  8.
    //       12:00:01.481 Checking status  9.
    //       12:00:01.981 Checking status 10.
    //
    //       Destroying timer.
    
    

    https://i1.read01.com/SIG=1mbfmpo/304d57794a74397a4263.jpg

    BackgroundWorker類別

    What - BackgroundWorker 是什麼

    https://i.imgur.com/5Lin5R5.png

    顧名思義,一個可以操作背景運作的類別,可以讓程式處理多工的事情,包括執行背景工作、回報工作進度、執行工作完成後的事件,當然也能隨時取消工作。簡言之,是一個簡單的執行緒控制元件。

    特色

  • 多執行緒的應用
    • 每一個BackgroundWork,都有一條自己的執行緒
  • 簡單直觀好上手
    • 停止、暫停、繼續、回報
  • Why - 為什麼需要

    當專案開發遇到計算量大的工作,執行耗時非常久,甚至導致介面卡鈍、當機。

  • 透過多執行緒分工
    • 避免卡死
    • 資源分散運用
  • When - 使用時機

  • 通常用在有使用使用者介面的情況,例如WinForm

    https://i.imgur.com/pAslN6h.png

  • 大量資料處理

  • 需要長時間IO

  • 網路資源存取(上傳、加載)

  • How - 如何使用

    方法屬性介紹

    屬性

    CancellationPending
    取得值,指出應用程式是否已經要求取消背景作業。
    IsBusy
    取得值,指出 BackgroundWorker 是否正在執行非同步作業。
    WorkerReportsProgress
    取得或設定值,指出 BackgroundWorker 是否可以報告進度更新。
    WorkerSupportsCancellation
    取得或設定值,指出 BackgroundWorker 是否支援非同步取消。
    
    

    常見方法

    RunWorkerAsync()
    開始執行背景作業。
    ReportProgress(Int32)
    引發 ProgressChanged 事件。
    
    

    https://i.imgur.com/Ba1OeNh.png

    CancelAsync()
    要求取消暫止的背景作業。
    
    

    事件

    事件Event 是一種特別的委派

  • 透過 += -= 來增加或減少要觸發的內容(方法)
  • 沒有回傳值,參數為(object sender, EventArgs eventArgs)
  • 外部只能加入方法,不能被外部執行
  •  private void InitializeBackgroundWorker()
            {
                backgroundWorker1.DoWork +=
                    new DoWorkEventHandler(backgroundWorker1_DoWork);
    
                backgroundWorker1.RunWorkerCompleted +=
                    new RunWorkerCompletedEventHandler(
                backgroundWorker1_RunWorkerCompleted);
    
                backgroundWorker1.ProgressChanged +=
                    new ProgressChangedEventHandler(
                backgroundWorker1_ProgressChanged);
            }
    
    

    壓力測試

    效能/性能測試 (Performance Testing)

    效能測試有助於適當維護系統,並在問題抵達系統使用者之前修正瑕疵。 相較于商務需求,它有助於維護應用程式的效率、回應性、擴充性和速度。 當有效完成時,效能測試應該提供您解決瓶頸所需的診斷資訊,進而導致效能不佳。 當資料流程因為容量不足而無法處理工作負載而中斷或停止時,就會發生瓶頸。

    為了避免發生效能不佳的情況,請認可時間和資源來測試系統效能。 效能測試的兩個子集、負載測試和壓力測試,可以決定最接近容量限制的 (,) 和最大 (失敗點,) 限制應用程式的容量。 藉由執行這些測試,您可以判斷所需的基礎結構,以支援預期的工作負載。

    負載測試 (Load Testing)

    負載測試會在工作負載增加時測量系統效能。 它會識別應用程式中斷的位置和時間,讓您可以在出貨到生產環境之前修正問題。 它會藉由在一般負載和繁重負載下測試系統行為來完成這項操作。

    負載測試會以負載的階段進行。 這些階段通常會由虛擬使用者 (VUs) 或模擬要求來測量,而階段則會在指定的間隔內進行。 負載測試可讓您深入瞭解您的應用程式需要調整的方式和時機,以便繼續符合您的客戶 (內部或外部) 的 SLA。 負載測試也有助於判斷分散式應用程式和微服務之間的延遲。

    https://i.imgur.com/DyGJf3g.png

    壓力測試 (Stress Testing)

    不同于負載測試,這可確保系統可以處理其設計目的,而壓力測試則著重于將系統超載,直到中斷為止。 壓力測試可判斷系統的穩定程度,以及能夠承受極大負載的能力。 它是藉由測試來自另一個服務的最大要求數目 (例如,系統在指定的時間可以處理的最大要求) ,然後效能就會失敗。 找出此最大值,以瞭解目前的環境可充分支援何種負載。

    https://i.imgur.com/h7fUgRf.png

    測試平台介紹 - loader.io

  • 到網頁註冊後即可開始使用

    https://i.imgur.com/2ZteQ79.png

  • 輸入要測試的網址

    https://i.imgur.com/OnUahFT.png

  • 驗證網址

    https://i.imgur.com/h4avfIa.png

  • 開始測試

    https://i.imgur.com/a1ToU4s.png

  • 三種測試內容

    https://i.imgur.com/1cFvJ67.png

    clients per test

    https://i.imgur.com/fVACs62.png

    clients per second

    https://i.imgur.com/1vharzd.png

    Maintain client load

    https://i.imgur.com/YuZEZa9.png

    測試內容填寫

    https://i.imgur.com/yBLNgO4.png

  • 檢視報告

    https://i.imgur.com/U6wVMfN.png

  • 特別推薦 - vegeta

    https://github.com/tsenart/vegeta

    https://i.imgur.com/5CcVTE4.png

    $ echo "GET <http://httpbin.org>" | vegeta attack -rate=10/s -duration=10s | vegeta report
    
    

    BackgroundWorker已有實作好的Event類型 EventHandler

    DoWork
    當呼叫 RunWorkerAsync() 時發生。
    ProgressChanged
    當呼叫 ReportProgress(Int32) 時發生。
    RunWorkerCompleted
    當背景作業已完成、取消或引發例外狀況時發生。
    
    

    使用步驟

    一、 可以回報與取消

    backgroundWorker1.WorkerReportsProgress = true; //可以回報
    backgroundWorker1.WorkerSupportsCancellation = true; //可以取消
    
    

    二、建立開始和取消動作

    private void startAsyncButton_Click(object sender, EventArgs e)
            {
                if (backgroundWorker1.IsBusy != true) //bgwer沒在忙的情況下
                {
                    // 開始執行
                    backgroundWorker1.RunWorkerAsync();
                }
            }
    
            private void cancelAsyncButton_Click(object sender, EventArgs e)
            {
                if (backgroundWorker1.WorkerSupportsCancellation == true)
                {
                    取消執行
                    backgroundWorker1.CancelAsync();
                }
            }
    
    

    三、定義要在背景執行的工作、回報進度方法、工作結束的方法

             // 工作內容
            private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
            {
                BackgroundWorker worker = sender as BackgroundWorker;
    
                for (int i = 1; i <= 10 i++)
              {
                    if (worker.CancellationPending == true) //確認工作已取消
                    {
                        e.Cancel = true;
                        break;
                    }
                    else
                    {
                        // Perform a time consuming operation and report progress.
                        System.Threading.Thread.Sleep(500);
                        worker.ReportProgress(i * 10); //設定進度條內容
                    }
                }
            }
    
            // 更新進度畫面
            backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
            {
                resultLabel.Text = (e.ProgressPercentage.ToString() + "%");
            }
    
            // 定義結束時的工作
            private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
            {
                if (e.Cancelled == true)
                {
                    resultLabel.Text = "Canceled!";
                }
                else if (e.Error != null)
                {
                    resultLabel.Text = "Error: " + e.Error.Message;
                }
                else
                {
                    resultLabel.Text = "Done!";
                }
            }
    
    四、加入事件中

 

Week 14

如何解決問題

專題名稱:阿偉呷飽未

確認一個問題

流浪動物捐款不足

TA

吸引願意捐款但不會主動捐款的人

問題的成因

  1. 不知捐給誰(選擇障礙)
    • 不知道哪裡可以捐
      • 沒有宣傳
    • 不知道該選誰
      • 不知道哪裡最需要(要用在刀口)
      • 不知道會不會被詐騙
  2. 想要捐款,但金額很小(一塊兩塊)
    • 捐款成本高
      • 時間
      • 金錢
    • 門檻
  3. 沒回饋感(曾經捐過,但跟阿罵的腳一樣沒感覺)
    • 滿足心靈上的滿足
      • 虛榮心
      • 高社會興趣
      • 小卡片(帶來的快樂)
    • 物質上的滿足
      • 節稅
      • 小卡片(實體)
  4. 其他

找出成因的解法

  1. 由我們統一處理款項
    • 哪裡可以捐
      • 整理受捐款機構列表
      • 電子郵件(主動告知)
      • 幫他們進行宣傳
      • 其他
    • 不知道捐給誰
      • 透明轉捐款金額
      • 整理機構使用錢的方式
      • 整理機構的總資產跟使用金額是否成比例
      • 整理機溝的合法證明(導向政府核可立案、金額狀況、使用證明)
      • 讓捐款者指定捐款使用方式(例如 指定糧食 指定醫療)
  2. 想要小額捐款
    • 成本高
      • 多元支付
      • 儲值模式,可免費退款(退儲值金餘額)
      • 用儲值金捐款(大家的已捐點數累積到一定量才出去)
      • 網頁要有手機板(RWD)
    • 門檻高
      • 集資方式買物資
  3. 沒回饋感
    • 物質上回饋
      • 集點換小獎品
      • 寄Email(提醒功能)
      • 捐款就給他實質東西,例如,卡片、感謝狀、小禮物
    • 滿足心靈上的滿足
      • 擁有特殊權利,例如有特殊裝飾、vip待遇
      • 排行榜

執行解法

前端

  1. 使用 Vue 框架架設網頁
  2. 使用 Google Chart 製作統計圖表

後端

  1. 建立影音資料庫(DB2?)
  2. 貓狗資料庫(MongoDB)
  3. 捐款者資料庫
  4. 被捐款者資料庫 >> 上傳資訊功能 >> 轉換統計圖表
  5. 登入驗證JWT
  6. 串金流
  7. 通訊功能 >> 串LineBot(LineMessagingAPI(Nuget))
  8. 基金會資料庫(DB)

參考網站

  1. 將 TA 明確 鎖定客群1. 捐款模式>> 銀行轉帳 信用卡定期捐贈 電子支付2. 捐款動機> 捐款領養人主要是基於人道對待動物精神,因而捐款支持該協會終養流浪動物理念3. 年齡>> 20~404. 性別>> 女性七成5. 收入6. 收容所資金需求>> 重視財務運用的透明與可信度,也關心服務成果。

Week15

 

研究GCP SQL 怎麼做

  1. Create a Cloud SQL instance → 選SQL server(MS SQL)

Untitled

Untitled

2.填寫以下資料

  • 執行個體ID:就是幫你的這個ms sql取一個名字,等等創建好之後還會再建立一個database,然後建立table,那個才是真正的database,這邊只是為你的這個ms sql取名而已。
  • 根密碼:就是之後登入ms sql 的密碼(帳號預設sqlserver)
  • 區域:可以選在台灣,如果你人在台灣,比較快
  • 分區:他每個都代表一個區域,應該網路上可以找到每個選項代表哪裡,如果你不知道選哪個,就選跟我一樣的asia-east1-a
  • 資料庫版本:看你本地是哪個版本就選哪個

填完按下執行個體

!!!!設定選項記得要看!!!!(記得裡面都要檢查: https://ithelp.ithome.com.tw/articles/10201363

例如 IP 一開始沒開私人 IP (也就是內部IP)之後都無法再開!代表你無法用GCP Server 的 IP 去取你建置的 GCP SQL,你只剩外部 IP 可以操作GCP SQL..............

Untitled

補救

Untitled

3.連線設定

  1. 點SQL主控台的連線設定 → 公開IP
  2. 先找到你的 IP,網路上很多查詢網站,推薦一個:點我查詢

Untitled

  1. 建立資料庫

  2. SQL主控台,按下資料庫

  3. 按建立資料庫

Untitled

Untitled

5.本機SQL Server 連線(填入3要素)

  1. Server name
  2. Login
  3. password

Untitled

Server name查詢:回SQL主控台,點所有執行個體,可以看到你目前所有的 instance 的 公開IP位址→ 複製填入Server name

Untitled

Untitled

Login查詢:預設使用者為sqlserver填入Login(也可以點新增使用者-使用者)

Password查詢:預設password為建立執行個體時的sqlserver密碼(也可以點新增使用者-密碼)

Untitled

Untitled

登入成功!!

這時候就可以把你本地的資料庫export出來,然後連到 GCP 的 SQL 後,import進去拉~~

arrow
arrow

    太空人的內太空 發表在 痞客邦 留言(1) 人氣()