10、使用信号量限制线程数量

使用信号量限制线程数量

知识点

什么是信号量

信号量(Semaphore)是一种同步原语,用于控制可以同时访问特定资源的线程数量。它维护一个计数器,表示可用资源的数量。

信号量的工作原理

  • 计数器:维护可用资源的数量
  • 获取资源:计数器减1,如果计数器为0则阻塞等待
  • 释放资源:计数器加1,唤醒等待的线程
  • 容量限制:设置最大并发访问数量

.NET中的信号量类型

  1. Semaphore:系统级信号量,支持跨进程
  2. SemaphoreSlim:轻量级信号量,仅支持进程内,性能更好

使用场景

  • 限制并发连接数
  • 控制资源池大小
  • 限制同时执行的任务数量
  • 实现生产者消费者模式

信号量vs其他同步机制

特性 Semaphore Mutex Lock
并发数 可配置 1 1
跨进程 支持 支持 不支持
性能 中等 慢 快

代码案例

案例1:基本信号量使用

using System;
using System.Threading;
using System.Threading.Tasks;

class BasicSemaphoreExample
{
    // 创建一个信号量,最多允许3个线程同时访问
    private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(3, 3);
    
    static void Main(string[] args)
    {
        Console.WriteLine("基本信号量示例 - 最多允许3个线程同时执行");
        
        // 启动10个任务,但只有3个能同时执行
        Task[] tasks = new Task[10];
        
        for (int i = 0; i < tasks.Length; i++)
        {
            int taskId = i;
            tasks[i] = Task.Run(() => DoWork(taskId));
        }
        
        Task.WaitAll(tasks);
        
        Console.WriteLine("所有任务完成");
        Console.WriteLine($"信号量当前计数: {semaphore.CurrentCount}");
        
        semaphore.Dispose();
    }
    
    static async Task DoWork(int taskId)
    {
        Console.WriteLine($"任务{taskId}: 等待信号量...");
        
        // 等待信号量
        await semaphore.WaitAsync();
        
        try
        {
            Console.WriteLine($"任务{taskId}: 获得信号量,开始工作");
            Console.WriteLine($"当前活跃任务数: {3 - semaphore.CurrentCount}");
            
            // 模拟工作
            await Task.Delay(2000);
            
            Console.WriteLine($"任务{taskId}: 工作完成");
        }
        finally
        {
            // 释放信号量
            semaphore.Release();
            Console.WriteLine($"任务{taskId}: 释放信号量");
        }
    }
}

案例2:限制并发下载数量

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

class ConcurrentDownloadLimiter
{
    // 限制同时下载的文件数量为5个
    private static readonly SemaphoreSlim downloadSemaphore = new SemaphoreSlim(5, 5);
    private static readonly HttpClient httpClient = new HttpClient();
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("并发下载限制示例");
        
        // 模拟下载URL列表
        List<string> urls = new List<string>();
        for (int i = 1; i <= 20; i++)
        {
            urls.Add($"https://httpbin.org/delay/{i % 3 + 1}"); // 模拟不同延迟的下载
        }
        
        Console.WriteLine($"开始下载 {urls.Count} 个文件,最大并发数: 5");
        
        // 启动所有下载任务
        Task<DownloadResult>[] downloadTasks = new Task<DownloadResult>[urls.Count];
        
        for (int i = 0; i < urls.Count; i++)
        {
            int index = i;
            downloadTasks[i] = DownloadFileAsync(urls[index], index + 1);
        }
        
        // 等待所有下载完成
        DownloadResult[] results = await Task.WhenAll(downloadTasks);
        
        // 显示结果
        Console.WriteLine("\n=== 下载结果统计 ===");
        int successCount = 0;
        int failCount = 0;
        
        foreach (var result in results)
        {
            if (result.Success)
            {
                successCount++;
                Console.WriteLine($"文件{result.FileId}: 成功 - 大小: {result.Size} bytes, 耗时: {result.Duration.TotalSeconds:F2}s");
            }
            else
            {
                failCount++;
                Console.WriteLine($"文件{result.FileId}: 失败 - {result.ErrorMessage}");
            }
        }
        
