讓 GPT-4 來修複 Golang “數據競爭”問題(續) - 每天5分鐘玩轉 GPT 編程系列(7)

来源:https://www.cnblogs.com/daniel-hutao/archive/2023/08/15/devchat-gpt-in-5min-7-race-detector-2.html
-Advertisement-
Play Games

[TOC] ## 1. 我以為 我以為 [GoPool](https://github.com/devchat-ai/gopool) 這個項目會曇花一現,從此在 GitHub 上封塵。 > 關於 GoPool 項目誕生的故事:[《僅三天,我用 GPT-4 生成了性能全網第一的 Golang Work ...


目錄

1. 我以為

我以為 GoPool 這個項目會曇花一現,從此在 GitHub 上封塵。

關於 GoPool 項目誕生的故事:《僅三天,我用 GPT-4 生成了性能全網第一的 Golang Worker Pool,輕鬆打敗 GitHub 萬星項目》

不過這兩天陸續有越來越多的人開始試用 GoPool,並且發現了一些 bug,提到了 GitHub 上。

那麼今天就繼續分享下用 GPT-4 解決 issues 的過程,不管你是對 Golang 感興趣,還是對 GPT-4 感興趣,下麵的內容都會是你的菜。

2. 陰魂不散的“數據競爭”問題

我才發了一篇和“數據競爭”相關的文章:

在上面這篇文章里介紹瞭如何用 GPT-4 來解決一個單元測試中的“數據競爭”問題,然後提了個 PR:

還沒來得及喝口水,就發現又有用戶提了一個新 issue:

好家伙,看樣子今天不能提前下班了。

3. 老規矩,關門放 GPT-4

下文使用 DevChat 和 GPT-4 交互。如果你對 DevChat 感興趣,可以跳轉閱讀這篇文章:

3.1 復現問題

用戶在 issue 里提到他在 TestGoPoolWithMutex() 這個測試用例中加了 WithTimeout(10*time.Microsecond) 後就遇到了“數據競爭”問題,代碼如下:

func TestGoPoolWithMutex(t *testing.T) {
	pool := NewGoPool(100, WithLock(new(sync.Mutex)), WithTimeout(10*time.Microsecond))
	defer pool.Release()
	for i := 0; i < 1000; i++ {
		pool.AddTask(func() (interface{}, error) {
			time.Sleep(10 * time.Millisecond)
			return nil, nil
		})
	}
	pool.Wait()
}

我在本地能夠穩定復現這個問題。那麼接下來,就是簡化場景,用一個單獨的 UT 來測試 WithTimeout(10*time.Microsecond) 功能。

3.2 讓 GPT-4 寫一個單元測試

  • 我對 DevChat 說:

DevChat 的完整回答是:


要添加一個名為 TestGoPoolWithTimeout() 的測試函數,我們需要考慮以下幾點:

  • 我們需要創建一個帶有超時選項的 GoPool。
  • 我們需要添加一個任務,該任務的執行時間超過了設置的超時時間,以便測試超時功能是否正常工作。
  • 我們需要檢查任務是否因為超時而失敗。

以下是添加的 TestGoPoolWithTimeout() 函數:

func TestGoPoolWithTimeout(t *testing.T) {
    var taskError = errors.New("task error")
    var taskRun = false

    pool := NewGoPool(100, WithTimeout(100*time.Millisecond), WithErrorCallback(func(err error) {
        if err != taskError {
            t.Errorf("Expected error %v, but got %v", taskError, err)
        }
        taskRun = true
    }))
    defer pool.Release()

    pool.AddTask(func() (interface{}, error) {
        time.Sleep(200 * time.Millisecond)
        return nil, taskError
    })

    pool.Wait()

    if !taskRun {
        t.Errorf("Expected task to run and timeout, but it did not run")
    }
}

這個測試函數首先創建了一個帶有 100 毫秒超時的 GoPool。然後,它添加了一個任務,該任務需要 200 毫秒才能完成,這超過了設置的超時時間。如果任務因為超時而失敗,它將調用錯誤回調函數,並將 taskRun 設置為 true。最後,測試函數檢查 taskRun 是否為 true,如果不是,說明任務沒有運行,這是一個錯誤。


這段代碼“看起來精緻,但是跑不通”。能不能發現錯誤所在決定了這段代碼最終能不能用起來。你發現問題了嗎?(答案在下文揭曉)

  • 我又對 DevChat 說:

這個測試用例沒有跑過。我註意到你在 WithErrorCallback 中判斷 err != taskError 則報錯,然而超時的時候 task 返回的錯誤不是 taskError,而是 "task timed out",所以 != 是否應該改成 ==?

