C#中常见的锁以及用法--18

目录

一.C#中存在的锁

二.锁的作用

三.锁的概念和定义

关于锁的完整代码示例

代码逐层剖析:

全局变量与同步变量

Lock(锁)关键字示例

Monitor(监视器锁)示例

Mutex(互斥量)示例(支持跨进程同步)

SemaphoreSlim(信号量)示例

ReadWriterLockSlim(读写锁)示例

SpinLock(自旋锁)示例

InterLocked示例

任务并行库(TPL)同步示例

总结


在C#语言中,为了在多线程环境中保护共享资源,防止线程间资源竞争,引入了多种锁的机制

本文将介绍一些常见的C#的锁以及其作用,概念和使用方式

一.C#中存在的

  1. **lock关键字(基于Monitor):**这是C#中最简单和常用的同步机制,用于确保在给定的对象上,同一时间只有一个线程可以进入被锁定的代码块
  2. **Monitor类:**提供了静态方法Enter和Exit,与lock关键字功能类似,但提供了更细粒度的控制,例如尝试进入和等待通知等
  3. **Mutex(互斥量):**系统级别的同步机制,可在跨进程之间同步进程,对比lock和Monitor,Mutex可以用于同步不同进程中的线程
  4. **Semaphore和SemaphoreSlim:**信号量允许限定一定数量的线程同时访问一个资源,SemaphoreSlim是轻量级版本,主要用于进程内同步
  5. **ReaderWriterLockSlim:**允许多个线程同时读取资源,但在写入时,要求独占访问.适用于读多写少的场景,以提高并发性能
  6. **SpinLock:**一种自旋锁,用于短时间的锁定,可以减少线程上下文切换的开销,但是需要去谨慎的去使用,避免占用CPU时间过长
  7. **Interlocked类:**提供了一组原子操作,能够在不使用锁的情况下对共享变量进行线程安全的操作,例如增减,交换,比较交换等
  8. **Task并行库中的常用机制:**如CannellationToken,Barrier,CountdownEvent等,用于更高层次的任务同步

二.锁的作用

  1. **线程同步:**确保多个线程在访问共享资源时,不会产生数据不一致或者是竞态条件
  2. **保证数据的完整性:**通过控制对资源的并发访问,防止数据损坏或者出现不可预期的结果
  3. **管理线程执行顺序:**在复杂的多线程应用中,控制线程的执行顺序,确保程序按照预期的逻辑运行

三.锁的概念和定义

  • **锁(Lock):**是一种同步机制,用于确保在同一时间,只有一个线程可以访问某一资源或者执行某段代码,锁可以防止多个线程共同修改共享数据,导致数据不一致.
  • **互斥(Mutual Exclusion):**指在多线程环境中,确保只有一个线程能够访问共享资源的特性.
  • **临界区(Critical Section):**程序中访问共享资源的代码块,需要被保护以防止并发访问.
  • **死锁(Deadlock):**当多个线程互相等待对方释放资源时,导致程序无法继续执行的状态.

关于锁的完整代码示例

