網路程式常面臨阻塞等待問題:DNS 解析、連線逾時、資料傳輸等操作可能耗時數秒。單線程循序處理會導致介面凍結、回應緩慢。本文透過實際案例——掃描區網 IP 取得裝置名稱,對比單線程與多線程兩種實作,量化多線程帶來的效能提升。
| 方法/屬性 | 用途 |
|---|---|
Start() | 啟動執行緒 |
Join() | 等待執行緒結束 |
Sleep(ms) | 暫停指定毫秒 |
Abort() | 強制終止(不建議常用) |
IsBackground | 設為背景執行緒,主程式結束時自動終止 |
Priority | 排程優先權 |
Name | 除錯用名稱 |
private void ScanSequential(string ipPrefix, int start, int end)
{
DateTime startTime = DateTime.Now;
listBoxResults.Items.Clear();
progressBar.Minimum = start;
progressBar.Maximum = end;
for (int i = start; i <= end; i++)
{
string ip = $"{ipPrefix}.{i}";
try
{
IPHostEntry host = Dns.GetHostEntry(ip);
listBoxResults.Items.Add($"{ip} - {host.HostName} [線上]");
}
catch
{
listBoxResults.Items.Add($"{ip} - 無回應 [離線]");
}
progressBar.Value = i;
Application.DoEvents(); // 避免 UI 凍結
}
TimeSpan elapsed = DateTime.Now - startTime;
lblTime.Text = $"耗時: {elapsed.TotalSeconds:F1} 秒";
}
// 掃描 10.138.198.1 ~ 10 實測約 43 秒
public delegate void UpdateListCallback(string ip, string hostName);
public class PingTask
{
public string IpAddress { get; set; }
public UpdateListCallback Callback { get; set; }
public void Execute()
{
string hostName = "";
try
{
IPHostEntry host = Dns.GetHostEntry(IpAddress);
hostName = host.HostName;
}
catch
{
hostName = "主機無回應";
}
Callback?.Invoke(IpAddress, hostName);
}
}
private DateTime _scanStartTime;
private void ScanParallel(string ipPrefix, int start, int end)
{
_scanStartTime = DateTime.Now;
listBoxResults.Items.Clear();
int threadCount = end - start + 1;
Thread[] threads = new Thread[threadCount];
progressBar.Minimum = start;
progressBar.Maximum = end + 1;
progressBar.Value = start;
for (int i = start; i <= end; i++)
{
int index = i - start;
string ip = $"{ipPrefix}.{i}";
var task = new PingTask
{
IpAddress = ip,
Callback = UpdateResultList
};
threads[index] = new Thread(task.Execute);
threads[index].Name = $"ScanThread_{ip}";
threads[index].IsBackground = true; // 重要:防止主程式關閉時殘留
threads[index].Start();
}
}
private void UpdateResultList(string ip, string hostName)
{
if (listBoxResults.InvokeRequired)
{
listBoxResults.Invoke(new UpdateListCallback(UpdateResultList), ip, hostName);
return;
}
lock (listBoxResults)
{
listBoxResults.Items.Add($"{ip} - {hostName}");
if (progressBar.Value < progressBar.Maximum)
progressBar.Value++;
if (progressBar.Value == progressBar.Maximum)
{
TimeSpan elapsed = DateTime.Now - _scanStartTime;
lblTime.Text = $"耗時: {elapsed.TotalSeconds:F1} 秒";
MessageBox.Show("掃描完成", "提示");
progressBar.Value = progressBar.Minimum;
}
}
}
| 掃描範圍 | 單線程耗時 | 多線程耗時 | 提升倍數 |
|---|---|---|---|
| 10 IP (10.138.198.1-10) | 43 秒 | 10 秒 | 4.3x |
| 50 IP | 約 3.5 分鐘 | 約 12 秒 | 17x |
| 254 IP (全 C 類網段) | 約 18 分鐘 | 約 15-20 秒 | 50x+ |
public async Task ScanWithTaskAsync(string ipPrefix, int start, int end, IProgress progress)
{
var tasks = new List();
var semaphore = new SemaphoreSlim(50); // 限制並發 50
for (int i = start; i <= end; i++)
{
string ip = $"{ipPrefix}.{i}";
tasks.Add(Task.Run(async () =>
{
await semaphore.WaitAsync();
try
{
IPHostEntry host = await Dns.GetHostEntryAsync(ip);
progress.Report($"{ip} - {host.HostName} [線上]");
}
catch
{
progress.Report($"{ip} - 無回應 [離線]");
}
finally
{
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
}
多線程在網路程式設計中效果顯著,將循序等待轉為平行處理可大幅縮短總耗時。從手動管理 `Thread` 進化到 `ThreadPool`、再到 `Task/async-await`,核心思維不變:將阻塞操作卸載到背景執行緒,主執行緒專注 UI 回應。實務上建議優先使用 Task-based 非同步模式,搭配適當的並發控制,兼顧效能與資源使用。