PHP 源碼學習之線程安全

来源:http://www.cnblogs.com/chenpingzhao/archive/2016/09/21/5894055.html
-Advertisement-
Play Games

從作用域上來說,C語言可以定義4種不同的變數:全局變數,靜態全局變數,局部變數,靜態局部變數。 下麵僅從函數作用域的角度分析一下不同的變數,假設所有變數聲明不重名。 全局變數,在函數外聲明,例如,int gVar;。全局變數,所有函數共用,在任何地方出現這個變數名都是指這個變數 靜態全局變數(sta ...


從作用域上來說,C語言可以定義4種不同的變數:全局變數,靜態全局變數,局部變數,靜態局部變數。

下麵僅從函數作用域的角度分析一下不同的變數,假設所有變數聲明不重名。

  • 全局變數,在函數外聲明,例如,int gVar;。全局變數,所有函數共用,在任何地方出現這個變數名都是指這個變數

  • 靜態全局變數(static sgVar),其實也是所有函數共用,但是這個會有編譯器的限制,算是編譯器提供的一種功能

  • 局部變數(函數/塊內的int var;),不共用,函數的多次執行中涉及的這個變數都是相互獨立的,他們只是重名的不同變數而已

  • 局部靜態變數(函數中的static int sVar;),本函數間共用,函數的每一次執行中涉及的這個變數都是這個同一個變數

上面幾種作用域都是從函數的角度來定義作用域的,可以滿足所有我們對單線程編程中變數的共用情況。 現在我們來分析一下多線程的情況。在多線程中,多個線程共用除函數調用棧之外的其他資源。 因此上面幾種作用域從定義來看就變成了。

  • 全局變數,所有函數共用,因此所有的線程共用,不同線程中出現的不同變數都是這同一個變數

  • 靜態全局變數,所有函數共用,也是所有線程共用

  • 局部變數,此函數的各次執行中涉及的這個變數沒有聯繫,因此,也是各個線程間也是不共用的

  • 靜態局部變數,本函數間共用,函數的每次執行涉及的這個變數都是同一個變數,因此,各個線程是共用的

一、緣起TSRM

在多線程系統中,進程保留著資源所有權的屬性,而多個併發執行流是執行在進程中運行的線程。 如 Apache2 中的 worker,主控制進程生成多個子進程,每個子進程中包含固定的線程數,各個線程獨立地處理請求。 同樣,為了不在請求到來時再生成線程,MinSpareThreads 和 MaxSpareThreads 設置了最少和最多的空閑線程數; 而 MaxClients 設置了所有子進程中的線程總數。如果現有子進程中的線程總數不能滿足負載,控制進程將派生新的子進程。

當 PHP 運行在如上類似的多線程伺服器時,此時的 PHP 處在多線程的生命周期中。 在一定的時間內,一個進程空間中會存在多個線程,同一進程中的多個線程公用模塊初始化後的全局變數, 如果和 PHP 在 CLI 模式下一樣運行腳本,則多個線程會試圖讀寫一些存儲在進程記憶體空間的公共資源(如在多個線程公用的模塊初始化後的函數外會存在較多的全局變數),

此時這些線程訪問的記憶體地址空間相同,當一個線程修改時,會影響其它線程,這種共用會提高一些操作的速度, 但是多個線程間就產生了較大的耦合,並且當多個線程併發時,就會產生常見的數據一致性問題或資源競爭等併發常見問題, 比如多次運行結果和單線程運行的結果不一樣。如果每個線程中對全局變數、靜態變數只有讀操作,而無寫操作,則這些個全局變數就是線程安全的,只是這種情況不太現實。

為解決線程的併發問題,PHP 引入了 TSRM: 線程安全資源管理器(Thread Safe Resource Manager)。 TRSM 的實現代碼在 PHP 源碼的 /TSRM 目錄下,調用隨處可見,通常,我們稱之為 TSRM 層。 一般來說,TSRM 層只會在被指明需要的時候才會在編譯時啟用(比如,Apache2+worker MPM,一個基於線程的MPM), 因為 Win32 下的 Apache 來說,是基於多線程的,所以這個層在 Win32 下總是被開啟的。

二、TSRM的實現

進程保留著資源所有權的屬性,線程做併發訪問,PHP 中引入的 TSRM 層關註的是對共用資源的訪問, 這裡的共用資源是線程之間共用的存在於進程的記憶體空間的全局變數。 當 PHP 在單進程模式下時,一個變數被聲明在任何函數之外時,就成為一個全局變數。

首先定義瞭如下幾個非常重要的全局變數(這裡的全局變數是多線程共用的)。

/* The memory manager table */
static tsrm_tls_entry   **tsrm_tls_table=NULL;
static int              tsrm_tls_table_size;
static ts_rsrc_id       id_count;

/* The resource sizes table */
static tsrm_resource_type   *resource_types_table=NULL;
static int                  resource_types_table_size;

**tsrm_tls_table 的全拼 thread safe resource manager thread local storage table,用來存放各個線程的 tsrm_tls_entry 鏈表。

tsrm_tls_table_size 用來表示 **tsrm_tls_table 的大小。

id_count 作為全局變數資源的 id 生成器,是全局唯一且遞增的。

*resource_types_table 用來存放全局變數對應的資源。

resource_types_table_size 表示 *resource_types_table 的大小。

其中涉及到兩個關鍵的數據結構 tsrm_tls_entry 和 tsrm_resource_type

typedef struct _tsrm_tls_entry tsrm_tls_entry;

struct _tsrm_tls_entry {
    void **storage;// 本節點的全局變數數組
    int count;// 本節點全局變數數
    THREAD_T thread_id;// 本節點對應的線程 ID
    tsrm_tls_entry *next;// 下一個節點的指針
};

typedef struct {
    size_t size;// 被定義的全局變數結構體的大小
    ts_allocate_ctor ctor;// 被定義的全局變數的構造方法指針
    ts_allocate_dtor dtor;// 被定義的全局變數的析構方法指針
    int done;
} tsrm_resource_type;

當新增一個全局變數時,id_count 會自增1(加上線程互斥鎖)。然後根據全局變數需要的記憶體、構造函數、析構函數生成對應的資源tsrm_resource_type,存入 *resource_types_table,再根據該資源,為每個線程的所有tsrm_tls_entry節點添加其對應的全局變數。

有了這個大致的瞭解,下麵通過仔細分析 TSRM 環境的初始化和資源 ID 的分配來理解這一完整的過程。

TSRM 環境的初始化

模塊初始化階段,在各個 SAPI main 函數中通過調用 tsrm_startup 來初始化 TSRM 環境。tsrm_startup 函數會傳入兩個非常重要的參數,一個是 expected_threads,表示預期的線程數, 一個是 expected_resources,表示預期的資源數。不同的 SAPI 有不同的初始化值,比如mod_php5,cgi 這些都是一個線程一個資源。

TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
    /* code... */

    tsrm_tls_table_size = expected_threads; // SAPI 初始化時預計分配的線程數,一般都為1

    tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));

    /* code... */

    id_count=0;

    resource_types_table_size = expected_resources; // SAPI 初始化時預先分配的資源表大小,一般也為1

    resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));

    /* code... */

    return 1;
}