DevChat 給出的回答:


你是對的,我犯了一個錯誤。在這個測試用例中,我們應該期望的錯誤是 "task timed out",而不是 taskError。因此,我們應該在 WithErrorCallback 中檢查錯誤是否等於 "task timed out"。以下是修正後的測試函數:

func TestGoPoolWithTimeout(t *testing.T) {
    var taskRun = false

    pool := NewGoPool(100, WithTimeout(100*time.Millisecond), WithErrorCallback(func(err error) {
        if err.Error() != "task timed out" {
            t.Errorf("Expected error 'task timed out', but got %v", err)
        }
        taskRun = true
    }))
    defer pool.Release()

    pool.AddTask(func() (interface{}, error) {
        time.Sleep(200 * time.Millisecond)
        return nil, nil
    })

    pool.Wait()

    if !taskRun {
        t.Errorf("Expected task to run and timeout, but it did not run")
    }
}

在這個修正後的版本中,我們在 WithErrorCallback 中檢查錯誤是否等於 "task timed out"。如果不是,我們就報告一個錯誤。我們還修改了添加的任務,使其在執行時不返回錯誤,因為我們期望的錯誤是由超時引起的,而不是任務本身引起的。


這時候 TestGoPoolWithTimeout() 其實能跑通了。不過當我執行 go test 命令加上了 -race 之後,有趣:

WARNING: DATA RACE

GPT-4 寫的 bug 讓 GPT-4 自己去修!

於是我語(一)重(股)心(腦)長(兒)地把錯誤信息發給了 DevChat:


這個測試用例能夠跑通,但是如果執行 race detector 則會報錯:

go test -race ./...
==================
WARNING: DATA RACE
Write at 0x00c00001a14f by goroutine 641:
  github.com/devchat-ai/gopool.TestGoPoolWithTimeout.func1()
      /Users/danielhu/go/mycode/gopool/gopool_test.go:160 +0xf4
  github.com/devchat-ai/gopool.(*worker).handleResult()
      /Users/danielhu/go/mycode/gopool/worker.go:92 +0x94
  github.com/devchat-ai/gopool.(*worker).start.func1()
      /Users/danielhu/go/mycode/gopool/worker.go:27 +0xa4

Previous read at 0x00c00001a14f by goroutine 541:
  github.com/devchat-ai/gopool.TestGoPoolWithTimeout()
      /Users/danielhu/go/mycode/gopool/gopool_test.go:171 +0x1a8
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1576 +0x188
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1629 +0x40

Goroutine 641 (running) created at:
  github.com/devchat-ai/gopool.(*worker).start()
      /Users/danielhu/go/mycode/gopool/worker.go:23 +0xdc
  github.com/devchat-ai/gopool.NewGoPool()
      /Users/danielhu/go/mycode/gopool/gopool.go:75 +0x490
  github.com/devchat-ai/gopool.TestGoPoolWithTimeout()
      /Users/danielhu/go/mycode/gopool/gopool_test.go:156 +0x138
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1576 +0x188
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1629 +0x40

Goroutine 541 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1629 +0x5e4
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:2036 +0x80
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1576 +0x188
  testing.runTests()
      /usr/local/go/src/testing/testing.go:2034 +0x700
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:1906 +0x950
  main.main()
      _testmain.go:63 +0x300
==================
--- FAIL: TestGoPoolWithTimeout (0.11s)
    gopool_test.go:172: Expected task to run and timeout, but it did not run
    testing.go:1446: race detected during execution of test
FAIL
FAIL    github.com/devchat-ai/gopool    1.267s
FAIL

這次 GPT-4 沒有說對不起,可惡,這態度,差評:

DevChat 這次給出的完整回答:


這個數據競爭問題是由於在多個 goroutine 中同時讀寫 taskRun 變數導致的。在 Go 中,如果你有多個 goroutine 同時讀寫一個變數,你需要使用某種形式的同步來確保數據的一致性。

在這個測試用例中,我們可以使用 sync/atomic 包提供的原子操作來解決這個問題。以下是修正後的測試函數:

import "sync/atomic"

func TestGoPoolWithTimeout(t *testing.T) {
    var taskRun int32

    pool := NewGoPool(100, WithTimeout(100*time.Millisecond), WithErrorCallback(func(err error) {
        if err.Error() != "task timed out" {
            t.Errorf("Expected error 'task timed out', but got %v", err)
        }
        atomic.StoreInt32(&taskRun, 1)
    }))
    defer pool.Release()

    pool.AddTask(func() (interface{}, error) {
        time.Sleep(200 * time.Millisecond)
        return nil, nil
    })

    pool.Wait()

    if atomic.LoadInt32(&taskRun) == 0 {
        t.Errorf("Expected task to run and timeout, but it did not run")
    }
}

