欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解C# ConcurrentBag的實現(xiàn)原理

 更新時間:2021年06月28日 10:18:55   作者:InCerry  
ConcurrentBag<T>實現(xiàn)了IProducerConsumerCollection<T>接口,該接口主要用于生產(chǎn)者消費者模式下,可見該類基本就是為生產(chǎn)消費者模式定制的。然后還實現(xiàn)了常規(guī)的IReadOnlyCollection<T>類,實現(xiàn)了該類就需要實現(xiàn)IEnumerable<T>、IEnumerable、 ICollection類

一、ConcurrentBag類

ConcurrentBag<T>對外提供的方法沒有List<T>那么多,但是同樣有Enumerable實現(xiàn)的擴展方法。類本身提供的方法如下所示。

名稱 說明
Add 將對象添加到 ConcurrentBag 中。
CopyTo 從指定數(shù)組索引開始,將 ConcurrentBag 元素復(fù)制到現(xiàn)有的一維 Array 中。
Equals(Object) 確定指定的 Object 是否等于當(dāng)前的 Object。 (繼承自 Object。)
Finalize 允許對象在“垃圾回收”回收之前嘗試釋放資源并執(zhí)行其他清理操作。 (繼承自 Object。)
GetEnumerator 返回循環(huán)訪問 ConcurrentBag 的枚舉器。
GetHashCode 用作特定類型的哈希函數(shù)。 (繼承自 Object。)
GetType 獲取當(dāng)前實例的 Type。 (繼承自 Object。)
MemberwiseClone 創(chuàng)建當(dāng)前 Object 的淺表副本。 (繼承自 Object。)
ToArray 將 ConcurrentBag 元素復(fù)制到新數(shù)組。
ToString 返回表示當(dāng)前對象的字符串。 (繼承自 Object。)
TryPeek 嘗試從 ConcurrentBag 返回一個對象但不移除該對象。
TryTake 嘗試從 ConcurrentBag 中移除并返回對象。

二、 ConcurrentBag線程安全實現(xiàn)原理

2.1、ConcurrentBag的私有字段

ConcurrentBag線程安全實現(xiàn)主要是通過它的數(shù)據(jù)存儲的結(jié)構(gòu)和細顆粒度的鎖。

public class ConcurrentBag<T> : IProducerConsumerCollection<T>, IReadOnlyCollection<T>
{
    // ThreadLocalList對象包含每個線程的數(shù)據(jù)
    ThreadLocal<ThreadLocalList> m_locals;

    // 這個頭指針和尾指針指向中的第一個和最后一個本地列表,這些本地列表分散在不同線程中
    // 允許在線程局部對象上枚舉
    volatile ThreadLocalList m_headList, m_tailList;

    // 這個標志是告知操作線程必須同步操作
    // 在GlobalListsLock 鎖中 設(shè)置
    bool m_needSync;
}

首選我們來看它聲明的私有字段,其中需要注意的是集合的數(shù)據(jù)是存放在ThreadLocal線程本地存儲中的。也就是說訪問它的每個線程會維護一個自己的集合數(shù)據(jù)列表,一個集合中的數(shù)據(jù)可能會存放在不同線程的本地存儲空間中,所以如果線程訪問自己本地存儲的對象,那么是沒有問題的,這就是實現(xiàn)線程安全的第一層,使用線程本地存儲數(shù)據(jù)。

然后可以看到ThreadLocalList m_headList, m_tailList;這個是存放著本地列表對象的頭指針和尾指針,通過這兩個指針,我們就可以通過遍歷的方式來訪問所有本地列表。它使用volatile修飾,不允許線程進行本地緩存,每個線程的讀寫都是直接操作在共享內(nèi)存上,這就保證了變量始終具有一致性。任何線程在任何時間進行讀寫操作均是最新值。

最后又定義了一個標志,這個標志告知操作線程必須進行同步操作,這是實現(xiàn)了一個細顆粒度的鎖,因為只有在幾個條件滿足的情況下才需要進行線程同步。

2.2、用于數(shù)據(jù)存儲的ThreadLocalList類

接下來我們來看一下ThreadLocalList類的構(gòu)造,該類就是實際存儲了數(shù)據(jù)的位置。實際上它是使用雙向鏈表這種結(jié)構(gòu)進行數(shù)據(jù)存儲。