精簡出其中完成的三個重要的工作,初始化了 tsrm_tls_table 鏈表、resource_types_table 數組,以及 id_count。而這三個全局變數是所有線程共用的,實現了線程間的記憶體管理的一致性。

資源 ID 的分配

我們知道初始化一個全局變數時需要使用 ZEND_INIT_MODULE_GLOBALS 巨集(下麵的數組擴展的例子中會有說明),而其實際則是調用的 ts_allocate_id 函數在多線程環境下申請一個全局變數,然後返回分配的資源 ID。代碼雖然比較多,實際還是比較清晰,下麵附帶註解進行說明:

TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor)
{
    int i;

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new resource id, %d bytes", size));

    // 加上多線程互斥鎖
    tsrm_mutex_lock(tsmm_mutex);

    /* obtain a resource id */
    *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); // 全局靜態變數 id_count 加 1
    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));

    /* store the new resource type in the resource sizes table */
    // 因為 resource_types_table_size 是有初始值的(expected_resources),所以不一定每次都要擴充記憶體
    if (resource_types_table_size < id_count) {
        resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);
        if (!resource_types_table) {
            tsrm_mutex_unlock(tsmm_mutex);
            TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));
            *rsrc_id = 0;
            return 0;
        }
        resource_types_table_size = id_count;
    }

    // 將全局變數結構體的大小、構造函數和析構函數都存入 tsrm_resource_type 的數組 resource_types_table 中
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].size = size;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].ctor = ctor;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].dtor = dtor;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].done = 0;

    /* enlarge the arrays for the already active threads */
    // PHP內核會接著遍歷所有線程為每一個線程的 tsrm_tls_entry
    for (i=0; i<tsrm_tls_table_size; i++) {
        tsrm_tls_entry *p = tsrm_tls_table[i];

        while (p) {
            if (p->count < id_count) {
                int j;

                p->storage = (void *) realloc(p->storage, sizeof(void *)*id_count);
                for (j=p->count; j<id_count; j++) {
                    // 在該線程中為全局變數分配需要的記憶體空間
                    p->storage[j] = (void *) malloc(resource_types_table[j].size);
                    if (resource_types_table[j].ctor) {
                        // 最後對 p->storage[j] 地址存放的全局變數進行初始化,
                        // 這裡 ts_allocate_ctor 函數的第二個參數不知道為什麼預留,整個項目中實際都未用到過,對比PHP7發現第二個參數也的確已經移除了
                        resource_types_table[j].ctor(p->storage[j], &p->storage);
                    }
                }
                p->count = id_count;
            }
            p = p->next;
        }
    }

    // 取消線程互斥鎖
    tsrm_mutex_unlock(tsmm_mutex);

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id));
    return *rsrc_id;
}