在這個修正後的版本中,我們使用 sync/atomic 包的 StoreInt32LoadInt32 函數來安全地更新和讀取 taskRun 變數。這樣,即使在多個 goroutine 中同時操作這個變數,也不會出現數據競爭的問題。


行,至少 DATA RACE 問題解決了。

3.3 修複 Wait() 中的邏輯漏洞

前面解決了 DATA RACE 問題,不過 TestGoPoolWithTimeout() 運行結果是:

$ go test -race ./...
--- FAIL: TestGoPoolWithTimeout (0.11s)
    gopool_test.go:172: Expected task to run and timeout, but it did not run
FAIL
FAIL    github.com/devchat-ai/gopool    1.203s
FAIL

開始燒腦起來了,繼續把這個日誌拋給 GPT-4 吧。

這次 DevChat 的回答是:

看起來測試用例沒有通過,原因是期望的任務沒有運行。這可能是因為 pool.Wait() 在任務超時之前就返回了。pool.Wait() 的實現只是簡單地等待任務隊列為空,但這並不能保證所有的任務都已經完成執行,特別是在任務超時的情況下。

為瞭解決這個問題,我們需要在 pool.Wait() 中添加一些邏輯來確保所有的任務都已經完成執行。一種可能的解決方案是使用一個 sync.WaitGroup 來跟蹤正在執行的任務數量。當一個任務開始執行時,我們調用 WaitGroup.Add(1),當任務完成執行時,我們調用 WaitGroup.Done()。然後,我們可以在 pool.Wait() 中調用 WaitGroup.Wait() 來等待所有的任務完成執行。

這是一個涉及到多個函數的修改,所以我建議你根據你的代碼庫的具體情況來進行這個修改。如果你需要更具體的幫助,可以提供更多的代碼上下文,我會很樂意幫助你。

GPT-4 說問題在 pool.Wait() 中,不過它給出的方案還是讓我猶豫。當前 Wait() 方法如下:

// Wait waits for all tasks to be dispatched.
 func (p *goPool) Wait() {
 	for len(p.taskQueue) > 0 {
 		time.Sleep(100 * time.Millisecond)
 	}
 }

這裡的邏輯是等待 taskQueue 為空,也就是所有的任務都被分發出去,有對應的 workers 去執行。GPT-4 分析的沒錯,這裡確實少了“等待所有任務執行完成”的邏輯。

  • 我對 DevChat 說:

在執行 task 的時候會有 popWorker() 動作,而 task 完成則有 pushWorker() 動作,因此如果 len(goPool.workerStack) == len(goPool.workers) 是不是也能說明所有的 tasks 都完成了?

DevChat 回答:


你的理解是正確的。在這個特定的 goPool 實現中,每當一個任務開始執行時,都會從 workerStackpop 一個 worker,而當任務完成時,會將 worker pushworkerStack。因此,如果 len(goPool.workerStack) == len(goPool.workers),那麼這確實意味著所有的任務都已經完成了。

因此,你可以修改 Wait() 函數,使其等待直到所有的 worker 都返回到 workerStack。以下是修改後的 Wait() 函數:

// Wait waits for all tasks to be dispatched and completed.
func (p *goPool) Wait() {
    for len(p.taskQueue) > 0 || len(p.workerStack) != len(p.workers) {
        time.Sleep(100 * time.Millisecond)
    }
}

在這個修改後的版本中,Wait() 函數會等待直到任務隊列為空並且所有的 worker 都返回到 workerStack。這樣,當 Wait() 函數返回時,我們就可以確保所有的任務都已經完成了。


於是我這樣改之後,繼續運行了下 Race Detector,結果如下:

$ go test -race -run TestGoPoolWithTimeout
==================
WARNING: DATA RACE
Read at 0x00c000146298 by goroutine 6:
  github.com/devchat-ai/gopool.(*goPool).Wait()
      /Users/danielhu/go/mycode/gopool/gopool.go:89 +0x7c
  github.com/devchat-ai/gopool.TestGoPoolWithTimeout()
      /Users/danielhu/go/mycode/gopool/gopool_test.go:169 +0x1a0
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1576 +0x188
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1629 +0x40

Previous write at 0x00c000146298 by goroutine 118:
  github.com/devchat-ai/gopool.(*goPool).popWorker()
      /Users/danielhu/go/mycode/gopool/gopool.go:113 +0xec
  github.com/devchat-ai/gopool.(*goPool).dispatch()
      /Users/danielhu/go/mycode/gopool/gopool.go:164 +0x10c
  github.com/devchat-ai/gopool.NewGoPool.func2()
      /Users/danielhu/go/mycode/gopool/gopool.go:78 +0x34