[Serializable]
// 構(gòu)造了雙向鏈表的節(jié)點
internal class Node
{
    public Node(T value)
    {
        m_value = value;
    }
    public readonly T m_value;
    public Node m_next;
    public Node m_prev;
}

/// <summary>
/// 集合操作類型
/// </summary>
internal enum ListOperation
{
    None,
    Add,
    Take
};

/// <summary>
/// 線程鎖定的類
/// </summary>
internal class ThreadLocalList
{
    // 雙向鏈表的頭結(jié)點 如果為null那么表示鏈表為空
    internal volatile Node m_head;

    // 雙向鏈表的尾節(jié)點
    private volatile Node m_tail;

    // 定義當(dāng)前對List進行操作的種類 
    // 與前面的 ListOperation 相對應(yīng)
    internal volatile int m_currentOp;

    // 這個列表元素的計數(shù)
    private int m_count;

    // The stealing count
    // 這個不是特別理解 好像是在本地列表中 刪除某個Node 以后的計數(shù)
    internal int m_stealCount;

    // 下一個列表 可能會在其它線程中
    internal volatile ThreadLocalList m_nextList;

    // 設(shè)定鎖定是否已進行
    internal bool m_lockTaken;

    // The owner thread for this list
    internal Thread m_ownerThread;

    // 列表的版本,只有當(dāng)列表從空變?yōu)榉强战y(tǒng)計是底層
    internal volatile int m_version;

    /// <summary>
    /// ThreadLocalList 構(gòu)造器
    /// </summary>
    /// <param name="ownerThread">擁有這個集合的線程</param>
    internal ThreadLocalList(Thread ownerThread)
    {
        m_ownerThread = ownerThread;
    }
    /// <summary>
    /// 添加一個新的item到鏈表首部
    /// </summary>
    /// <param name="item">The item to add.</param>
    /// <param name="updateCount">是否更新計數(shù).</param>
    internal void Add(T item, bool updateCount)
    {
        checked
        {
            m_count++;
        }
        Node node = new Node(item);
        if (m_head == null)
        {
            Debug.Assert(m_tail == null);
            m_head = node;
            m_tail = node;
            m_version++; // 因為進行初始化了,所以將空狀態(tài)改為非空狀態(tài)
        }
        else
        {
            // 使用頭插法 將新的元素插入鏈表
            node.m_next = m_head;
            m_head.m_prev = node;
            m_head = node;
        }
        if (updateCount) // 更新計數(shù)以避免此添加同步時溢出
        {
            m_count = m_count - m_stealCount;
            m_stealCount = 0;
        }
    }

    /// <summary>
    /// 從列表的頭部刪除一個item
    /// </summary>
    /// <param name="result">The removed item</param>
    internal void Remove(out T result)
    {
        // 雙向鏈表刪除頭結(jié)點數(shù)據(jù)的流程
        Debug.Assert(m_head != null);
        Node head = m_head;
        m_head = m_head.m_next;
        if (m_head != null)
        {
            m_head.m_prev = null;
        }
        else
        {
            m_tail = null;
        }
        m_count--;
        result = head.m_value;

    }

    /// <summary>
    /// 返回列表頭部的元素
    /// </summary>
    /// <param name="result">the peeked item</param>
    /// <returns>True if succeeded, false otherwise</returns>
    internal bool Peek(out T result)
    {
        Node head = m_head;
        if (head != null)
        {
            result = head.m_value;
            return true;
        }
        result = default(T);
        return false;
    }

    /// <summary>
    /// 從列表的尾部獲取一個item
    /// </summary>
    /// <param name="result">the removed item</param>
    /// <param name="remove">remove or peek flag</param>
    internal void Steal(out T result, bool remove)
    {
        Node tail = m_tail;
        Debug.Assert(tail != null);
        if (remove) // Take operation
        {
            m_tail = m_tail.m_prev;
            if (m_tail != null)
            {
                m_tail.m_next = null;
            }
            else
            {
                m_head = null;
            }
            // Increment the steal count
            m_stealCount++;
        }
        result = tail.m_value;
    }


    /// <summary>
    /// 獲取總計列表計數(shù), 它不是線程安全的, 如果同時調(diào)用它, 則可能提供不正確的計數(shù)
    /// </summary>
    internal int Count
    {
        get
        {
            return m_count - m_stealCount;
        }
    }
}

從上面的代碼中我們可以更加驗證之前的觀點,就是ConcurentBag<T>在一個線程中存儲數(shù)據(jù)時,使用的是雙向鏈表,ThreadLocalList實現(xiàn)了一組對鏈表增刪改查的方法。

