C# ConcurrentBag的实现原理

一、前言

笔者最近在做一个项目,项目中为了提升吞吐量,使用了消息队列,中间实现了生产消费模式,在生产消费者模式中需要有一个集合,来存储生产者所生产的物品,笔者使用了最常见的List<T>集合类型。

由于生产者线程有很多个,消费者线程也有很多个,所以不可避免的就产生了线程同步的问题。开始笔者是使用lock关键字,进行线程同步,但是性能并不是特别理想,然后有网友说可以使用SynchronizedList<T>来代替使用List<T>达到线程安全的目的。于是笔者就替换成了SynchronizedList<T>,但是发现性能依旧糟糕,于是查看了SynchronizedList<T>的源代码,发现它就是简单的在List<T>提供的API的基础上加了lock,所以性能基本与笔者实现方式相差无几。

最后笔者找到了解决的方案,使用ConcurrentBag<T>类来实现,性能有很大的改观,于是笔者查看了ConcurrentBag<T>的源代码,实现非常精妙,特此在这记录一下。

二、ConcurrentBag类

ConcurrentBag<T>实现了IProducerConsumerCollection<T>接口,该接口主要用于生产者消费者模式下,可见该类基本就是为生产消费者模式定制的。然后还实现了常规的IReadOnlyCollection<T>类,实现了该类就需要实现IEnumerable<T>、IEnumerable、 ICollection类。

ConcurrentBag<T>对外提供的方法没有List<T>那么多,但是同样有Enumerable实现的扩展方法。类本身提供的方法如下所示。

三、 ConcurrentBag线程安全实现原理

1. ConcurrentBag的私有字段

ConcurrentBag线程安全实现主要是通过它的数据存储的结构和细颗粒度的锁。

 public class ConcurrentBag<T> : IProducerConsumerCollection<T>, IReadOnlyCollection<T>{        // ThreadLocalList对象包含每个线程的数据ThreadLocal<ThreadLocalList> m_locals; // 这个头指针和尾指针指向中的第一个和最后一个本地列表,这些本地列表分散在不同线程中// 允许在线程局部对象上枚举volatile ThreadLocalList m_headList, m_tailList; // 这个标志是告知操作线程必须同步操作// 在GlobalListsLock 锁中 设置bool m_needSync;}

首选我们来看它声明的私有字段,其中需要注意的是集合的数据是存放在ThreadLocal线程本地存储中的。也就是说访问它的每个线程会维护一个自己的集合数据列表,一个集合中的数据可能会存放在不同线程的本地存储空间中,所以如果线程访问自己本地存储的对象,那么是没有问题的,这就是实现线程安全的第一层,使用线程本地存储数据。

然后可以看到ThreadLocalList m_headList, m_tailList;这个是存放着本地列表对象的头指针和尾指针,通过这两个指针,我们就可以通过遍历的方式来访问所有本地列表。它使用volatile修饰,所以它是线程安全的。

最后又定义了一个标志,这个标志告知操作线程必须进行同步操作,这是实现了一个细颗粒度的锁,因为只有在几个条件满足的情况下才需要进行线程同步。

2. 用于数据存储的TrehadLocalList类

接下来我们来看一下ThreadLocalList类的构造,该类就是实际存储了数据的位置。实际上它是使用双向链表这种结构进行数据存储。

[Serializable]// 构造了双向链表的节点internal class Node{    public Node(T value)    {m_value = value;}    public readonly T m_value;    public Node m_next;    public Node m_prev;
}/// <summary>/// 集合操作类型/// </summary>internal enum ListOperation
{None,Add,Take
};/// <summary>/// 线程锁定的类/// </summary>internal class ThreadLocalList{    // 双向链表的头结点 如果为null那么表示链表为空internal volatile Node m_head;    // 双向链表的尾节点private volatile Node m_tail;    // 定义当前对List进行操作的种类 // 与前面的 ListOperation 相对应internal volatile int m_currentOp;    // 这个列表元素的计数private int m_count;    // The stealing count// 这个不是特别理解 好像是在本地列表中 删除某个Node 以后的计数internal int m_stealCount;    // 下一个列表 可能会在其它线程中internal volatile ThreadLocalList m_nextList;    // 设定锁定是否已进行internal bool m_lockTaken;    // The owner thread for this listinternal Thread m_ownerThread;    // 列表的版本,只有当列表从空变为非空统计是底层internal volatile int m_version;    /// <summary>/// ThreadLocalList 构造器/// </summary>/// <param name="ownerThread">拥有这个集合的线程</param>internal ThreadLocalList(Thread ownerThread)    {m_ownerThread = ownerThread;}    /// <summary>/// 添加一个新的item到链表首部/// </summary>/// <param name="item">The item to add.</param>/// <param name="updateCount">是否更新计数.</param>internal void Add(T item, bool updateCount)    {        checked{m_count++;}Node node = new Node(item);        if (m_head == null){Debug.Assert(m_tail == null);m_head = node;m_tail = node;m_version++; // 因为进行初始化了,所以将空状态改为非空状态}        else{            // 使用头插法 将新的元素插入链表node.m_next = m_head;m_head.m_prev = node;m_head = node;}        if (updateCount) // 更新计数以避免此添加同步时溢出{m_count = m_count - m_stealCount;m_stealCount = 0;}}    /// <summary>/// 从列表的头部删除一个item/// </summary>/// <param name="result">The removed item</param>internal void Remove(out T result)    {        // 双向链表删除头结点数据的流程Debug.Assert(m_head != null);Node head = m_head;m_head = m_head.m_next;        if (m_head != null){m_head.m_prev = null;}        else{m_tail = null;}m_count--;result = head.m_value;}    /// <summary>/// 返回列表头部的元素/// </summary>/// <param name="result">the peeked item</param>/// <returns>True if succeeded, false otherwise</returns>internal bool Peek(out T result)    {Node head = m_head;        if (head != null){result = head.m_value;            return true;}result = default(T);        return false;}    /// <summary>/// 从列表的尾部获取一个item/// </summary>/// <param name="result">the removed item</param>/// <param name="remove">remove or peek flag</param>internal void Steal(out T result, bool remove)    {Node tail = m_tail;Debug.Assert(tail != null);        if (remove) // Take operation{m_tail = m_tail.m_prev;            if (m_tail != null){m_tail.m_next = null;}            else{m_head = null;}            // Increment the steal countm_stealCount++;}result = tail.m_value;}    /// <summary>/// 获取总计列表计数, 它不是线程安全的, 如果同时调用它, 则可能提供不正确的计数/// </summary>internal int Count{        get{            return m_count - m_stealCount;}}
}

