多线程编程之常见错误实践和优秀实践

开发 前端
在多个线程同时访问共享资源时,可能会发生数据竞争,导致程序错误。为了避免竞态条件,可以使用同步机制,例如互斥锁、信号量、条件变量等,确保同一时刻只有一个线程访问共享资源。

在多线程编程中,由于存在共享资源和竞争条件等问题,容易出现各种错误。以下是一些常见的多线程编程错误及如何避免它们:

竞态条件(Race Condition):在多个线程同时访问共享资源时,可能会发生数据竞争,导致程序错误。为了避免竞态条件,可以使用同步机制,例如互斥锁、信号量、条件变量等,确保同一时刻只有一个线程访问共享资源。

错误实践代码:

int count = 0;

// 创建 10 个线程对共享变量进行累加操作
for (int i = 0; i < 10; i++)
{
    new Thread(() =>
    {
        for (int j = 0; j < 1000; j++)
        {
            count++;
        }
    }).Start();
}

// 等待所有线程执行完成后输出累加结果
Thread.Sleep(1000);
Console.WriteLine("count = " + count);

上述代码会启动 10 个线程对共享的变量 count 进行累加操作。由于 count 变量是共享的,多个线程可能会同时访问 count,导致数据竞争,从而导致程序错误。

最佳实践代码:

int count = 0;
object lockObj = new object();

// 创建 10 个线程对共享变量进行累加操作
for (int i = 0; i < 10; i++)
{
    new Thread(() =>
    {
        for (int j = 0; j < 1000; j++)
        {
            lock(lockObj)
                count++;
        }
    }).Start();
}

// 等待所有线程执行完成后输出累加结果
Thread.Sleep(1000);
Console.WriteLine("count = " + count);

在最佳实践示例代码中,使用了互斥锁来保护共享变量 count 的访问,确保同一时刻只有一个线程对 count 进行操作。

死锁(Deadlock):当多个线程同时等待对方释放资源时,可能会出现死锁情况,导致程序无法继续执行。为了避免死锁,需要合理地设计同步流程,避免出现环路等结构。

错误实践代码:

object lockObj1 = new object();
object lockObj2 = new object();

// 线程 1
new Thread(() =>
{
    lock(lockObj1)
    {
        Console.WriteLine("thread1 acquired lock1");
        Thread.Sleep(1000);

        lock (lockObj2)
        {
            Console.WriteLine("thread1 acquired lock2");
        }
    }
}).Start();

// 线程 2
new Thread(() =>
{
    lock (lockObj2)
    {
        Console.WriteLine("thread2 acquired lock2");
        Thread.Sleep(1000);

        lock (lockObj1)
        {
            Console.WriteLine("thread2 acquired lock1");
        }
    }
}).Start();

上述代码中,两个线程分别占用不同的锁 lockObj1 和 lockObj2,并且在使用完一个锁之后尝试获取另一个锁,从而可能导致死锁的问题。

最佳实践代码:

object lockObj1 = new object();
object lockObj2 = new object();

// 线程 1
new Thread(() =>
{
    lock(lockObj1)
    {
        Console.WriteLine("thread1 acquired lock1");
        Thread.Sleep(1000);

        lock (lockObj2)
        {
            Console.WriteLine("thread1 acquired lock2");
        }
    }
}).Start();

// 线程 2
new Thread(() =>
{
    lock (lockObj1)
    {
        Console.WriteLine("thread2 acquired lock1");
        Thread.Sleep(1000);

        lock (lockObj2)
        {
            Console.WriteLine("thread2 acquired lock2");
        }
    }
}).Start();

在最佳实践示例代码中,将两个线程获取锁的顺序统一为 lockObj1 -> lockObj2,从而避免死锁问题。

过度的锁竞争(Lock Contention):当多个线程在高频率地访问同一个锁时,可能会导致过度的锁竞争,降低程序的并发性能。为了避免过度的锁竞争,可以使用非阻塞算法、读写锁等替代互斥锁;也可以尝试减小锁粒度,将锁的范围缩小到最小。

错误实践代码:

object lockObj = new object();
List<int> list = new List<int>();
Random random = new Random();

// 创建 10 个线程对共享集合进行操作,使用互斥锁保护 list 的并发访问
for (int i = 0; i < 10; i++)
{
    new Thread(() =>
    {
        for (int j = 0; j < 10000; j++)
        {
            lock(lockObj)
            {
                // 使用随机数生成一个新的元素并添加到集合中
                int randNum = random.Next(100);
                list.Add(randNum);
            }
        }
    }).Start();
}

// 等待所有线程执行完成后输出集合元素个数
Thread.Sleep(1000);
Console.WriteLine("list count = " + list.Count);

在上述代码中,由于使用了互斥锁保护集合的并发访问,每个线程在对集合进行操作时都需要获取锁,从而可能导致过度的锁竞争,导致程序性能下降。

最佳实践代码:

ConcurrentBag<int> bag = new ConcurrentBag<int>();
Random random = new Random();

// 创建 10 个线程对共享集合进行操作,使用并发容器代替互斥锁进行线程安全的并发访问
for (int i = 0; i < 10; i++)
{
    new Thread(() =>
    {
        for (int j = 0; j < 10000; j++)
        {
            // 使用随机数生成一个新的元素并添加到集合中
            int randNum = random.Next(100);
            bag.Add(randNum);
        }
    }).Start();
}

// 等待所有线程执行完成后输出集合元素个数
Thread.Sleep(1000);
Console.WriteLine("list count = " + bag.Count);