2.3、ConcurrentBag實現(xiàn)新增元素

接下來我們看一看ConcurentBag<T>是如何新增元素的。

/// <summary>
/// 嘗試獲取無主列表,無主列表是指線程已經(jīng)被暫?;蛘呓K止,但是集合中的部分數(shù)據(jù)還存儲在那里
/// 這是避免內(nèi)存泄漏的方法
/// </summary>
/// <returns></returns>
private ThreadLocalList GetUnownedList()
{
    //此時必須持有全局鎖
    Contract.Assert(Monitor.IsEntered(GlobalListsLock));

    // 從頭線程列表開始枚舉 找到那些已經(jīng)被關(guān)閉的線程
    // 將它所在的列表對象 返回
    ThreadLocalList currentList = m_headList;
    while (currentList != null)
    {
        if (currentList.m_ownerThread.ThreadState == System.Threading.ThreadState.Stopped)
        {
            currentList.m_ownerThread = Thread.CurrentThread; // the caller should acquire a lock to make this line thread safe
            return currentList;
        }
        currentList = currentList.m_nextList;
    }
    return null;
}
/// <summary>
/// 本地幫助方法,通過線程對象檢索線程線程本地列表
/// </summary>
/// <param name="forceCreate">如果列表不存在,那么創(chuàng)建新列表</param>
/// <returns>The local list object</returns>
private ThreadLocalList GetThreadList(bool forceCreate)
{
    ThreadLocalList list = m_locals.Value;

    if (list != null)
    {
        return list;
    }
    else if (forceCreate)
    {
        // 獲取用于更新操作的 m_tailList 鎖
        lock (GlobalListsLock)
        {
            // 如果頭列表等于空,那么說明集合中還沒有元素
            // 直接創(chuàng)建一個新的
            if (m_headList == null)
            {
                list = new ThreadLocalList(Thread.CurrentThread);
                m_headList = list;
                m_tailList = list;
            }
            else
            {
			   // ConcurrentBag內(nèi)的數(shù)據(jù)是以雙向鏈表的形式分散存儲在各個線程的本地區(qū)域中
                // 通過下面這個方法 可以找到那些存儲有數(shù)據(jù) 但是已經(jīng)被停止的線程
                // 然后將已停止線程的數(shù)據(jù) 移交到當(dāng)前線程管理
                list = GetUnownedList();
                // 如果沒有 那么就新建一個列表 然后更新尾指針的位置
                if (list == null)
                {
                    list = new ThreadLocalList(Thread.CurrentThread);
                    m_tailList.m_nextList = list;
                    m_tailList = list;
                }
            }
            m_locals.Value = list;
        }
    }
    else
    {
        return null;
    }
    Debug.Assert(list != null);
    return list;
}
/// <summary>
/// Adds an object to the <see cref="ConcurrentBag{T}"/>.
/// </summary>
/// <param name="item">The object to be added to the
/// <see cref="ConcurrentBag{T}"/>. The value can be a null reference
/// (Nothing in Visual Basic) for reference types.</param>
public void Add(T item)
{
    // 獲取該線程的本地列表, 如果此線程不存在, 則創(chuàng)建一個新列表 (第一次調(diào)用 add)
    ThreadLocalList list = GetThreadList(true);
    // 實際的數(shù)據(jù)添加操作 在AddInternal中執(zhí)行
    AddInternal(list, item);
}

/// <summary>
/// </summary>
/// <param name="list"></param>
/// <param name="item"></param>
private void AddInternal(ThreadLocalList list, T item)
{
    bool lockTaken = false;
    try
    {
		#pragma warning disable 0420
        Interlocked.Exchange(ref list.m_currentOp, (int)ListOperation.Add);
		#pragma warning restore 0420
	    // 同步案例:
        // 如果列表計數(shù)小于兩個, 因為是雙向鏈表的關(guān)系 為了避免與任何竊取線程發(fā)生沖突 必須獲取鎖
        // 如果設(shè)置了 m_needSync, 這意味著有一個線程需要凍結(jié)包 也必須獲取鎖
        if (list.Count < 2 || m_needSync)
        {
            // 將其重置為None 以避免與竊取線程的死鎖
            list.m_currentOp = (int)ListOperation.None;
            // 鎖定當(dāng)前對象
            Monitor.Enter(list, ref lockTaken);
        }
        // 調(diào)用 ThreadLocalList.Add方法 將數(shù)據(jù)添加到雙向鏈表中
        // 如果已經(jīng)鎖定 那么說明線程安全  可以更新Count 計數(shù)
        list.Add(item, lockTaken);
    }
    finally
    {
        list.m_currentOp = (int)ListOperation.None;
        if (lockTaken)
        {
            Monitor.Exit(list);
        }
    }
}