从上面的代码中我们可以更加验证之前的观点,就是ConcurentBag<T>在一个线程中存储数据时,使用的是双向链表,ThreadLocalList实现了一组对链表增删改查的方法。

3. ConcurrentBag实现新增元素

接下来我们看一看ConcurentBag<T>是如何新增元素的。

/// <summary>/// 尝试获取无主列表,无主列表是指线程已经被暂停或者终止,但是集合中的部分数据还存储在那里/// 这是避免内存泄漏的方法/// </summary>/// <returns></returns>private ThreadLocalList GetUnownedList(){    //此时必须持有全局锁Contract.Assert(Monitor.IsEntered(GlobalListsLock));    // 从头线程列表开始枚举 找到那些已经被关闭的线程// 将它所在的列表对象 返回ThreadLocalList currentList = m_headList;    while (currentList != null){        if (currentList.m_ownerThread.ThreadState == System.Threading.ThreadState.Stopped){currentList.m_ownerThread = Thread.CurrentThread; // the caller should acquire a lock to make this line thread safereturn currentList;}currentList = currentList.m_nextList;}    return null;
}/// <summary>/// 本地帮助方法,通过线程对象检索线程线程本地列表/// </summary>/// <param name="forceCreate">如果列表不存在,那么创建新列表</param>/// <returns>The local list object</returns>private ThreadLocalList GetThreadList(bool forceCreate){ThreadLocalList list = m_locals.Value;    if (list != null){        return list;}    else if (forceCreate)    {        // 获取用于更新操作的 m_tailList 锁lock (GlobalListsLock){            // 如果头列表等于空,那么说明集合中还没有元素// 直接创建一个新的if (m_headList == null){list = new ThreadLocalList(Thread.CurrentThread);m_headList = list;m_tailList = list;}            else{               // ConcurrentBag内的数据是以双向链表的形式分散存储在各个线程的本地区域中// 通过下面这个方法 可以找到那些存储有数据 但是已经被停止的线程// 然后将已停止线程的数据 移交到当前线程管理list = GetUnownedList();                // 如果没有 那么就新建一个列表 然后更新尾指针的位置if (list == null){list = new ThreadLocalList(Thread.CurrentThread);m_tailList.m_nextList = list;m_tailList = list;}}m_locals.Value = list;}}    else{        return null;}Debug.Assert(list != null);    return list;
}/// <summary>/// Adds an object to the <see cref="ConcurrentBag{T}"/>./// </summary>/// <param name="item">The object to be added to the/// <see cref="ConcurrentBag{T}"/>. The value can be a null reference/// (Nothing in Visual Basic) for reference types.</param>public void Add(T item){    // 获取该线程的本地列表, 如果此线程不存在, 则创建一个新列表 (第一次调用 add)ThreadLocalList list = GetThreadList(true);    // 实际的数据添加操作 在AddInternal中执行AddInternal(list, item);
}/// <summary>/// </summary>/// <param name="list"></param>/// <param name="item"></param>private void AddInternal(ThreadLocalList list, T item){    bool lockTaken = false;    try{        #pragma warning disable 0420Interlocked.Exchange(ref list.m_currentOp, (int)ListOperation.Add);        #pragma warning restore 0420// 同步案例:// 如果列表计数小于两个, 因为是双向链表的关系 为了避免与任何窃取线程发生冲突 必须获取锁// 如果设置了 m_needSync, 这意味着有一个线程需要冻结包 也必须获取锁if (list.Count < 2 || m_needSync){            // 将其重置为None 以避免与窃取线程的死锁list.m_currentOp = (int)ListOperation.None;            // 锁定当前对象Monitor.Enter(list, ref lockTaken);}        // 调用 ThreadLocalList.Add方法 将数据添加到双向链表中// 如果已经锁定 那么说明线程安全  可以更新Count 计数list.Add(item, lockTaken);}    finally{list.m_currentOp = (int)ListOperation.None;        if (lockTaken){Monitor.Exit(list);}}
}