cs 复制代码
    // 共享资源
    private static int _sharedResource = 0;
    private static List<int> _sharedList = new List<int>();
    
    // 各种锁的对象声明
    private static readonly object _lockObject = new object();
    private static readonly object _monitorLock = new object();
    private static readonly Mutex _mutex = new Mutex();
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(2); // 允许2个并发访问
    private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
    private static SpinLock _spinLock = new SpinLock();
    
    static async Task Main(string[] args)
    {
        // 1. Lock示例
        await DemonstrateLock();
        
        // 2. Monitor示例
        await DemonstrateMonitor();
        
        // 3. Mutex示例
        await DemonstrateMutex();
        
        // 4. Semaphore示例
        await DemonstrateSemaphore();
        
        // 5. ReaderWriterLock示例
        await DemonstrateReaderWriterLock();
        
        // 6. SpinLock示例
        await DemonstrateSpinLock();
        
        // 7. Interlocked示例
        DemonstrateInterlocked();
        
        // 8. 任务并行库同步示例
        await DemonstrateTPLSync();
    }

    // 1. Lock关键字示例
    private static async Task DemonstrateLock()
    {
        Console.WriteLine("\n=== Lock示例 ===");
        var tasks = new List<Task>();
        
        for (int i = 0; i < 3; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                // lock块确保同一时间只有一个线程可以执行这段代码
                lock (_lockObject)
                {
                    _sharedResource++;
                    Console.WriteLine($"Lock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                    Thread.Sleep(100); // 模拟工作
                }
            }));
        }
        await Task.WhenAll(tasks);
    }

    // Monitor类示例
    private static async Task DemonstrateMonitor()
    {
        Console.WriteLine("\n=== Monitor示例 ===");
        var tasks = new List<Task>();

        for (int i = 0; i < 3; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                bool lockTaken = false;
                try
                {
                    // 尝试获取锁,设置1秒超时
                    Monitor.TryEnter(_monitorLock, TimeSpan.FromSeconds(1), ref lockTaken);
                    if (lockTaken)
                    {
                        _sharedResource++;
                        Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                        Thread.Sleep(100);
                    }
                    else
                    {
                        Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 未能获取锁");
                    }
                }
                finally
                {
                    // 如果成功获取了锁,则释放
                    if (lockTaken)
                    {
                        Monitor.Exit(_monitorLock);
                    }
                }
            }));
        }
        await Task.WhenAll(tasks);
    }

    // 3. Mutex示例(支持跨进程同步)
    private static async Task DemonstrateMutex()
    {
        Console.WriteLine("\n=== Mutex示例 ===");
        var tasks = new List<Task>();
        
        // 创建一个命名互斥锁,可以跨进程使用
        using (var namedMutex = new Mutex(false, "GlobalMutexExample"))
        {
            for (int i = 0; i < 3; i++)
            {
                tasks.Add(Task.Run(() =>
                {
                    try
                    {
                        namedMutex.WaitOne();
                        _sharedResource++;
                        Console.WriteLine($"Mutex: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                        Thread.Sleep(100);
                    }
                    finally
                    {
                        namedMutex.ReleaseMutex();
                    }
                }));
            }
            await Task.WhenAll(tasks);
        }
    }

    // 4. SemaphoreSlim示例
    private static async Task DemonstrateSemaphore()
    {
        Console.WriteLine("\n=== SemaphoreSlim示例 ===");
        var tasks = new List<Task>();
        
        // 创建4个任务,但同时只允许2个任务执行
        for (int i = 0; i < 4; i++)
        {
            tasks.Add(Task.Run(async () =>
            {
                await _semaphore.WaitAsync();
                try
                {
                    Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 开始执行");
                    await Task.Delay(500);
                }
                finally
                {
                    _semaphore.Release();
                    Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 释放信号量");
                }
            }));
        }
        await Task.WhenAll(tasks);
    }

    // 5. ReaderWriterLockSlim示例
    private static async Task DemonstrateReaderWriterLock()
    {
        Console.WriteLine("\n=== ReaderWriterLockSlim示例 ===");
        var tasks = new List<Task>();
        
        // 添加多个读取任务
        for (int i = 0; i < 3; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                _rwLock.EnterReadLock();
                try
                {
                    Console.WriteLine($"Reader: 线程 {Task.CurrentId} 正在读取");
                    Thread.Sleep(100);
                }
                finally
                {
                    _rwLock.ExitReadLock();
                }
            }));
        }

        // 添加写入任务
        tasks.Add(Task.Run(() =>
        {
            _rwLock.EnterWriteLock();
            try
            {
                Console.WriteLine($"Writer: 线程 {Task.CurrentId} 正在写入");
                Thread.Sleep(200);
            }
            finally
            {
                _rwLock.ExitWriteLock();
            }
        }));

        await Task.WhenAll(tasks);
    }

    // 6. SpinLock示例
    private static async Task DemonstrateSpinLock()
    {
        Console.WriteLine("\n=== SpinLock示例 ===");
        var tasks = new List<Task>();
        
        for (int i = 0; i < 3; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                bool lockTaken = false;
                try
                {
                    _spinLock.Enter(ref lockTaken);
                    _sharedResource++;
                    Console.WriteLine($"SpinLock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                    // SpinLock适用于非常短的操作,这里仅作演示
                }
                finally
                {
                    if (lockTaken) _spinLock.Exit();
                }
            }));
        }
        await Task.WhenAll(tasks);
    }

    // 7. Interlocked示例
    private static void DemonstrateInterlocked()
    {
        Console.WriteLine("\n=== Interlocked示例 ===");
        
        // 原子递增操作
        int value = Interlocked.Increment(ref _sharedResource);
        Console.WriteLine($"Interlocked递增后的值: {value}");

        // 原子交换操作
        int original = Interlocked.Exchange(ref _sharedResource, 100);
        Console.WriteLine($"交换前的值: {original}, 交换后的值: {_sharedResource}");

        // 比较并交换操作
        int comparand = 100;
        int newValue = 200;
        int result = Interlocked.CompareExchange(ref _sharedResource, newValue, comparand);
        Console.WriteLine($"比较交换结果: {result}, 当前值: {_sharedResource}");
    }

    // 8. 任务并行库同步示例
    private static async Task DemonstrateTPLSync()
    {
        Console.WriteLine("\n=== TPL同步示例 ===");
        
        // 使用CancellationTokenSource来控制任务取消
        using (var cts = new CancellationTokenSource())
        {
            // 创建一个Barrier,等待3个任务都完成某个阶段
            var barrier = new Barrier(3);
            var tasks = new List<Task>();

            for (int i = 0; i < 3; i++)
            {
                tasks.Add(Task.Run(() =>
                {
                    Console.WriteLine($"任务 {Task.CurrentId} 开始第一阶段");
                    barrier.SignalAndWait(); // 等待所有任务完成第一阶段

                    Console.WriteLine($"任务 {Task.CurrentId} 开始第二阶段");
                    barrier.SignalAndWait(); // 等待所有任务完成第二阶段

                }, cts.Token));
            }

            await Task.WhenAll(tasks);
        }
Console.ReadKey();
    }