        Console.WriteLine($"\n成功: {successCount}, 失败: {failCount}");
        Console.WriteLine($"信号量最终计数: {downloadSemaphore.CurrentCount}");
        
        downloadSemaphore.Dispose();
        httpClient.Dispose();
    }
    
    static async Task<DownloadResult> DownloadFileAsync(string url, int fileId)
    {
        var result = new DownloadResult { FileId = fileId };
        var startTime = DateTime.Now;
        
        Console.WriteLine($"文件{fileId}: 等待下载槽位...");
        
        // 等待信号量
        await downloadSemaphore.WaitAsync();
        
        try
        {
            Console.WriteLine($"文件{fileId}: 开始下载 {url}");
            Console.WriteLine($"当前活跃下载数: {5 - downloadSemaphore.CurrentCount}");
            
            // 执行下载
            var response = await httpClient.GetAsync(url);
            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsByteArrayAsync();
                result.Success = true;
                result.Size = content.Length;
            }
            else
            {
                result.Success = false;
                result.ErrorMessage = $"HTTP {response.StatusCode}";
            }
        }
        catch (Exception ex)
        {
            result.Success = false;
            result.ErrorMessage = ex.Message;
        }
        finally
        {
            result.Duration = DateTime.Now - startTime;
            downloadSemaphore.Release();
            Console.WriteLine($"文件{fileId}: 下载完成,释放槽位");
        }
        
        return result;
    }
    
    class DownloadResult
    {
        public int FileId { get; set; }
        public bool Success { get; set; }
        public int Size { get; set; }
        public TimeSpan Duration { get; set; }
        public string ErrorMessage { get; set; }
    }
}

案例3:数据库连接池模拟

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class DatabaseConnectionPool
{
    private readonly SemaphoreSlim connectionSemaphore;
    private readonly ConcurrentQueue<DatabaseConnection> availableConnections;
    private readonly int maxConnections;
    private int currentConnections = 0;
    
    public DatabaseConnectionPool(int maxConnections)
    {
        this.maxConnections = maxConnections;
        this.connectionSemaphore = new SemaphoreSlim(maxConnections, maxConnections);
        this.availableConnections = new ConcurrentQueue<DatabaseConnection>();
        
        Console.WriteLine($"数据库连接池初始化,最大连接数: {maxConnections}");
    }
    
    public async Task<DatabaseConnection> GetConnectionAsync(TimeSpan timeout)
    {
        Console.WriteLine("请求数据库连接...");
        
        // 等待可用连接槽位
        if (!await connectionSemaphore.WaitAsync(timeout))
        {
            throw new TimeoutException("获取数据库连接超时");
        }
        
        try
        {
            // 尝试从池中获取现有连接
            if (availableConnections.TryDequeue(out DatabaseConnection connection))
            {
                Console.WriteLine($"从池中获取现有连接 {connection.Id}");
                return connection;
            }
            
            // 如果没有现有连接,创建新连接
            int connectionId = Interlocked.Increment(ref currentConnections);
            var newConnection = new DatabaseConnection(connectionId);
            await newConnection.OpenAsync();
            
            Console.WriteLine($"创建新的数据库连接 {newConnection.Id}");
            return newConnection;
        }
        catch
        {
            // 如果创建连接失败,释放信号量
            connectionSemaphore.Release();
            throw;
        }
    }
    
    public void ReturnConnection(DatabaseConnection connection)
    {
        if (connection != null && connection.IsOpen)
        {
            Console.WriteLine($"返回连接 {connection.Id} 到池中");
            availableConnections.Enqueue(connection);
        }
        else
        {
            Console.WriteLine($"连接 {connection?.Id} 已关闭或无效");
        }
        
        // 释放信号量槽位
        connectionSemaphore.Release();
    }
    
    public void Dispose()
    {
        // 关闭所有连接
        while (availableConnections.TryDequeue(out DatabaseConnection connection))
        {
            connection.Close();
        }
        
        connectionSemaphore.Dispose();
        Console.WriteLine("数据库连接池已关闭");
    }
    
    public int ActiveConnections => maxConnections - connectionSemaphore.CurrentCount;
}