在最佳实践示例代码中,使用了线程安全的并发容器 ConcurrentBag 代替了互斥锁,确保了集合的线程安全,同时避免了过度的锁竞争问题。

上下文切换(Context Switching):当多个线程在不断地切换执行时,可能会引起上下文切换的开销增加,从而导致程序性能下降。为了避免上下文切换,可以使用线程池等技术,减少线程的创建和销毁操作。

错误实践代码:

List<int> list = new List<int>();

// 创建 100 个线程对共享集合进行操作
for(int i = 0;i < 100;i++)
{
    new Thread(() =>
    {
        for(int j = 0;j < 100000;j++)
        {
            // 在集合中添加一个元素
            list.Add(1);
        }
    }).Start();
}

// 等待所有线程执行完成后输出集合元素个数
Thread.Sleep(5000);
Console.WriteLine("list count = " + list.Count);

在上述代码中,由于同时启动了大量的线程,在并发执行时会不断地进行上下文切换,导致程序性能下降

最佳实践代码:

const int threadCount = 10;
List<int> list = new List<int>();

// 使用线程池创建多个线程,避免频繁的线程创建和销毁操作
for(int i = 0;i < threadCount;i++)
{
    ThreadPool.QueueUserWorkItem((state) =>
    {
        for(int j = 0;j < 100000;j++)
        {
            // 在集合中添加一个元素
            lock(list)
                list.Add(1);
        }
    });
}

// 等待所有线程执行完成后输出集合元素个数
while(Thread.VolatileRead(ref threadCount) > 0)
{
    Thread.Sleep(100);
}
Console.WriteLine("list count = " + list.Count);

在最佳实践示例代码中,使用线程池代替了手动创建线程的方式,避免了频繁的线程创建和销毁操作,从而减少了上下文切换的开销。此外,在访问共享变量 list 时,使用了互斥锁来确保线程安全。

内存泄漏(Memory Leak):在多线程编程中,由于对资源的释放不当,可能会引发内存泄漏问题。为了避免内存泄漏,需要正确地使用内存管理机制,并保证资源在使用完毕后及时释放。

错误实践代码:

class ResourceHolder
{
    private byte[] buffer = new byte[1024 * 1024 * 10];

    // 析构函数
    ~ResourceHolder()
    {
        Console.WriteLine("ResourceHolder finalized.");
    }
}

// 创建 100 个线程,每个线程都会创建一个 ResourceHolder 对象并存储在集合中
List<ResourceHolder> holders = new List<ResourceHolder>();
for(int i = 0;i < 100;i++)
{
    new Thread(() =>
    {
        holders.Add(new ResourceHolder());
    }).Start();
}

// 等待所有线程执行完成后等待一段时间,触发 GC 进行垃圾回收
Thread.Sleep(5000);
GC.Collect();

Console.WriteLine("Done.");

在上述代码中,由于创建了大量的 ResourceHolder 对象,并将其存储在集合中,但是没有及时释放这些对象,从而可能导致内存泄漏的问题。

最佳实践代码:

class ResourceHolder : IDisposable
{
    private byte[] buffer = new byte[1024 * 1024 * 10];

    // 实现 IDisposable 接口
    public void Dispose()
    {
        Console.WriteLine("ResourceHolder disposed.");
    }
}

// 创建 100 个线程,每个线程都会创建一个 ResourceHolder 对象并存储在集合中
List<ResourceHolder> holders = new List<ResourceHolder>();
for(int i = 0;i < 100;i++)
{
    new Thread(() =>
    {
        // 使用 using 语句块确保及时释放资源
        using(ResourceHolder holder = new ResourceHolder())
        {
            holders.Add(holder);
        }
    }).Start();
}

// 等待所有线程执行完成后输出 Done
Thread.Sleep(5000);
Console.WriteLine("Done.");

在最佳实践示例代码中,使用了 IDisposable 接口和 using 语句块来确保及时释放资源,避免了内存泄漏问题。

除此之外,还有一些其他的多线程编程错误,例如访问未初始化的共享资源、线程间通信不当、异常处理不当等。为了避免这些错误,需要在编码过程中严格遵循多线程编程的最佳实践,例如使用安全的并发容器、避免锁策略过度简单、避免线程死循环等。同时,在编码过程中仔细阅读相关文档和资料,了解当前使用的库或框架的特性和限制,以确保代码的正确性和健壮性。

责任编辑:姜华 来源: 今日头条
相关推荐

2009-02-24 08:36:51

多线程线程池网络服务器

2023-10-26 12:01:30

Golang字符串

2022-08-24 08:16:33

容器安全容器

2020-08-20 07:54:58

Node多线程解密

2013-05-28 15:35:47

html5多线程

2024-01-23 13:00:00

Arthas命令Java

2024-10-10 09:46:18

2021-02-25 15:58:46

C++线程编程开发技术

2020-10-21 11:55:44

Shell编程语言Linux

2021-03-05 07:38:52

C++线程编程开发技术

2019-09-06 09:00:00

开发技能代码

2013-06-07 16:30:08

iOS多线程iOS开发NSThread

2009-06-16 13:48:42

Java多线程

2022-10-20 10:02:16

前端测试开发

2023-05-16 15:25:08

2024-05-20 10:00:00

代码Python编程

2023-06-13 13:39:00

多线程异步编程

2009-03-12 10:52:43

Java线程多线程

2024-04-30 12:56:00

多线程.NET

2023-09-19 14:59:47

线程开发
点赞
收藏

51CTO技术栈公众号