代码逐层剖析:

全局变量与同步变量

cs 复制代码
// 共享资源   
private static int _sharedResource = 0;   
private static List<int> _sharedList = new List<int>();
   
// 各种锁的对象声明   
private static readonly object _lockObject = new object();   
private static readonly object _monitorLock = new object();   
private static readonly Mutex _mutex = new Mutex();   
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(2); // 允许2个并发访问   
private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();   
private static SpinLock _spinLock = new SpinLock();

_sharedResource:这是一个整数,作为共享的资源,多个线程会对其进行读写

_sharedList:一个整数列表,用于示例需要

_lockObject等同步对象:这些对象用于各自的同步原语,以确保线程安全

Lock(锁)关键字示例

cs 复制代码
 private static async Task DemonstrateLock()
 {
     Console.WriteLine("\n=== Lock示例 ===");
     var tasks = new List<Task>();
     
     for (int i = 0; i < 3; i++)
     {
         tasks.Add(Task.Run(() =>
         {
             // lock块确保同一时间只有一个线程可以执行这段代码
             lock (_lockObject)
             {
                 _sharedResource++;
                 Console.WriteLine($"Lock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                 Thread.Sleep(100); // 模拟工作
                 Console.WriteLine($"Lock: 线程 {Task.CurrentId} 释放锁");
             }
         }));
     }
     await Task.WhenAll(tasks);
 }

代码解释:

  • **目的:**演示了lock关键字的使用,即如何确保在一个代码块内同时只能有一个线程运行
  • 实现细节:
    • 创建了三个任务,每个任务都尝试访问共享资源 _sharedResource
    • 使用lock(_lockObject)来锁定代码块,这样同一时间只有一个线程能够进入该代码块
    • 在锁定的代码块内,递增_sharedResource的值并输出当前线程的信息
  • **重要性:**lock是C#中最基本的同步机制,使用简单,但只能用于同一进程内的线程同步

代码运行结果展示

Monitor(监视器锁)示例

cs 复制代码
private static async Task DemonstrateMonitor()   
{
    Console.WriteLine("\n=== Monitor示例 ===");
    var tasks = new List<Task>();

    for (int i = 0; i < 3; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            bool lockTaken = false;
            try
            {
                // 尝试获取锁,设置1秒超时
                Monitor.TryEnter(_monitorLock, TimeSpan.FromSeconds(1), ref lockTaken);
                if (lockTaken)
                {
                    _sharedResource++;
                    Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                    Thread.Sleep(100);
                }
                else
                {
                    Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 未能获取锁");
                }
            }
            finally
            {
                // 如果成功获取了锁,则释放
                if (lockTaken)
                {
                    Console.WriteLine($"Monitor: 线程 {Task.CurrentId} 释放锁");
                    Monitor.Exit(_monitorLock);
                }
            }
        }));
    }
    await Task.WhenAll(tasks);
}