Goroutine 6 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1629 +0x5e4
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:2036 +0x80
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1576 +0x188
  testing.runTests()
      /usr/local/go/src/testing/testing.go:2034 +0x700
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:1906 +0x950
  main.main()
      _testmain.go:63 +0x300

Goroutine 118 (running) created at:
  github.com/devchat-ai/gopool.NewGoPool()
      /Users/danielhu/go/mycode/gopool/gopool.go:78 +0x660
  github.com/devchat-ai/gopool.TestGoPoolWithTimeout()
      /Users/danielhu/go/mycode/gopool/gopool_test.go:156 +0x138
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1576 +0x188
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1629 +0x40
==================
--- FAIL: TestGoPoolWithTimeout (0.21s)
    testing.go:1446: race detected during execution of test
FAIL
exit status 1
FAIL    github.com/devchat-ai/gopool    0.519s

慘啊,今天是和 DATA RACE 杠上了???

我把這段日誌直接貼給了 GPT-4,然後 GPT-4 感慨了下:看起來我們又遇到了數據競爭問題。

DevChat 此處的完整回答是:


看起來我們又遇到了數據競爭問題。這次的問題是在 Wait() 函數和 popWorker() 函數中同時讀寫 workerStack。為瞭解決這個問題,我們需要在訪問 workerStack 時使用互斥鎖來保護數據。

以下是修改後的 Wait() 函數和 popWorker() 函數:

import "sync"

type goPool struct {
    // other fields...
    workerStackMutex sync.Mutex
}

// Wait waits for all tasks to be dispatched and completed.
func (p *goPool) Wait() {
    for {
        p.workerStackMutex.Lock()
        workerStackLen := len(p.workerStack)
        p.workerStackMutex.Unlock()

        if len(p.taskQueue) == 0 && workerStackLen == len(p.workers) {
            break
        }

        time.Sleep(100 * time.Millisecond)
    }
}

// popWorker pops a worker from the worker stack.
func (p *goPool) popWorker() *worker {
    p.workerStackMutex.Lock()
    defer p.workerStackMutex.Unlock()

    if len(p.workerStack) == 0 {
        return nil
    }

    w := p.workerStack[len(p.workerStack)-1]
    p.workerStack = p.workerStack[:len(p.workerStack)-1]
    return w
}

在這個修改後的版本中,我們添加了一個 workerStackMutex 欄位到 goPool 結構體中,用於在訪問 workerStack 時提供互斥保護。在 Wait()popWorker() 函數中,我們都使用了這個互斥鎖來保護 workerStack 的讀寫操作,從而避免了數據競爭的問題。


看起來來這個修改能跑通,不過似乎不夠優雅。這時候就又一次體現“GPT 也不能幫你完成你認知之外的工作”這個道理了。這段代碼整體不錯,但是有瑕疵,能看懂,能微調,GPT 就能幫你提效;反之,和 GPT 聊天就是浪費口水。

  • 我又對 DevChat 說:

但是 goPool 本來就有一個 lock 欄位,能不能直接用?

這次 GPT-4 和我說“對不起”了。我很欣慰,她又開始變得懂禮貌了。

DevChat 回答:


對不起,我沒有註意到 goPool 結構體中已經有一個 lock 欄位。你完全可以使用這個已有的互斥鎖來保護 workerStack 的訪問,而不需要添加一個新的互斥鎖。

以下是使用 lock 欄位來保護 workerStack 訪問的修改後的 Wait() 函數和 popWorker() 函數:

// Wait waits for all tasks to be dispatched and completed.
func (p *goPool) Wait() {
    for {
        p.lock.Lock()
        workerStackLen := len(p.workerStack)
        p.lock.Unlock()

        if len(p.taskQueue) == 0 && workerStackLen == len(p.workers) {
            break
        }

        time.Sleep(100 * time.Millisecond)
    }
}

// popWorker pops a worker from the worker stack.
func (p *goPool) popWorker() *worker {
    p.lock.Lock()
    defer p.lock.Unlock()

    if len(p.workerStack) == 0 {
        return nil
    }

    w := p.workerStack[len(p.workerStack)-1]
    p.workerStack = p.workerStack[:len(p.workerStack)-1]
    return w
}

在這個修改後的版本中,我們使用 lock 欄位來保護 workerStack 的讀寫操作,從而避免了數據競爭的問題。