從上面代碼中,我們可以很清楚的知道Add()方法是如何運行的,其中的關(guān)鍵就是GetThreadList()方法,通過該方法可以獲取當(dāng)前線程的數(shù)據(jù)存儲列表對象,假如不存在數(shù)據(jù)存儲列表,它會自動創(chuàng)建或者通過GetUnownedList()方法來尋找那些被停止但是還存儲有數(shù)據(jù)列表的線程,然后將數(shù)據(jù)列表返回給當(dāng)前線程中,防止了內(nèi)存泄漏。

在數(shù)據(jù)添加的過程中,實現(xiàn)了細顆粒度的lock同步鎖,所以性能會很高。刪除和其它操作與新增類似,本文不再贅述。

2.4、ConcurrentBag 如何實現(xiàn)迭代器模式

看完上面的代碼后,我很好奇ConcurrentBag<T>是如何實現(xiàn)IEnumerator來實現(xiàn)迭代訪問的,因為ConcurrentBag<T>是通過分散在不同線程中的ThreadLocalList來存儲數(shù)據(jù)的,那么在實現(xiàn)迭代器模式時,過程會比較復(fù)雜。

后面再查看了源碼之后,發(fā)現(xiàn)ConcurrentBag<T>為了實現(xiàn)迭代器模式,將分在不同線程中的數(shù)據(jù)全都存到一個List<T>集合中,然后返回了該副本的迭代器。所以每次訪問迭代器,它都會新建一個List<T>的副本,這樣雖然浪費了一定的存儲空間,但是邏輯上更加簡單了。

/// <summary>
/// 本地幫助器方法釋放所有本地列表鎖
/// </summary>
private void ReleaseAllLocks()
{
    // 該方法用于在執(zhí)行線程同步以后 釋放掉所有本地鎖
    // 通過遍歷每個線程中存儲的 ThreadLocalList對象 釋放所占用的鎖
    ThreadLocalList currentList = m_headList;
    while (currentList != null)
    {

        if (currentList.m_lockTaken)
        {
            currentList.m_lockTaken = false;
            Monitor.Exit(currentList);
        }
        currentList = currentList.m_nextList;
    }
}

/// <summary>
/// 從凍結(jié)狀態(tài)解凍包的本地幫助器方法
/// </summary>
/// <param name="lockTaken">The lock taken result from the Freeze method</param>
private void UnfreezeBag(bool lockTaken)
{
    // 首先釋放掉 每個線程中 本地變量的鎖
    // 然后釋放全局鎖
    ReleaseAllLocks();
    m_needSync = false;
    if (lockTaken)
    {
        Monitor.Exit(GlobalListsLock);
    }
}

/// <summary>
/// 本地幫助器函數(shù)等待所有未同步的操作
/// </summary>
private void WaitAllOperations()
{
    Contract.Assert(Monitor.IsEntered(GlobalListsLock));

    ThreadLocalList currentList = m_headList;
    // 自旋等待 等待其它操作完成
    while (currentList != null)
    {
        if (currentList.m_currentOp != (int)ListOperation.None)
        {
            SpinWait spinner = new SpinWait();
            // 有其它線程進行操作時,會將cuurentOp 設(shè)置成 正在操作的枚舉
            while (currentList.m_currentOp != (int)ListOperation.None)
            {
                spinner.SpinOnce();
            }
        }
        currentList = currentList.m_nextList;
    }
}

/// <summary>
/// 本地幫助器方法獲取所有本地列表鎖
/// </summary>
private void AcquireAllLocks()
{
    Contract.Assert(Monitor.IsEntered(GlobalListsLock));

    bool lockTaken = false;
    ThreadLocalList currentList = m_headList;
    
    // 遍歷每個線程的ThreadLocalList 然后獲取對應(yīng)ThreadLocalList的鎖
    while (currentList != null)
    {
        // 嘗試/最后 bllock 以避免在獲取鎖和設(shè)置所采取的標志之間的線程港口
        try
        {
            Monitor.Enter(currentList, ref lockTaken);
        }
        finally
        {
            if (lockTaken)
            {
                currentList.m_lockTaken = true;
                lockTaken = false;
            }
        }
        currentList = currentList.m_nextList;
    }
}

