class Crawler {
private readonly Thread[] _workers;
private readonly ThreadStart _work;
public Crawler(int threadCount) {
_workers = new Thread[threadCount];
_work = DownloadWorker;
for (int i = 0; i < threadCount; i++) {
_workers[i] = new Thread(_work) { IsBackground = true };
_workers[i].Start();
}
}
}
建立 UrlQueue 資料表:
CREATE TABLE UrlQueue (
Url NVARCHAR(500) PRIMARY KEY,
Content NVARCHAR(MAX) NULL,
AttemptCount INT DEFAULT 0,
WorkerId INT DEFAULT -1, -- -1 表示未認領
Status TINYINT DEFAULT 0 -- 0=待處理, 1=處理中, 2=成功, 3=失敗
);
工作者認領邏輯(交易確保原子性):
string ClaimUrl(int workerId) {
using (var tran = conn.BeginTransaction()) {
var cmd = conn.CreateCommand();
cmd.Transaction = tran;
cmd.CommandText = @"
UPDATE TOP(1) UrlQueue
SET WorkerId = @wid, AttemptCount = AttemptCount + 1, Status = 1
OUTPUT INSERTED.Url
WHERE Status = 0 AND AttemptCount < @maxRetry;
";
cmd.Parameters.AddWithValue("@wid", workerId);
cmd.Parameters.AddWithValue("@maxRetry", 3);
var url = cmd.ExecuteScalar()?.ToString();
tran.Commit();
return url;
}
}
void DownloadWorker() {
int workerId = Thread.CurrentThread.ManagedThreadId;
int emptyCycles = 0;
while (true) {
string url = ClaimUrl(workerId);
if (string.IsNullOrEmpty(url)) {
if (++emptyCycles > 10) break; // 連續 10 次無任務視為結束
Thread.Sleep(1000);
continue;
}
emptyCycles = 0;
try {
string html = Download(url);
ParseAndEnqueueNewUrls(html);
UpdateUrlStatus(url, html, success: true);
} catch {
UpdateUrlStatus(url, null, success: false);
}
}
}
public void Start() { /* 已在建構函式啟動 */ }
public void Stop() {
foreach (var t in _workers) {
if (t.IsAlive) t.Abort(); // 或使用 CancellationToken 協作式取消
}
}
ThreadPool / Task / Parallel.ForEach / Dataflow (TPL Dataflow) 替代手動 ThreadSemaphoreSlim 限制並發下載數,避免對目標站點造成過大壓力HttpClient 單例 + Polly 重試策略Bloom Filter + Redis / 記憶體 HashSet,減少資料庫壓力robots.txt、設定 User-Agent、間隔請求文章展示了以資料庫為協調中心的多執行緒爬蟲雛形,解決了線程數控制、URL 去重認領、工作窮盡判斷、優雅停止四大核心問題。現代開發建議升級為 TPL Dataflow 或 Channel 管道模式,程式碼更簡潔、可維護性更高。