這個回答有點“後遺症”,也就是前面提到了 popWorker() 方法的修改,所以這裡它也給出了修改內容,其實這個方法不需要動,我的原版代碼是:

func (p *goPool) popWorker() int {
	p.lock.Lock()
	workerIndex := p.workerStack[len(p.workerStack)-1]
	p.workerStack = p.workerStack[:len(p.workerStack)-1]
	p.lock.Unlock()
	return workerIndex
}

len(p.workerStack) == 0 這個判斷是多餘的,因為調用 popWorker()dispatch() 方法長這樣:

// dispatch dispatches tasks to workers.
func (p *goPool) dispatch() {
	for t := range p.taskQueue {
		p.cond.L.Lock()
		for len(p.workerStack) == 0 {
			p.cond.Wait()
		}
		p.cond.L.Unlock()
		workerIndex := p.popWorker()
		p.workers[workerIndex].taskQueue <- t
	}
}

這裡通過 len(p.workerStack) == 0 判斷保證了調用 popWorker() 被調用時,len(p.workerStack) != 0

所以最後我採納了 GPT-4 給出的這個 Wait() 方法:

func (p *goPool) Wait() {
    for {
        p.lock.Lock()
        workerStackLen := len(p.workerStack)
        p.lock.Unlock()

        if len(p.taskQueue) == 0 && workerStackLen == len(p.workers) {
            break
        }

        time.Sleep(100 * time.Millisecond)
    }
}

最後我提了這個 PR:Improve task execution and testing in goPool

4. 總結

還是那句話:GPT 多數情況下並不能幫你解決認知之外的問題。

除非是比較簡單的代碼,不然如果 GPT 寫的代碼你看不懂,那麼大概率這段代碼也不可用。GPT 給的100行代碼里很可能有1行是錯的,如果你能發現這一行錯誤,能夠把它改對,那你就省了99行的時間。反之,這100行跑不通的代碼對你來說一文不值。

總之,GPT 還是能幫你省下不少事,但是不能幫你幹完所有事。


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

-Advertisement-
Play Games
更多相關文章
  • 背景:在linux環境下,很多服務我們都使用docker來跑,很是方便,容器服務獨立,配置獨立,數據獨立等等,但是有個問題,就是如果某個服務異常了,暫停了,停止了,一直重啟中,我們要怎麼及時的知道是哪個服務,併進行處理,保證業務正常運行。 本文主要介紹使用docker服務自帶的一些命令來實現一個基本 ...
  • Lua程式設計第四版第二部分編程實操自做練習題答案,帶:star:為重點。 ## 14.1 :star: > 該函數用於兩個稀疏矩陣相加 ```lua function martixAdd(a, b) local c = {} for i = 1, #a, 1 do c[i] = {} for k, ...
  • [TOC] ## 類型的基本歸類 **整形家族:** ```c char unsigned char signed char short unsigned short [int] signed short [int] int unsigned int signed int long unsigned ...
  • 在Python中,列表(list)是一種有序、可變的數據結構,用於存儲多個元素。列表可以包含不同類型的元素,包括整數、浮點數、字元串等。實際上列表有點類似C++語言中的數組,但僅僅只是類似,和數組還是有點不一樣的。列表非常適合利用順序和位置定位某一元素,尤其是當元素的順序或內容經常發生改變時。 在P ...
  • # 1. 回顧 > 1. java實現多線程: [1]繼承Thread類並重寫run方法 [2]實現Runnable介面 > > 2. 線程Thread中常用的方法: setName(): Thread.currentThread().getName(): > > ​ static void sle ...
  • [TOC] # 簡介 ImGui 是一個用於C++的用戶界面庫,跨平臺、無依賴,支持OpenGL、DirectX等多種渲染API,是一種即時UI(Immediate Mode User Interface)庫,保留模式與即時模式的區別參考[**保留模式與即時模式**](https://learn.m ...
  • 利用AI幫你讀文章、利用AI幫你分析非結構化數據,這些最為潮流的AI輔助工具,相信很多讀者都在各種媒體上看到過了。但還是有不少人並沒有真正的使用過,這裡有很多原因導致,具體就不細說了,懂的都懂。 今天TJ就給大家推薦一個你可以線上使用,也可以自己搭建的AI輔助工具:[**Quivr**](https ...
  • Sun公司以及其他虛擬機提供商發佈了許多可以運行在各種不同平臺上的虛擬機,這些虛擬機都可以載入和執行同一種平臺無關的的程式存儲格式——位元組碼(ByteCode),從而實現了程式的“一次編寫,到處運行”。“Class文件”這種特定的二進位文件格式所關聯,Class文件中包含了Java虛擬機指令集和符號... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...