C#开发基础之多线程编程的常见错误实践和最佳实践

news2024/12/24 2:52:38

在这里插入图片描述

前言

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

1. 竞态条件(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 进行操作。

2. 死锁(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,从而避免死锁问题。

3. 过度的锁竞争(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 代替了互斥锁,确保了集合的线程安全,同时避免了过度的锁竞争问题。

4. 上下文切换(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 时,使用了互斥锁来确保线程安全。

5. 内存泄漏(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 语句块来确保及时释放资源,避免了内存泄漏问题。

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2096918.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络 第1章

计算机网络概念 计算机网络&#xff08;Computer networking&#xff09;是一个分散的、自治的计算机系统&#xff0c;通过通信设备与线路连接起来&#xff0c;由功能完善的软件实现资源共享和信息传递的系统。 计算机网络&#xff08;简称网络&#xff09;&#xff1a;由若干结…

CAD | 背景改为白色

Op空格 打开选项面板 /或者直接右键选项 然后 显示-颜色-二位模型空间-统一背景-白-应用并关闭-确定

Red Hat 9 — Red Hat 9.4Linux系统 虚拟机安装【保姆级教程】

Mac分享吧 文章目录 效果一、下载软件二、安装软件与配置1、安装2、配置 三、查看基本信息安装完成&#xff01;&#xff01;&#xff01; 效果 一、下载软件 下载软件 地址&#xff1a;www.macfxb.cn 二、安装软件与配置 1、安装 2、配置 三、查看基本信息 安装完成&#xf…

Python批量提取pdf标题-作者信息

程序示例精选 Python批量提取pdf标题-作者信息 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《Python批量提取pdf标题-作者信息》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0…

编译FFmpeg动态库

编译FFmpeg动态库 环境 macOS High SierraFFmpeg 4.3android-ndk-r21b 编译so库 下载FFmpeg4.3源代码&#xff0c;进入源码目录创建build_android.sh脚本&#xff0c;ffmpeg从4.0起新增了target-osandroid&#xff0c;所以不用再修改configure文件。 注意&#xff1a; ndk…

WPF 手撸插件 七 日志记录

1、环境日志这里使用的是log4net. 2、WPF全局捕获异常&#xff0c;代码如下。 using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Ta…

系统架构设计师——系统性能

性能指标 计算机性能指标 操作系统性能指标 网络的性能指标 数据库的性能指标 数据库管理系统的性能指标 应用系统的性能指标 Web服务器的性能指标 性能计算 定义法 计算方法主要包括定义法、公式法、程序检测法和仪器检测法。这些方法分别通过直接获取理想数据、应用衍生出的…

【docker】docker 镜像仓库的管理

Docker 仓库&#xff08; Docker Registry &#xff09; 是用于存储和分发 Docker 镜像的集中式存储库。 它就像是一个大型的镜像仓库&#xff0c;开发者可以将自己创建的 Docker 镜像推送到仓库中&#xff0c;也可以从仓库中拉取所需的镜像。 Docker 仓库可以分为公共仓…

Jsoncpp的安装与使用

目录 安装Jsoncpp Jsoncpp的使用 Value类 构造函数 检测保存的数据类型 提取数据 对json数组的操作 对Json对象的操作 FastWriter类 Reader类 JsonCpp 是一个C库&#xff0c;用于解析和生成JSON数据。它支持解析JSON文件或字符串到C对象&#xff0c;以及将C对象序列…

MySQL的安装—>Mariadb的安装(day21)

该网盘链接有效期为7天&#xff0c;有需要评论区扣我&#xff1a; 通过网盘分享的文件&#xff1a;mariadb-10.3.7-winx64.msi 链接: https://pan.baidu.com/s/1-r_w3NuP8amhIEedmTkWsQ?pwd2ua7 提取码: 2ua7 1 双击打开安装软件 本次安装的是mariaDB&#xff0c;双击打开mar…

SprinBoot+Vue学生选课微信小程序的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue3.6 uniapp代码 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平…

python进阶篇-day03-学生管理系统与深浅拷贝

day03-学生管理系统-面向对象 魔术方法: __ dict __将对象的属性和属性值封装为字典 用字典的值实例化对象: 对象名(**字典) > 拆包 student.py """ 该文件记录的是: 学生类的信息. ​ 学生的属性如下:姓名, 性别, 年龄, 联系方式, 描述信息 ""&…

单片机-STM32 ADC应用(五)

1.ADC模数转换 模拟数字转换器即A/D转换器&#xff0c;或简称ADC&#xff0c;通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义&#xff0c;仅仅表示一个相对大小。故任何一个…

STM32学习记录-11-RTC实时时钟

1 Unix时间戳 Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间 2 UTC/GMT GMT(Green…

量化面试题:什么是朴素贝叶斯分类器?

朴素贝叶斯分类器是一种基于贝叶斯定理的简单而有效的分类算法。它的核心思想是利用特征之间的条件独立性假设来进行分类。以下是朴素贝叶斯分类器的几个关键点&#xff1a; 贝叶斯定理&#xff1a;朴素贝叶斯分类器基于贝叶斯定理&#xff0c;该定理描述了在已知某些条件下&a…

名城优企游学活动走进龙腾半导体:CRM助力构建营销服全流程体系

8月29日&#xff0c;由纷享销客主办的“数字中国 高效增长——名城优企游学系列活动之走进龙腾半导体”研讨会在西安市圆满落幕&#xff0c;来自业内众多领袖专家参与本次研讨会&#xff0c;深入分享交流半导体行业的数字化转型实践&#xff0c;探讨行业数字化、智能化转型之路…

华大智造 否极泰来

甲辰年开年至今&#xff0c;华大智造&#xff08;688114.SH&#xff09;经历了上市以来“最漫长的季节”。 仅在这半年多时间里&#xff0c;这家已经实现全球化布局且能排位在行业最前列的中国生命科技企业&#xff0c;遭遇了几乎所有能遭遇的不利局面。 宏观环境&#xff0c…

前端代码提交前的最后防线:使用Husky确保代码质量

需求背景 我们通常会引入ESLint和Prettier这样的工具来帮助我们规范本地代码的格式。然而&#xff0c;这种格式化过程仅在本地有效&#xff0c;并且依赖于我们在VSCode中手动设置自动保存功能。如果团队成员忘记进行这样的配置&#xff0c;或者在没有格式化的情况下提交了代码…

GIS地理信息+智慧巡检技术解决方案(Word原件)

1.系统概述 1.1.需求描述 1.2.需求分析 1.3.重难点分析 1.4.重难点解决措施 2.系统架构设计 2.1.系统架构图 2.2.关键技术 3.系统功能设计 3.1.功能清单列表 软件全套资料部分文档清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#x…

Datawhale X 李宏毅苹果书AI夏令营 Task1.2深度学习进阶详解

目录 一、批量梯度下降法&#xff08;Batch Gradient Descent&#xff0c;BGD&#xff09; 二、随机梯度下降法&#xff08;Stochastic Gradient Descent&#xff0c;SGD&#xff09; 三、动量法&#xff08;Momentum Method&#xff09; 四、自适应学习率的方法 五、并行…