从上面代码中,我们可以很清楚的知道Add()方法是如何运行的,其中的关键就是GetThreadList()方法,通过该方法可以获取当前线程的数据存储列表对象,假如不存在数据存储列表,它会自动创建或者通过GetUnownedList()方法来寻找那些被停止但是还存储有数据列表的线程,然后将数据列表返回给当前线程中,防止了内存泄漏。

在数据添加的过程中,实现了细颗粒度的lock同步锁,所以性能会很高。删除和其它操作与新增类似,本文不再赘述。

4. ConcurrentBag 如何实现迭代器模式

看完上面的代码后,我很好奇ConcurrentBag<T>是如何实现IEnumerator来实现迭代访问的,因为ConcurrentBag<T>是通过分散在不同线程中的ThreadLocalList来存储数据的,那么在实现迭代器模式时,过程会比较复杂。

后面再查看了源码之后,发现ConcurrentBag<T>为了实现迭代器模式,将分在不同线程中的数据全都存到一个List<T>集合中,然后返回了该副本的迭代器。所以每次访问迭代器,它都会新建一个List<T>的副本,这样虽然浪费了一定的存储空间,但是逻辑上更加简单了。

/// <summary>/// 本地帮助器方法释放所有本地列表锁/// </summary>private void ReleaseAllLocks(){    // 该方法用于在执行线程同步以后 释放掉所有本地锁// 通过遍历每个线程中存储的 ThreadLocalList对象 释放所占用的锁ThreadLocalList currentList = m_headList;    while (currentList != null){        if (currentList.m_lockTaken){currentList.m_lockTaken = false;Monitor.Exit(currentList);}currentList = currentList.m_nextList;}
}/// <summary>/// 从冻结状态解冻包的本地帮助器方法/// </summary>/// <param name="lockTaken">The lock taken result from the Freeze method</param>private void UnfreezeBag(bool lockTaken){    // 首先释放掉 每个线程中 本地变量的锁// 然后释放全局锁ReleaseAllLocks();m_needSync = false;    if (lockTaken){Monitor.Exit(GlobalListsLock);}
}/// <summary>/// 本地帮助器函数等待所有未同步的操作/// </summary>private void WaitAllOperations(){Contract.Assert(Monitor.IsEntered(GlobalListsLock));ThreadLocalList currentList = m_headList;    // 自旋等待 等待其它操作完成while (currentList != null){        if (currentList.m_currentOp != (int)ListOperation.None){SpinWait spinner = new SpinWait();            // 有其它线程进行操作时,会将cuurentOp 设置成 正在操作的枚举while (currentList.m_currentOp != (int)ListOperation.None){spinner.SpinOnce();}}currentList = currentList.m_nextList;}
}/// <summary>/// 本地帮助器方法获取所有本地列表锁/// </summary>private void AcquireAllLocks(){Contract.Assert(Monitor.IsEntered(GlobalListsLock));    bool lockTaken = false;ThreadLocalList currentList = m_headList;    // 遍历每个线程的ThreadLocalList 然后获取对应ThreadLocalList的锁while (currentList != null){        // 尝试/最后 bllock 以避免在获取锁和设置所采取的标志之间的线程港口try{Monitor.Enter(currentList, ref lockTaken);}        finally{            if (lockTaken){currentList.m_lockTaken = true;lockTaken = false;}}currentList = currentList.m_nextList;}
}/// <summary>/// Local helper method to freeze all bag operations, it/// 1- Acquire the global lock to prevent any other thread to freeze the bag, and also new new thread can be added/// to the dictionary/// 2- Then Acquire all local lists locks to prevent steal and synchronized operations/// 3- Wait for all un-synchronized operations to be done/// </summary>/// <param name="lockTaken">Retrieve the lock taken result for the global lock, to be passed to Unfreeze method</param>private void FreezeBag(ref bool lockTaken){Contract.Assert(!Monitor.IsEntered(GlobalListsLock));    // 全局锁定可安全地防止多线程调用计数和损坏 m_needSyncMonitor.Enter(GlobalListsLock, ref lockTaken);    // 这将强制同步任何将来的添加/执行操作m_needSync = true;    // 获取所有列表的锁AcquireAllLocks();    // 等待所有操作完成WaitAllOperations();
}/// <summary>/// 本地帮助器函数返回列表中的包项, 这主要由 CopyTo 和 ToArray 使用。/// 这不是线程安全, 应该被称为冻结/解冻袋块/// 本方法是私有的 只有使用 Freeze/UnFreeze之后才是安全的 /// </summary>/// <returns>List the contains the bag items</returns>private List<T> ToList(){Contract.Assert(Monitor.IsEntered(GlobalListsLock));    // 创建一个新的ListList<T> list = new List<T>();ThreadLocalList currentList = m_headList;    // 遍历每个线程中的ThreadLocalList 将里面的Node的数据 添加到list中while (currentList != null){Node currentNode = currentList.m_head;        while (currentNode != null){list.Add(currentNode.m_value);currentNode = currentNode.m_next;}currentList = currentList.m_nextList;}    return list;
}/// <summary>/// Returns an enumerator that iterates through the <see
/// cref="ConcurrentBag{T}"/>./// </summary>/// <returns>An enumerator for the contents of the <see
/// cref="ConcurrentBag{T}"/>.</returns>/// <remarks>/// The enumeration represents a moment-in-time snapshot of the contents/// of the bag.  It does not reflect any updates to the collection after /// <see cref="GetEnumerator"/> was called.  The enumerator is safe to use/// concurrently with reads from and writes to the bag./// </remarks>public IEnumerator<T> GetEnumerator(){    // Short path if the bag is emptyif (m_headList == null)        return new List<T>().GetEnumerator(); // empty listbool lockTaken = false;    try{        // 首先冻结整个 ConcurrentBag集合FreezeBag(ref lockTaken);        // 然后ToList 再拿到 List的 IEnumeratorreturn ToList().GetEnumerator();}    finally{UnfreezeBag(lockTaken);}
}
由上面的代码可知道,为了获取迭代器对象,总共进行了三步主要的操作。
使用FreezeBag()方法,冻结整个ConcurrentBag<T>集合。因为需要生成集合的List<T>副本,生成副本期间不能有其它线程更改损坏数据。将ConcurrrentBag<T>生成List<T>副本。因为ConcurrentBag<T>存储数据的方式比较特殊,直接实现迭代器模式困难,考虑到线程安全和逻辑,最佳的办法是生成一个副本。完成以上操作以后,就可以使用UnfreezeBag()方法解冻整个集合。