當通過 ts_allocate_id 函數分配全局資源 ID 時,PHP 內核會先加上互斥鎖,確保生成的資源 ID 的唯一,這裡鎖的作用是在時間維度將併發的內容變成串列,因為併發的根本問題就是時間的問題。當加鎖以後,id_count 自增,生成一個資源 ID,生成資源 ID 後,就會給當前資源 ID 分配存儲的位置, 每一個資源都會存儲在 resource_types_table 中,當一個新的資源被分配時,就會創建一個 tsrm_resource_type。 所有 tsrm_resource_type 以數組的方式組成 tsrm_resource_table,其下標就是這個資源的 ID。 其實我們可以將 tsrm_resource_table 看做一個 HASH 表,key 是資源 ID,value 是 tsrm_resource_type 結構(任何一個數組都可以看作一個 HASH 表,如果數組的key 值有意義的話)。

在分配了資源 ID 後,PHP 內核會接著遍歷所有線程為每一個線程的 tsrm_tls_entry 分配這個線程全局變數需要的記憶體空間。 這裡每個線程全局變數的大小在各自的調用處指定(也就是全局變數結構體的大小)。最後對地址存放的全局變數進行初始化。為此我畫了一張圖予以說明

圖8.2 PHP 線程安全示意圖

上圖中還有一個困惑的地方,tsrm_tls_table 的元素是如何添加的,鏈表是如何實現的。我們把這個問題先留著,後面會討論。

每一次的 ts_allocate_id 調用,PHP 內核都會遍歷所有線程併為每一個線程分配相應資源, 如果這個操作是在PHP生命周期的請求處理階段進行,豈不是會重覆調用?

PHP 考慮了這種情況,ts_allocate_id 的調用在模塊初始化時就調用了。