/// <summary>
/// Local helper method to freeze all bag operations, it
/// 1- Acquire the global lock to prevent any other thread to freeze the bag, and also new new thread can be added
/// to the dictionary
/// 2- Then Acquire all local lists locks to prevent steal and synchronized operations
/// 3- Wait for all un-synchronized operations to be done
/// </summary>
/// <param name="lockTaken">Retrieve the lock taken result for the global lock, to be passed to Unfreeze method</param>
private void FreezeBag(ref bool lockTaken)
{
    Contract.Assert(!Monitor.IsEntered(GlobalListsLock));

    // 全局鎖定可安全地防止多線程調(diào)用計數(shù)和損壞 m_needSync
    Monitor.Enter(GlobalListsLock, ref lockTaken);

    // 這將強制同步任何將來的添加/執(zhí)行操作
    m_needSync = true;

    // 獲取所有列表的鎖
    AcquireAllLocks();

    // 等待所有操作完成
    WaitAllOperations();
}

/// <summary>
/// 本地幫助器函數(shù)返回列表中的包項, 這主要由 CopyTo 和 ToArray 使用。
/// 這不是線程安全, 應(yīng)該被稱為凍結(jié)/解凍袋塊
/// 本方法是私有的 只有使用 Freeze/UnFreeze之后才是安全的 
/// </summary>
/// <returns>List the contains the bag items</returns>
private List<T> ToList()
{
    Contract.Assert(Monitor.IsEntered(GlobalListsLock));
	// 創(chuàng)建一個新的List
    List<T> list = new List<T>();
    ThreadLocalList currentList = m_headList;
    // 遍歷每個線程中的ThreadLocalList 將里面的Node的數(shù)據(jù) 添加到list中
    while (currentList != null)
    {
        Node currentNode = currentList.m_head;
        while (currentNode != null)
        {
            list.Add(currentNode.m_value);
            currentNode = currentNode.m_next;
        }
        currentList = currentList.m_nextList;
    }

    return list;
}

/// <summary>
/// Returns an enumerator that iterates through the <see
/// cref="ConcurrentBag{T}"/>.
/// </summary>
/// <returns>An enumerator for the contents of the <see
/// cref="ConcurrentBag{T}"/>.</returns>
/// <remarks>
/// The enumeration represents a moment-in-time snapshot of the contents
/// of the bag.  It does not reflect any updates to the collection after 
/// <see cref="GetEnumerator"/> was called.  The enumerator is safe to use
/// concurrently with reads from and writes to the bag.
/// </remarks>
public IEnumerator<T> GetEnumerator()
{
    // Short path if the bag is empty
    if (m_headList == null)
        return new List<T>().GetEnumerator(); // empty list

    bool lockTaken = false;
    try
    {
        // 首先凍結(jié)整個 ConcurrentBag集合
        FreezeBag(ref lockTaken);
        // 然后ToList 再拿到 List的 IEnumerator
        return ToList().GetEnumerator();
    }
    finally
    {
        UnfreezeBag(lockTaken);
    }
}

由上面的代碼可知道,為了獲取迭代器對象,總共進行了三步主要的操作。

1.使用FreezeBag()方法,凍結(jié)整個ConcurrentBag<T>集合。因為需要生成集合的List<T>副本,生成副本期間不能有其它線程更改損壞數(shù)據(jù)。

2.將ConcurrrentBag<T>生成List<T>副本。因為ConcurrentBag<T>存儲數(shù)據(jù)的方式比較特殊,直接實現(xiàn)迭代器模式困難,考慮到線程安全和邏輯,最佳的辦法是生成一個副本。

3.完成以上操作以后,就可以使用UnfreezeBag()方法解凍整個集合。

那么FreezeBag()方法是如何來凍結(jié)整個集合的呢?也是分為三步走。

1.首先獲取全局鎖,通過 Monitor.Enter(GlobalListsLock, ref lockTaken);這樣一條語句,這樣其它線程就不能凍結(jié)集合。