代码解释:

  • **目的:**演示Monitor类的使用,特别是TryEnter方法,以及如何手动管理锁的获取和释放
  • 实现细节:
    • 创建了三个任务,每个任务尝试获取_monitorLock的锁
    • 使用Monitor.TryEnter方法,设置了一秒的超时时间,如果在这段时间内未能获取到锁,则返回False
    • lockTaken是一个布尔量,表示是否成功获取到锁
    • 如果成功获取了锁,执行共享资源的操作;否则输出未能获取锁的信息
    • 在Finally块中,检查是否获取了锁,如果是,则调用Monitor.Exit释放锁
  • **重要性:**Monitor类提供了比lock更灵活的机制,比如超时等待,尝试获取锁等

代码运行结果展示:

Mutex(互斥量)示例(支持跨进程同步)

cs 复制代码
private static async Task DemonstrateMutex()
{
    Console.WriteLine("\n=== Mutex示例 ===");
    var tasks = new List<Task>();
    
    // 创建一个命名互斥锁,可以跨进程使用
    using (var namedMutex = new Mutex(false, "GlobalMutexExample"))
    {
        for (int i = 0; i < 3; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                try
                {
                    namedMutex.WaitOne();
                    _sharedResource++;
                    Console.WriteLine($"Mutex: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                    Thread.Sleep(100);
                }
                finally
                {
                    Console.WriteLine($"Mutex: 线程 {Task.CurrentId} 释放锁");
                    namedMutex.ReleaseMutex();
                }
            }));
        }
        await Task.WhenAll(tasks);
    }
}

代码解释:

  • **目的:**演示Mutex(互斥量)的使用,特别是命名互斥量的跨进程同步特性
  • 实现细节:
    • 使用 new Mutex(false,"GlobalMutexExample")创建了一个命名的全局互斥锁,可以在不同的进程间共享
    • 创建了3个任务,每个任务尝试进入互斥锁的临界区
    • 使用 namedMutex.WaitOne()等待获取互斥锁
    • 获取锁后,修改共享资源并输出信息
    • 在finally块中,调用namedMutex.ReleaseMutex()释放互斥锁
  • 重要性:Mutex可以用于跨进程的同步,这在需要多个进程访问同一资源时十分有用

代码运行结果展示:

SemaphoreSlim(信号量)示例

cs 复制代码
private static async Task DemonstrateSemaphore()   
{
    Console.WriteLine("\n=== SemaphoreSlim示例 ===");
    var tasks = new List<Task>();
    
    // 创建4个任务,但同时只允许2个任务执行
    for (int i = 0; i < 4; i++)
    {
        tasks.Add(Task.Run(async () =>
        {
            await _semaphore.WaitAsync();
            try
            {
                Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 开始执行");
                await Task.Delay(500);
            }
            finally
            {
                _semaphore.Release();
                Console.WriteLine($"Semaphore: 线程 {Task.CurrentId} 释放信号量");
            }
        }));
    }
    await Task.WhenAll(tasks);
}