那么FreezeBag()方法是如何来冻结整个集合的呢?也是分为三步走。

首先获取全局锁,通过Monitor.Enter(GlobalListsLock, ref lockTaken);这样一条语句,这样其它线程就不能冻结集合。然后获取所有线程中ThreadLocalList的锁,通过`AcquireAllLocks()方法来遍历获取。这样其它线程就不能对它进行操作损坏数据。等待已经进入了操作流程线程结束,通过WaitAllOperations()方法来实现,该方法会遍历每一个ThreadLocalList对象的m_currentOp属性,确保全部处于None操作。

完成以上流程后,那么就是真正的冻结了整个ConcurrentBag<T>集合,要解冻的话也类似。在此不再赘述。

四、总结

下面给出一张图,描述了ConcurrentBag<T>是如何存储数据的。通过每个线程中的ThreadLocal来实现线程本地存储,每个线程中都有这样的结构,互不干扰。然后每个线程中的m_headList总是指向ConcurrentBag<T>的第一个列表,m_tailList指向最后一个列表。列表与列表之间通过m_locals 下的 m_nextList相连,构成一个单链表。

数据存储在每个线程的m_locals中,通过Node类构成一个双向链表。
PS: 要注意m_tailListm_headList并不是存储在ThreadLocal中,而是所有的线程共享一份。

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

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

相关文章

Linux里10个最危险的命令

全世界只有3.14 % 的人关注了数据与算法之美Linux命令行佷有用、很高效&#xff0c;也很有趣&#xff0c;但有时候也很危险&#xff0c;尤其是在你不确定你自己在正在做什么时候。推荐阅读Linux之父林纳斯自传《只是为了好玩》这篇文章将会向你介绍十条命令&#xff0c;但你最好…

让你立刻爱上数学的10个算术游戏

全世界只有3.14 % 的人关注了数据与算法之美死理性派的小编经常会被问到的一个问题&#xff1a;数学到底哪里有趣了&#xff0c;数学之美又在哪里&#xff1f;这篇文章精心选择了 10 个老少咸宜的算术问题&#xff0c;以定理、趣题甚至未解之谜等各种形式带领大家窥探数学世界的…

iPhone 的 Push(推送通知)功能原理浅析

第一部分&#xff1a;Push原理(以下绝大多数内容参考自、图片来自iPhone OS Reference Library)机制简介Push 的工作机制可以简单的概括为下图图中&#xff0c;Provider是指某个iPhone软件的Push服务器。 APNS 是Apple Push Notification Service&#xff08;Apple Push服务器&…

编程从业五年的十四条经验,句句朴实

全世界只有3.14 % 的人关注了数据与算法之美排列不分先后&#xff1a;1. 当性能遇到问题时&#xff0c;如果能在应用层进行计算和处理&#xff0c;那就把它从数据库层拿出来。排序和分组就是典型的例子。在应用层做性能提升总是要比在数据库层容易的多。就像对于MySQL&#xff…

.Net之Swagger基础使用

介绍Swagger 是一个规范和完整的框架&#xff0c;用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。日常可以用于后端开发人员测试接口或者前后端联调使用。从.net5开始&#xff0c;swagger已经集成到vs2019编译器中&#xff0c;可以通过勾对选项“启用OpenAPI支持”显示…

孩子不是笨,他和“最强大脑”差的是这个!

小木最近看到一则消息推送&#xff0c;说家长辅导孩子陪写作业简直就是一道“送命题”。朋友圈更是掀起了一股“提前嫁儿嫁女”的热潮。为什么孩子对数学一点兴趣也没有&#xff1f;为什么再简单的一道题&#xff0c;换个数字换个形式&#xff0c;孩子就一问三不会了&#xff1…

c++ 隐藏进程_Linux 查看进程的动态信息

前言top命令经常用来监控Linux的系统状况&#xff0c;是常用的性能分析工具&#xff0c;能够实时显示系统中各个进程的资源占用情况目录一、描述二、top命令常用字段含义三、top中的子命令四、总结五、思维导图一、描述1、top命令经常用来监控Linux的系统状况是常用的性能分析工…

.net core针对async ()=的安全处理

最近在做一个功能需要传递一个委托作为回调逻辑处理&#xff0c;但在使用中定义了async ()>来处理awaiter逻辑那就存在一个安全问题了。了解async/awaiter的朋友一定清楚async void函数带来的致命风险&#xff01;async void会阻断异常路由&#xff0c;即当前函数没有try的情…

这三道题,总有一道你是答不出来的

全世界只有3.14 % 的人关注了数据与算法之美大家好&#xff0c;我是最近过得不太舒心的卢sir。经常被小思妹提的各种奇葩数学题搞得云里雾里的。我一看数学题就想做&#xff0c;没想到这些题目都是奇葩中的奇葩&#xff0c;不是那写错就是这计算错。为了捍卫我最后的倔强&#…

前端对div连线_《前端图形学从入门到放弃》003 三维世界

从本篇起&#xff0c;我们将正式进入webgl的3D世界本篇涵盖的内容包括&#xff1a;webgl它在干啥&#xff1f;如何画一个正方体&#xff1f;如何成为一个“有深度”的正方体&#xff1f;正方体要离家出走了&#xff01;webgl它在干啥&#xff1f;首先我们需要知道webgl的世界其…

通过R,让你的数据分析更简便!

R作为一种统计分析软件&#xff0c;广泛应用于生物、医学、电商、新闻等数据相关行业&#xff0c;是目前主流数据应用软件之一。为了更好地帮助大家了解并快速入门R语言&#xff0c;现超级数学建模携手柯老师以R语言为基础&#xff0c;向大家隆重推出《R语言基础》系列课。柯老…

r语言用行名称提取数据框信息显示na_学会这些R语言技巧至少可以节省半年时间...

ubuntu备忘定期清空回收站扩增子数据牢记r ubuntu 相关技巧和备忘待解决问题1&#xff1a;phyloseq有一篇文章案例使用输入和输出文件相同的文件名&#xff0c;无法执行待解决问题2&#xff1a;待解决问题3&#xff1a;样品分组文件太长了&#xff0c;导致提取出来数据存在NA值…

hibernate.cfg.xml的一些事

Hibernate连接数据库的配置文件书写一般有两种方式&#xff1a; 第一种方式&#xff1a;使用开发环境直接连接数据库最后生成hibernate.cfg.xml文件 第二种方式&#xff1a;使用已有的模版直接拷贝的工作的目录下&#xff0c;通过相应的修改获得需要的连接数据库的配置文件&…

配置静态路由下一跳为本地出战接口和IP地址的区别

配置静态路由下一跳为本地出战接口和IP地址的区别 在配置静态路由时&#xff0c;下一跳可以使用下一路由器的IP地址&#xff0c;也可以使用本路由器的出站接口。在点对点的网络中&#xff0c;两者可能没有什么差别&#xff0c;但在以太网中或者NBMA网络类型&#xff0c;两者有很…

使用 Source Generator 自动生成 WEB API

使用 Source Generator 自动生成 WEB APIIntro上次我们介绍了使用 Source Generator 的应用&#xff0c;有小伙伴留言说想要自动生成一套 ABP 相关的东西&#xff0c;我对 ABP 不怎么熟悉&#xff0c;所以写了一个简单版的雏形&#xff0c;可以根据自定义的模板去动态生成&…

Excel中的散点图这么强大,学习了!

全世界只有3.14 % 的人关注了数据与算法之美平时见得最多的也许是柱形图了&#xff0c;但我个人最喜欢的却是散点图。在讲散点图之前&#xff0c;我先阐述一个不太严谨的个人观点。我认为&#xff0c;所有的数据图表都可以分为两类&#xff0c;一类是偏重于展示&#xff0c;一类…

mysql修改字段的顺序_Mysql中如何修改字段的排列顺序?

创建数据表的数据,字段在表中的位置已经确定了。但要修改字段在表中的排列位置,则需要使用ALTER TABLE语句来处理。在MySQL中,修改字段排列位置的基本语法格式如下: ALTER TABLE MODLFY 字段名1 数据类型 FIRST|AFTER 字段名2 在上述格式中,“字段名1”指的是修改位置的字段…

趣读:程序员泪流满面的20个瞬间!

全世界只有3.14 % 的人关注了数据与算法之美【一】老板突然说&#xff0c;想跟你聊一聊你的年终review结果【二】记一次难忘的debug经历【三】——昨晚又加班了吧&#xff1f;——你怎么知道的&#xff1f;【四】老板定下了春节期间on call的人!【五】产品crash了&#xff0c;最…

记一次 .NET 某医院HIS系统 CPU爆高分析

一&#xff1a;背景 1. 讲故事前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高&#xff0c;求助如何分析&#xff1f;和这位朋友沟通下来&#xff0c;据说这问题困扰了他们几年????&#xff0c;还请了微软的工程师过来解决&#xff0c;无疾而终&#xff0c…

mysql判断表存在的sql语句_SQL 语句判断已知表是否存在_MySQL

问:怎样用SQL语句来判断已知表是否存在?答:具体解决方法如下:注释:以下代码为通常的引用Dao做的一模块以下为引用的内容&#xff1a;Function fExistTable(strTableName As String) As IntegerDim db As DatabaseDim i As IntegerSet db DBEngine.Workspaces(0).Databases(0)…