class DatabaseConnection
{
    public int Id { get; }
    public bool IsOpen { get; private set; }
    
    public DatabaseConnection(int id)
    {
        Id = id;
    }
    
    public async Task OpenAsync()
    {
        // 模拟连接建立时间
        await Task.Delay(100);
        IsOpen = true;
        Console.WriteLine($"数据库连接 {Id} 已建立");
    }
    
    public async Task<string> ExecuteQueryAsync(string query)
    {
        if (!IsOpen)
            throw new InvalidOperationException("连接未打开");
        
        // 模拟查询执行时间
        await Task.Delay(500);
        return $"查询结果: {query} (连接 {Id})";
    }
    
    public void Close()
    {
        IsOpen = false;
        Console.WriteLine($"数据库连接 {Id} 已关闭");
    }
}

class DatabaseConnectionPoolExample
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("数据库连接池示例");
        
        // 创建连接池,最大5个连接
        var connectionPool = new DatabaseConnectionPool(5);
        
        // 启动多个任务模拟并发数据库操作
        Task[] tasks = new Task[12];
        
        for (int i = 0; i < tasks.Length; i++)
        {
            int taskId = i;
            tasks[i] = Task.Run(() => DatabaseOperationAsync(connectionPool, taskId));
        }
        
        await Task.WhenAll(tasks);
        
        Console.WriteLine("所有数据库操作完成");
        connectionPool.Dispose();
    }
    
    static async Task DatabaseOperationAsync(DatabaseConnectionPool pool, int taskId)
    {
        DatabaseConnection connection = null;
        
        try
        {
            Console.WriteLine($"任务{taskId}: 请求数据库连接");
            
            // 获取连接,设置5秒超时
            connection = await pool.GetConnectionAsync(TimeSpan.FromSeconds(5));
            
            Console.WriteLine($"任务{taskId}: 获得连接 {connection.Id},当前活跃连接数: {pool.ActiveConnections}");
            
            // 执行多个查询
            for (int i = 0; i < 3; i++)
            {
                string query = $"SELECT * FROM table{i} WHERE task_id = {taskId}";
                string result = await connection.ExecuteQueryAsync(query);
                Console.WriteLine($"任务{taskId}: {result}");
            }
            
            // 模拟处理时间
            await Task.Delay(1000);
            
            Console.WriteLine($"任务{taskId}: 数据库操作完成");
        }
        catch (TimeoutException)
        {
            Console.WriteLine($"任务{taskId}: 获取数据库连接超时");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"任务{taskId}: 数据库操作异常 - {ex.Message}");
        }
        finally
        {
            // 返回连接到池中
            if (connection != null)
            {
                pool.ReturnConnection(connection);
            }
        }
    }
}