2.然后獲取所有線程中ThreadLocalList的鎖,通過`AcquireAllLocks()方法來遍歷獲取。這樣其它線程就不能對它進行操作損壞數(shù)據(jù)。

3.等待已經(jīng)進入了操作流程線程結(jié)束,通過 WaitAllOperations()方法來實現(xiàn),該方法會遍歷每一個ThreadLocalList對象的m_currentOp屬性,確保全部處于None操作。

完成以上流程后,那么就是真正的凍結(jié)了整個ConcurrentBag<T>集合,要解凍的話也類似。在此不再贅述。

三、總結(jié)

下面給出一張圖,描述了ConcurrentBag<T>是如何存儲數(shù)據(jù)的。通過每個線程中的ThreadLocal來實現(xiàn)線程本地存儲,每個線程中都有這樣的結(jié)構(gòu),互不干擾。然后每個線程中的m_headList總是指向ConcurrentBag<T>的第一個列表,m_tailList指向最后一個列表。列表與列表之間通過m_locals 下的 m_nextList相連,構(gòu)成一個單鏈表。

數(shù)據(jù)存儲在每個線程的m_locals中,通過Node類構(gòu)成一個雙向鏈表。

PS: 要注意m_tailListm_headList并不是存儲在ThreadLocal中,而是所有的線程共享一份。

以上就是詳解C# ConcurrentBag的實現(xiàn)原理的詳細內(nèi)容,更多關(guān)于C# ConcurrentBag的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 使用C#快速搭建一個在windows運行的exe應(yīng)用

    使用C#快速搭建一個在windows運行的exe應(yīng)用

    這篇文章主要介紹了使用C#快速搭建一個在windows運行的exe應(yīng)用,這是一個比較舊的內(nèi)容,但是一直都沒有空寫,今天花點時間,把我掌握的C# 分享給初學(xué)的人或者感興趣的人,希望能對你有一定幫助,感興趣的小伙伴跟著小編一起來看看吧
    2024-07-07
  • C#軟件注冊碼的實現(xiàn)代碼

    C#軟件注冊碼的實現(xiàn)代碼

    開發(fā)軟件時,當(dāng)用到商業(yè)用途時,注冊碼與激活碼就顯得很重要了,現(xiàn)在的軟件技術(shù)實在在強了,各種國內(nèi)外大型軟件都有注冊機制,但我們學(xué)習(xí)的是技術(shù)
    2013-05-05
  • C# L型棋牌覆蓋實現(xiàn)代碼與效果

    C# L型棋牌覆蓋實現(xiàn)代碼與效果

    C# L型棋牌覆蓋實現(xiàn)代碼與效果,需要的朋友可以參考一下
    2013-04-04
  • C#使用iTextSharp將PDF轉(zhuǎn)成文本的方法

    C#使用iTextSharp將PDF轉(zhuǎn)成文本的方法

    這篇文章主要介紹了C#使用iTextSharp將PDF轉(zhuǎn)成文本的方法,涉及C#操作pdf文件的相關(guān)技巧,需要的朋友可以參考下
    2015-05-05
  • C#實現(xiàn)狀態(tài)欄提示信息功能的示例

    C#實現(xiàn)狀態(tài)欄提示信息功能的示例

    今天小編就為大家分享一篇C#實現(xiàn)狀態(tài)欄提示信息功能的示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-06-06
  • C#中Equals方法的常見誤解

    C#中Equals方法的常見誤解

    equals方法被用來檢測兩個對象是否相等,即兩個對象的內(nèi)容是否相等。本文主要介紹的是equals方法,初學(xué)者對它幾個常見的誤解,一起來看。
    2015-10-10
  • C#委托現(xiàn)實示例分析

    C#委托現(xiàn)實示例分析

    這篇文章主要介紹了C#委托現(xiàn)實,實例分析了C#委托的使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-04-04
  • C#?重寫Notification提示窗口的示例代碼

    C#?重寫Notification提示窗口的示例代碼

    本文主要介紹了C#?重寫Notification提示窗口的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • C#利用GDI+繪制旋轉(zhuǎn)文字等效果實例

    C#利用GDI+繪制旋轉(zhuǎn)文字等效果實例

    這篇文章主要介紹了C#利用GDI+繪制旋轉(zhuǎn)文字等效果實例,是非常實用的重要技巧,需要的朋友可以參考下
    2014-09-09
  • C# async/await任務(wù)超時處理的實現(xiàn)

    C# async/await任務(wù)超時處理的實現(xiàn)

    本文主要介紹了C# async/await任務(wù)超時處理的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02

最新評論