TSRM 啟動後,在模塊初始化過程中會遍歷每個擴展的模塊初始化方法, 擴展的全局變數在擴展的實現代碼開頭聲明,在 MINIT 方法中初始化。 其在初始化時會知會 TSRM 申請的全局變數以及大小,這裡所謂的知會操作其實就是前面所說的 ts_allocate_id 函數。 TSRM 在記憶體池中分配並註冊,然後將資源ID返回給擴展。

全局變數的使用

以標準的數組擴展為例,首先會聲明當前擴展的全局變數。

ZEND_DECLARE_MODULE_GLOBALS(array)

然後在模塊初始化時會調用全局變數初始化巨集初始化 array,比如分配記憶體空間操作。

static void php_array_init_globals(zend_array_globals *array_globals)
{
    memset(array_globals, 0, sizeof(zend_array_globals));
}

/* code... */

PHP_MINIT_FUNCTION(array) /* {{{ */
{
    ZEND_INIT_MODULE_GLOBALS(array, php_array_init_globals, NULL);
    /* code... */
}

這裡的聲明和初始化操作都是區分ZTS和非ZTS。

#ifdef ZTS

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    ts_rsrc_id module_name##_globals_id;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    ts_allocate_id(&module_name##_globals_id, sizeof(zend_##module_name##_globals), (ts_allocate_ctor) globals_ctor, (ts_allocate_dtor) globals_dtor);

#else

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    zend_##module_name##_globals module_name##_globals;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    globals_ctor(&module_name##_globals);

#endif

對於非ZTS的情況,直接聲明變數,初始化變數;對於ZTS情況,PHP內核會添加TSRM,不再是聲明全局變數,而是用ts_rsrc_id代替,初始化時也不再是初始化變數,而是調用ts_allocate_id函數在多線程環境中給當前這個模塊申請一個全局變數並返回資源ID。其中,資源ID變數名由模塊名加global_id組成。

如果要調用當前擴展的全局變數,則使用:ARRAYG(v),這個巨集的定義:

#ifdef ZTS
#define ARRAYG(v) TSRMG(array_globals_id, zend_array_globals *, v)
#else
#define ARRAYG(v) (array_globals.v)
#endif

如果是非ZTS則直接調用全局變數的屬性欄位,如果是ZTS,則需要通過TSRMG獲取變數。

TSRMG的定義:

#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)

去掉這一堆括弧,TSRMG巨集的意思就是從tsrm_ls中按資源ID獲取全局變數,並返回對應變數的屬性欄位。

那麼現在的問題是這個 tsrm_ls 從哪裡來的?

tsrm_ls 的初始化

tsrm_ls 通過 ts_resource(0) 初始化。展開實際最後調用的是 ts_resource_ex(0,NULL) 。下麵將 ts_resource_ex 一些巨集展開,線程以 pthread 為例。

#define THREAD_HASH_OF(thr,ts)  (unsigned long)thr%(unsigned long)ts

static MUTEX_T tsmm_mutex;

void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
{
    THREAD_T thread_id;
    int hash_value;
    tsrm_tls_entry *thread_resources;

    // tsrm_tls_table 在 tsrm_startup 已初始化完畢
    if(tsrm_tls_table) {
        // 初始化時 th_id = NULL;
        if (!th_id) {

            //第一次為空 還未執行過 pthread_setspecific 所以 thread_resources 指針為空
            thread_resources = pthread_getspecific(tls_key);

            if(thread_resources){
                TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);
            }

            thread_id = pthread_self();
        } else {
            thread_id = *th_id;
        }
    }
    // 上鎖
    pthread_mutex_lock(tsmm_mutex);

    // 直接取餘,將其值作為數組下標,將不同的線程散列分佈在 tsrm_tls_table 中
    hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size);
    // 在 SAPI 調用 tsrm_startup 之後,tsrm_tls_table_size = expected_threads
    thread_resources = tsrm_tls_table[hash_value];

    if (!thread_resources) {
        // 如果還沒,則新分配。
        allocate_new_resource(&tsrm_tls_table[hash_value], thread_id);
        // 分配完畢之後再執行到下麵的 else 區間
        return ts_resource_ex(id, &thread_id);
    } else {
         do {
            // 沿著鏈表逐個匹配
            if (thread_resources->thread_id == thread_id) {
                break;
            }
            if (thread_resources->next) {
                thread_resources = thread_resources->next;
            } else {
                // 鏈表的盡頭仍然沒有找到,則新分配,接到鏈表的末尾
                allocate_new_resource(&thread_resources->next, thread_id);
                return ts_resource_ex(id, &thread_id);
            }
         } while (thread_resources);
    }

    TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);

    // 解鎖
    pthread_mutex_unlock(tsmm_mutex);

}