案例4:生产者消费者with信号量

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class ProducerConsumerWithSemaphore
{
    private static readonly SemaphoreSlim emptySlots = new SemaphoreSlim(10, 10); // 空槽位
    private static readonly SemaphoreSlim filledSlots = new SemaphoreSlim(0, 10);  // 已填充槽位
    private static readonly ConcurrentQueue<WorkItem> buffer = new ConcurrentQueue<WorkItem>();
    private static volatile bool isProducing = true;
    
    class WorkItem
    {
        public int Id { get; set; }
        public string Data { get; set; }
        public DateTime CreatedTime { get; set; }
    }
    
    static async Task Main(string[] args)
    {
        Console.WriteLine("生产者消费者模式(信号量实现)");
        Console.WriteLine("缓冲区大小: 10");
        
        // 启动生产者
        Task[] producers = new Task[2];
        for (int i = 0; i < producers.Length; i++)
        {
            int producerId = i;
            producers[i] = Task.Run(() => ProducerAsync(producerId));
        }
        
        // 启动消费者
        Task[] consumers = new Task[3];
        for (int i = 0; i < consumers.Length; i++)
        {
            int consumerId = i;
            consumers[i] = Task.Run(() => ConsumerAsync(consumerId));
        }
        
        // 运行10秒后停止生产
        await Task.Delay(10000);
        isProducing = false;
        Console.WriteLine("停止生产信号已发出");
        
        // 等待生产者完成
        await Task.WhenAll(producers);
        
        // 通知消费者没有更多数据
        for (int i = 0; i < consumers.Length; i++)
        {
            filledSlots.Release(); // 释放信号让消费者能够检查退出条件
        }
        
        // 等待消费者完成
        await Task.WhenAll(consumers);
        
        Console.WriteLine($"程序结束,缓冲区剩余项目: {buffer.Count}");
        
        emptySlots.Dispose();
        filledSlots.Dispose();
    }
    
    static async Task ProducerAsync(int producerId)
    {
        int itemId = 0;
        
        while (isProducing)
        {
            try
            {
                // 等待空槽位(最多等待1秒)
                if (await emptySlots.WaitAsync(1000))
                {
                    // 创建工作项
                    var workItem = new WorkItem
                    {
                        Id = itemId++,
                        Data = $"Producer{producerId}_Item{itemId}",
                        CreatedTime = DateTime.Now
                    };
                    
                    // 添加到缓冲区
                    buffer.Enqueue(workItem);
                    Console.WriteLine($"生产者{producerId}: 生产了 {workItem.Data},缓冲区大小: {buffer.Count}");
                    
                    // 通知有新的填充槽位
                    filledSlots.Release();
                }
                else
                {
                    Console.WriteLine($"生产者{producerId}: 等待空槽位超时");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"生产者{producerId}: 异常 - {ex.Message}");
            }
            
            // 模拟生产时间
            await Task.Delay(500);
        }
        
        Console.WriteLine($"生产者{producerId}: 停止生产");
    }
    
    static async Task ConsumerAsync(int consumerId)
    {
        while (true)
        {
            try
            {
                // 等待填充槽位(最多等待2秒)
                if (await filledSlots.WaitAsync(2000))
                {
                    // 尝试从缓冲区获取项目
                    if (buffer.TryDequeue(out WorkItem workItem))
                    {
                        Console.WriteLine($"消费者{consumerId}: 消费了 {workItem.Data},缓冲区大小: {buffer.Count}");
                        Console.WriteLine($"消费者{consumerId}: 处理时间延迟: {(DateTime.Now - workItem.CreatedTime).TotalMilliseconds:F0}ms");
                        
                        // 模拟处理时间
                        await Task.Delay(1000);
                        
                        // 通知有新的空槽位
                        emptySlots.Release();
                    }
                    else if (!isProducing)
                    {
                        // 如果缓冲区为空且不再生产,退出
                        Console.WriteLine($"消费者{consumerId}: 没有更多项目,退出");
                        break;
                    }
                }
                else
                {
                    Console.WriteLine($"消费者{consumerId}: 等待项目超时");
                    
                    // 如果不再生产且等待超时,可能需要退出
                    if (!isProducing && buffer.IsEmpty)
                    {
                        Console.WriteLine($"消费者{consumerId}: 生产已停止且缓冲区为空,退出");
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"消费者{consumerId}: 异常 - {ex.Message}");
            }
        }
        
        Console.WriteLine($"消费者{consumerId}: 已退出");
    }
}

知识点总结

  1. 信号量的核心概念:

    • 计数器机制控制并发访问数量
    • 获取资源时计数器减1,释放时加1
    • 计数器为0时后续访问被阻塞
  2. SemaphoreSlim vs Semaphore:

    • SemaphoreSlim:轻量级,仅进程内,支持异步
    • Semaphore:重量级,支持跨进程,仅同步操作
  3. 最佳实践:

    • 总是在finally块中释放信号量
    • 使用using模式确保资源清理
    • 设置合理的超时时间
    • 避免死锁和资源泄漏
  4. 适用场景:

    • 连接池管理
    • 限制并发下载/上传
    • 资源配额控制
    • 生产者消费者模式
  5. 性能优化:

    • 选择合适的信号量类型
    • 避免频繁的获取/释放操作
    • 合理设置初始计数和最大计数
    • 考虑异步操作减少线程阻塞
posted @ 2025-08-27 01:24  jessqiu  阅读(66)  评论(0)    收藏  举报