代码解释:

  • **目的:**演示SemaphoreSlim的使用,控制同时访问某资源的线程数
  • 实现细节:
    • _semaphore初始化时设置了初始计数值为2,因此最多允许2个线程同时进入
    • 创建了四个任务,每个任务都试图获取信号量
    • 使用await _semaphore.WaitAsync() 异步地等待信号量
    • 获取信号量后,输出信息并等待500毫秒,模拟工作
    • 在finally块中,调用 _semaphore.Release() 方法去释放信号量,并输出信息
  • **重要性:**信号量用于限制同时访问某资源或代码块的线程数量,SemaphoreSlim是线程内的轻量级版本

代码运行展示:

ReadWriterLockSlim(读写锁)示例

cs 复制代码
private static async Task DemonstrateReaderWriterLock()   
{
    Console.WriteLine("\n=== ReaderWriterLockSlim示例 ===");
    var tasks = new List<Task>();
    
    // 添加多个读取任务
    for (int i = 0; i < 3; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            _rwLock.EnterReadLock();
            try
            {
                Console.WriteLine($"Reader: 线程 {Task.CurrentId} 正在读取");
                Thread.Sleep(100);
            }
            finally
            {
                _rwLock.ExitReadLock();
            }
        }));
    }

    // 添加写入任务
    tasks.Add(Task.Run(() =>
    {
        _rwLock.EnterWriteLock();
        try
        {
            Console.WriteLine($"Writer: 线程 {Task.CurrentId} 正在写入");
            Thread.Sleep(200);
        }
        finally
        {
            _rwLock.ExitWriteLock();
        }
    }));

    await Task.WhenAll(tasks);
}

代码解释:

  • **目的:**演示ReadWriterLockSlim的使用,允许多个线程同时读取,但写入时需要独占
  • 实现细节:
    • 创建了3个读取任务,使用_rwLock.EnterReadLock()获取读锁
    • 创建了1个写入任务,使用_rwLock.EnterWriteLock()获取写锁
    • 读取任务之间可以并发执行,但写入任务需要等待所有读锁释放后才能获取写锁
    • 每个任务在完成后,调用相应的ExitReadLock()和ExitWriteLock()释放锁
  • **重要性:**读写锁提高了并发性能,适用于读多写少的情景

代码运行结果展示:

SpinLock(自旋锁)示例

cs 复制代码
private static async Task DemonstrateSpinLock()   
{
    Console.WriteLine("\n=== SpinLock示例 ===");
    var tasks = new List<Task>();
    
    for (int i = 0; i < 3; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            bool lockTaken = false;
            try
            {
                _spinLock.Enter(ref lockTaken);
                _sharedResource++;
                Console.WriteLine($"SpinLock: 线程 {Task.CurrentId} 修改资源值为: {_sharedResource}");
                // SpinLock适用于非常短的操作,这里仅作演示
            }
            finally
            {
                if (lockTaken) _spinLock.Exit();
            }
        }));
    }
    await Task.WhenAll(tasks);
}

代码解释:

  • **目的:**演示了SpinLock的使用,它是一种自旋锁,适用于非常短时间的锁定操作
  • 实现细节 :
    • 创建了三个任务,每个任务都尝试获取_spinLock的锁
    • 使用_spinLock.Enter(ref lockTaken)来获取锁
    • 获取锁后,修改共享资源并输出信息
    • 在finally块中,检查lockTaken,如果获取了锁,则调用_spinLock.Exit()方法释放锁
  • **重要性:**SpinLock适用于非常短的临界区,因为它会一直循环等待直至获取锁,避免线程上下文的切换,但如果操作时间过长,会导致CPU占用过高

在使用自旋锁时,一定要去释放锁,否则会造成死锁

代码运行结果展示:

InterLocked示例