而 allocate_new_resource 則是為新的線程在對應的鏈表中分配記憶體,並且將所有的全局變數都加入到其 storage 指針數組中。

static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
    int i;

    (*thread_resources_ptr) = (tsrm_tls_entry *) malloc(sizeof(tsrm_tls_entry));
    (*thread_resources_ptr)->storage = (void **) malloc(sizeof(void *)*id_count);
    (*thread_resources_ptr)->count = id_count;
    (*thread_resources_ptr)->thread_id = thread_id;
    (*thread_resources_ptr)->next = NULL;

    // 設置線程本地存儲變數。在這裡設置之後,再到 ts_resource_ex 里取
    pthread_setspecific(*thread_resources_ptr);

    if (tsrm_new_thread_begin_handler) {
        tsrm_new_thread_begin_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    for (i=0; i<id_count; i++) {
        if (resource_types_table[i].done) {
            (*thread_resources_ptr)->storage[i] = NULL;
        } else {
            // 為新增的 tsrm_tls_entry 節點添加 resource_types_table 的資源
            (*thread_resources_ptr)->storage[i] = (void *) malloc(resource_types_table[i].size);
            if (resource_types_table[i].ctor) {
                resource_types_table[i].ctor((*thread_resources_ptr)->storage[i], &(*thread_resources_ptr)->storage);
            }
        }
    }

    if (tsrm_new_thread_end_handler) {
        tsrm_new_thread_end_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    pthread_mutex_unlock(tsmm_mutex);
}

上面有一個知識點,Thread Local Storage ,現在有一全局變數 tls_key,所有線程都可以使用它,改變它的值。 錶面上看起來這是一個全局變數,所有線程都可以使用它,而它的值在每一個線程中又是單獨存儲的。這就是線程本地存儲的意義。 那麼如何實現線程本地存儲呢?

需要聯合 tsrm_startupts_resource_exallocate_new_resource 函數並配以註釋一起舉例說明:

// 以 pthread 為例
// 1. 首先定義了 tls_key 全局變數
static pthread_key_t tls_key;

// 2. 然後在 tsrm_startup 調用 pthread_key_create() 來創建該變數
pthread_key_create( &tls_key, 0 ); 

// 3. 在 allocate_new_resource 中通過 tsrm_tls_set 將 *thread_resources_ptr 指針變數存入了全局變數 tls_key 中
tsrm_tls_set(*thread_resources_ptr);// 展開之後為 pthread_setspecific(*thread_resources_ptr);

// 4. 在 ts_resource_ex 中通過 tsrm_tls_get() 獲取在該線程中設置的 *thread_resources_ptr 
//    多線程併發操作時,相互不會影響。
thread_resources = tsrm_tls_get();

在理解了 tsrm_tls_table 數組和其中鏈表的創建之後,再看 ts_resource_ex 函數中調用的這個返回巨集

#define TSRM_SAFE_RETURN_RSRC(array, offset, range)     \
    if (offset==0) {                                    \
        return &array;                                  \
    } else {                                            \
        return array[TSRM_UNSHUFFLE_RSRC_ID(offset)];   \
    }

就是根據傳入 tsrm_tls_entry 和 storage 的數組下標 offset ,然後返回該全局變數在該線程的 storage數組中的地址。到這裡就明白了在多線程中獲取全局變數巨集 TSRMG 巨集定義了。

其實這在我們寫擴展的時候會經常用到:

#define TSRMLS_D void ***tsrm_ls   /* 不帶逗號,一般是唯一參數的時候,定義時用 */
#define TSRMLS_DC , TSRMLS_D       /* 也是定義時用,不過參數前面有其他參數,所以需要個逗號 */
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , TSRMLS_C

NOTICE 寫擴展的時候可能很多同學都分不清楚到底用哪一個,通過巨集展開我們可以看到,他們分別是帶逗號和不帶逗號,以及申明及調用,那麼英語中“D"就是代表:Define,而 後面的"C"是 Comma,逗號,前面的"C"就是Call。

以上為ZTS模式下的定義,非ZTS模式下其定義全部為空。

參考資料

 

本文來源於:https://github.com/zhoumengkang/tipi/blob/master/book/chapt08/08-03-zend-thread-safe-in-php.markdown?spm=5176.100239.blogcont60787.4.Mvv5xg&file=08-03-zend-thread-safe-in-php.markdown


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

-Advertisement-
Play Games
更多相關文章
  • 1、需求 做一個項目會有很多模塊,主要是方便復用,通過各個模塊之間聚合。模塊也可以獨立出來,如公用類庫,也可以在做其它項目中使用。該文的實例會有兩個模塊:分別為dallin web模塊,dallin utils工具類模塊 2、新建一個Maven父項目 1. 打開IDEA工具,通過file——new— ...
  • 還記得那些美妙的夜晚嗎 你洗洗打算看一個小電影就睡了,這個時候突然想起來今天晚上是伺服器更新的日子,你要在凌晨時分去把最新的代碼更新到伺服器,以保證明天大家一覺醒來打開網站,發現昨天的 Bug 都不見了。這時候你瞬間沒有了看電影的興緻了,這應該就是一個運維人員的日常了吧! 為什麼要在凌晨時分去更新服 ...
  • 上面運行結果是 [49 50][51 50] 。 --> 結論: bytes.Buffer Reset之後,如果再寫入新的數據,如果數據的長度沒有超過Reset之前緩衝區的長度,那麼Buffer內部不會重新開闢記憶體,也就是說, 寫入的數據會覆蓋之前的數據。 在本例中,這裡【之前的數據】就是那個 b ...
  • 註意:這裡使用了mybatis3.2.1版本,剛開始用了3.4.1的版本,會報一個很奇怪的錯(java.lang.AbstractMethodError: org.mybatis.spring.transaction.SpringManagedTransaction.getTimeout()Ljav ...
  • 這篇文章主要介紹了PHP的mysqli_query參數MYSQLI_STORE_RESULT和MYSQLI_USE_RESULT的區別,本文給出了這兩個參數的5個區別,需要的朋友可以參考下 雖然nosql變得流行,但是我感覺sql還是主流今天在翻php manul的時候,發現mysqli 的查詢可以 ...
  • 在網上查了許多解決方法,下麵是自己測試過能行的方法,只需在nginx.conf文件添加內容就可以了. 打開nginx.conf文件 ...
  • 獨占鎖 -- 鎖在一個時間點只能被一個線程鎖占有。根據鎖的獲取機制,它又劃分為“公平鎖”和“非公平鎖”。公平鎖,是按照通過CLH等待線程按照先來先得的規則,公平的獲取鎖;而非公平鎖,則當線程要獲取鎖時,它會無視CLH等待隊列而直接獲取鎖。獨占鎖的典型實例子是ReentrantLock,此外,Reen... ...
  • 2016年9月21日09:21:431.爬蟲的抓取周期:(1)首先生成初始請求爬第一個url,並指定一個回調函數被稱為與下載這些請求的響應。(2)第一個請求執行通過調用 start_requests()方法(預設情況下)生成 Request中指定的url start_urls和 parse方法作為請 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...