cs 复制代码
private static void DemonstrateInterlocked()   
{
    Console.WriteLine("\n=== Interlocked示例 ===");
    
    // 原子递增操作
    int value = Interlocked.Increment(ref _sharedResource);
    Console.WriteLine($"Interlocked递增后的值: {value}");

    // 原子交换操作
    int original = Interlocked.Exchange(ref _sharedResource, 100);
    Console.WriteLine($"交换前的值: {original}, 交换后的值: {_sharedResource}");

    // 比较并交换操作
    int comparand = 100;
    int newValue = 200;
    int result = Interlocked.CompareExchange(ref _sharedResource, newValue, comparand);
    Console.WriteLine($"比较交换结果: {result}, 当前值: {_sharedResource}");
}

代码解释:

  • **目的:**演示InterLocked类的使用,用于执行原子操作,避免显示的锁定
  • 实现细节:
    • Interlocked.Increment:对共享变量执行原子递增操作,返回递增后的值
    • Interlocked.Exchange:将共享变量设置为新值,并返回原始值
    • Interlocked.CompareExchange:如果共享变量的值等于比较值,则将其设置为新值,并返回原始值
  • 重要性:InterLocked提供了高效的线程安全操作,适用于简单的变量操作,而不需要使用锁

代码运行结果展示:

任务并行库(TPL)同步示例

cs 复制代码
private static async Task DemonstrateTPLSync()   
{
    Console.WriteLine("\n=== TPL同步示例 ===");
    
    // 使用CancellationTokenSource来控制任务取消
    using (var cts = new CancellationTokenSource())
    {
        // 创建一个Barrier,等待3个任务都完成某个阶段
        var barrier = new Barrier(3);
        var tasks = new List<Task>();

        for (int i = 0; i < 3; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                Console.WriteLine($"任务 {Task.CurrentId} 开始第一阶段");
                barrier.SignalAndWait(); // 等待所有任务完成第一阶段

                Console.WriteLine($"任务 {Task.CurrentId} 开始第二阶段");
                barrier.SignalAndWait(); // 等待所有任务完成第二阶段

            }, cts.Token));
        }

        await Task.WhenAll(tasks);
    }
    Console.ReadKey();
}

代码解释:

  • 目的:演示任务并行库(TPL)中的同步机制,特别是Barrier的使用
  • 实现细节:
    • 创建了一个Barrier,参与方数量为3,表示需要等待三个任务
    • 创建了3个任务,每个任务在两个阶段都调用了barrier.SignalAndWait();
      • 第一阶段:任务输出开始第一阶段的信息,然后调用barrier.SignalAndWait(),等待其他任务也完成第一阶段
      • 第二阶段:任务输出开始第二阶段的信息,然后再次调用barrier.SignalAndWait()
    • CancellationTokenSource可以控制任务的取消
  • 重要性:Barrier用于协调多个任务的执行,使得它们可以在某个同步点等待彼此,同步进入下一阶段的操作

代码运行结果展示:


总结

  • lock和Monitor:适用于简单的线程同步,同一进程内的线程
  • Mutex:可用于跨进程的同步,但性能较 lock 较低
  • SemaphoreSlim:控制同时访问资源的线程数量
  • ReadWriterLockSlim:优化读多写少的场景,提高并发性能
  • SpinLock:适用于非常短的锁定操作,避免线程上下文切换,但可能导致CPU占用高
  • InterLocked:提供简单的原子操作,性能高,没有锁开销
  • 任务并行库同步机制""提供高级的同步方式,如 Barrier,CountdownEvent
相关推荐
m0_7487080516 分钟前
C++中的观察者模式实战
开发语言·c++·算法
qq_5375626728 分钟前
跨语言调用C++接口
开发语言·c++·算法
wjs202439 分钟前
DOM CDATA
开发语言
Tingjct40 分钟前
【初阶数据结构-二叉树】
c语言·开发语言·数据结构·算法
猷咪1 小时前
C++基础
开发语言·c++
IT·小灰灰1 小时前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧1 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q1 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳01 小时前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾1 小时前
php 对接deepseek
android·开发语言·php