死锁问题分析和解决——资源回收时

news2024/9/19 10:38:36

1.描述问题

在完成线程池核心功能功能时,没有遇到太大的问题(Any,Result,Semfore的设计),在做线程池资源回收时,遇到了死锁的问题        

1、在ThreadPool的资源回收,等待线程池所有线程退出时,发生死锁问题,导致进程无法退出

死锁代码:

#include "threadpool.h"

#include <thread>
#include <iostream>

const int TASK_MAX_THRESHHOLD = INT32_MAX;
const int THREAD_MAX_THRESHHOLD = 100;
const int THREAD_MAX_IDLE_TIME = 60;//单位:秒

//线程池构造
ThreadPool::ThreadPool()
	: initThreadSize_(0)
	, taskSize_(0)
	, idleThreadSize_(0)//刚开始时还没有线程
	, curThreadSize_(0)
	, taskQueMaxThreshHold_(TASK_MAX_THRESHHOLD)
	, threadSizeThreshHold_(THREAD_MAX_THRESHHOLD)
	, poolMode_(PoolMode::MODE_FIXED)
	, isPoolRunning_(false)
{}

//线程池析构
ThreadPool::~ThreadPool()
{
	isPoolRunning_ = false;
	notEmpty_.notify_all();

	//等待线程池里面所有的线程返回  有两种状态:阻塞 & 正在执行任务中
	std::unique_lock<std::mutex> lock(taskQueMtx_);
	exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}

//设置线程池的工作模式
void ThreadPool::setMode(PoolMode mode)
{
	if (checkRunningState())
		return;
	poolMode_ = mode;
}

// 设置task任务队列上限阈值
void ThreadPool::setTaskQueMaxThreshHold(int threshhold)
{
	if (checkRunningState())
		return;
	taskQueMaxThreshHold_ = threshhold;
}

//设置线程池cached模式下线程阈值
void ThreadPool::setThreadSizeThreshHold(int threshhold)
{
	if (checkRunningState())
		return;
	if (poolMode_ == PoolMode::MODE_CACHED)
	{
		threadSizeThreshHold_ = threshhold;
	}
}

// 给线程池提交任务  用户调用该接口,传入任务对象,生产任务
Result ThreadPool::submitTask(std::shared_ptr<Task> sp)
{
	//获取锁
	std::unique_lock<std::mutex> lock(taskQueMtx_);

	//线程的通信  等待任务队列有空余
	// 用户提交任务,最长不能阻塞超过1s,否则判断提交任务失败,返回
	//while (taskQue_.size() == taskQueMaxThreshHold_)
	//{
	//	notFull_.wait(lock);
	//}

	/*
	* wait:直到等待满足条件(第二个参数lamada)才返回
	* wait_for:满足条件返回真,到了约定的时间段(5s)返回假
	* wait_until:满足条件返回真,到了约定的时间点(下周一)返回假
	*/
	if (!notFull_.wait_for(lock, std::chrono::seconds(1),
		[&]()->bool {return taskQue_.size() < (size_t)taskQueMaxThreshHold_; }))//等同于上面的语句,参数:需要释放的锁  函数对象(要能满足条件变量)
		//任务队列中的任务数小于上限的阈值,否则就阻塞在这句
	{
		//表示notFull_等待1s,条件依然没有满足
		std::cerr << "task queue is full,submit task fail." << std::endl;
		//return task->getResult(); //Task  Result  线程执行完task,task对象就被析构掉了
		return Result(sp, false);//返回临时对象,应该自动匹配右值的资源转移,如果编译不通过,把C++标准调高一点
	}

	//如果有空余,把任务放入任务队列中
	taskQue_.emplace(sp);
	taskSize_++;

	//因为新放了任务,任务队列肯定不空了,在notEmpty_上进行通知,赶快分配线程执行任务
	notEmpty_.notify_all();

	//cached模式 任务处理比较紧急 场景:小而快的任务 需要根据任务数量和空闲线程的数量,判断是否需要创建新的线程出来
	if (poolMode_ == PoolMode::MODE_CACHED
		&& taskSize_ > idleThreadSize_
		&& curThreadSize_ < threadSizeThreshHold_)
	{
		std::cout << ">>> create new thread..." << std::this_thread::get_id() << " exit!" << std::endl;
		//创建新的线程对象
		auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));
		int threadId = ptr->getId();
		threads_.emplace(threadId, std::move(ptr));
		//threads_.emplace_back(std::move(ptr));
		//启动线程
		threads_[threadId]->start();
		//修改线程个数相关的变量
		curThreadSize_++;
		idleThreadSize_++;
	}

	//返回任务的Result对象
	return Result(sp);
	// return task->getResult();
}


//开启线程池
void ThreadPool::start(int initThreadSize)
{
	//设置线程池的运行状态
	isPoolRunning_=true;

	//记录初始线程个数
	initThreadSize_ = initThreadSize;
	curThreadSize_ = initThreadSize;

	//创建线程对象
	for (int i = 0; i < initThreadSize_; i++)
	{
		//创建thread线程对象的时候,把线程函数给到thread线程对象
		auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));
		int threadId = ptr->getId();
		threads_.emplace(threadId, std::move(ptr));
		//threads_.emplace_back(std::move(ptr));//unique_ptr将左值引用的拷贝构造和赋值都delete了,需要右值(进行资源转移)
	}

	//启动所有线程 std::vector<Thread*> threads_;
	for (int i = 0; i < initThreadSize_; i++)
	{
		threads_[i]->start(); //需要去执行一个线程函数
		idleThreadSize_++;//记录初始空闲线程的数量
	}
}

//定义线程函数  线程池的所有线程从任务队列里面消费任务
void ThreadPool::threadFunc(int threadid) //线程函数返回,相应的线程也就结束了
{
	/*std::cout << "begin threadFunc tid:" << std::this_thread::get_id() << std::endl;
	std::cout << "end threadFunc tid:" << std::this_thread::get_id() << std::endl;*/
	auto lastTime = std::chrono::high_resolution_clock().now();

	while (isPoolRunning_)
	{
		std::shared_ptr<Task> task;
		{
			//先获取锁
			std::unique_lock<std::mutex> lock(taskQueMtx_);

			std::cout << "tid:" << std::this_thread::get_id()
				<< "尝试获取任务..." << std::endl;

			//cached模式下,有可能已经创建了很多的线程,但是空闲时间超过60s,应该把多余的线程结束回收掉(超过initThreadSize_数量的线程要进行回收)
			//当前时间-上一次线程执行的时间>60s

			//每一秒中返回一次  怎么区分:超时返回?还是有任务待执行返回
			while (taskQue_.size() == 0)
			{
				if (poolMode_ == PoolMode::MODE_CACHED)
				{
					//条件变量,超时返回了
					if (std::cv_status::timeout == notEmpty_.wait_for(lock, std::chrono::seconds(1)))
					{
						auto now = std::chrono::high_resolution_clock().now();
						auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);
						if (dur.count() >= THREAD_MAX_IDLE_TIME && curThreadSize_ > initThreadSize_)
						{
							//开始回收当前线程
							//记录线程数量的相关变量的值修改
							//把线程对象从线程列表容器中删除  没有办法  threadFunc  <=>thread对象
							//threadid=>thread对象=》删除
							threads_.erase(threadid);// 这个id不是std::this_thread::getid()  是自己生成的,我们自定义的
							curThreadSize_--;
							idleThreadSize_--;

							std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
							return;
						}
					}
				}
				else
				{
					//等待notEmpty条件
					notEmpty_.wait(lock);
				}

				//线程池结束,回收线程资源
				if (!isPoolRunning_)
				{
					threads_.erase(threadid);// 这个id不是std::this_thread::getid()  是自己生成的,我们自定义的
					std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
					exitCond_.notify_all();
					return;
				}
			}

			idleThreadSize_--;//唤醒线程工作,空闲线程-1

			std::cout << "tid:" << std::this_thread::get_id()
				<< "获取任务成功..." << std::endl;

			//从任务队列中取一个任务出来
			task = taskQue_.front();
			taskQue_.pop();
			taskSize_--;

			//如果依然有剩余任务,继续通知其它的线程执任务
			if (taskQue_.size() > 0)
			{
				notEmpty_.notify_all();
			}

			//取出一个任务,进行通知,通知可以继续提交生产任务
			notFull_.notify_all();
		}//就应该把锁释放掉

		//当前线程负责执行这个任务
		if (task != nullptr)
		{
			//task->run();//执行任务;把任务的返回值setVal方法给到Result
			task->exec();
		}

		idleThreadSize_++;//线程执行完任务,空闲线程+1
		lastTime = std::chrono::high_resolution_clock().now();//更新线程执行完任务的时间
	}
	threads_.erase(threadid);// 这个id不是std::this_thread::getid()  是自己生成的,我们自定义的
	std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;
	exitCond_.notify_all();
}

bool ThreadPool::checkRunningState() const
{
	return isPoolRunning_;
}

///   线程方法实现
int Thread::generateId_ = 0;

//线程构造
Thread::Thread(ThreadFunc func)
	:func_(func)
	,threadId_(generateId_++)
{}

//线程析构
Thread::~Thread(){}

//启动线程
void Thread::start()
{
	//创建一个线程来执行一个线程函数
	std::thread t(func_, threadId_);//C++11来说 线程对象t  和线程函数func_
	t.detach();//设置分离线程,线程对象t出作用域会析构,但是线程函数不能结束否则程序会挂掉,所以要将线程分离出去,做到二者互不影响
				//pthread_detach  pthread_t设置成分离线程
				//主线程要用pthread_join回收线程,防止孤儿线程的出现

}	//获取线程id
int Thread::getId()const
{
	return threadId_;
}

///   Task方法实现
Task::Task()
	:result_(nullptr)
{}

void Task::exec()
{
	result_->setVal(run());//这里发生多态调用
}

void Task::setResult(Result* res)
{
	result_ = res;
}

///   Result方法的实现
Result::Result(std::shared_ptr<Task> task, bool isValid)
	:isValid_(isValid)
	,task_(task)
{
	task_->setResult(this);
}

Any Result::get() // 用户调用的
{
	if (!isValid_)
	{
		return "";
	}

	//task任务如果没有执行完,这里会阻塞用户的线程
	sem_.wait();//用户调用get时,如果任务在线程池中,还没有被执行完,那么调用get方法的线程就会阻塞住
	return std::move(any_);//右值引用
}

void Result::setVal(Any any)//谁调用的呢??
{
	//存储task的返回值
	this->any_ = std::move(any);
	sem_.post();//已经获取的任务的返回值,增加信号量资源
}

 

我们的资源回收代码如下:

//线程池析构
ThreadPool::~ThreadPool()
{
	isPoolRunning_ = false;
	
	notEmpty_.notify_all();
	//等待线程池里面所有的线程返回  有两种状态:阻塞 & 正在执行任务中
	std::unique_lock<std::mutex> lock(taskQueMtx_);
	exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}

现在,有的线程没有被回收,线程队列中还有线程,所以就一直阻塞等待了。
线程池的那个线程为什么没有被回收掉?
(时而出现,时而不出现的问题)

我们通过在windows上调试:

我们通过在Linux上进行gdb调试

主要通过gdb attach到正在运行的进程,通过info threads,thread tid,bt等命令查看各个线程的调用堆栈信息,结合项目代码,定位到发生死锁的代码片段,分析死锁问题发生的原因

2.分析问题

原先针对上面的2种情况的处理方法如下:

第3种情况:
有的线程执行完任务,又进入while循环了

在这里有2种情况:
1、pool线程先获取到锁,线程池的线程获取不到锁,阻塞。
此时pool线程看wait条件,size>0,不满足条件,就进入等待wait状态了,并且把互斥锁mutex释放掉。
线程池的线程就获取到锁了,发现任务队列没有任务了,这个任务就在notEmpty条件变量上wait,但是此时pool线程没有办法再对这个条件变量notify了。
发生死锁了!!!

2、线程池里的线程先获取到锁,发生任务队列为空,在条件变量notEmpty上wait了,释放锁,然后pool线程抢到锁,只是看exitCond条件变量的wait条件,看size还是大于0,还是死锁了。

解决方法:pool线程获取到锁后再notify

//线程池析构
ThreadPool::~ThreadPool()
{
	isPoolRunning_ = false;
	
	//等待线程池里面所有的线程返回  有两种状态:阻塞 & 正在执行任务中
	std::unique_lock<std::mutex> lock(taskQueMtx_);
	notEmpty_.notify_all();
	exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}

 我们在消费者线程进行锁+双重判断:

//定义线程函数   线程池的所有线程从任务队列里面消费任务
void ThreadPool::threadFunc(int threadid)//线程函数返回,相应的线程也就结束了
{
	auto lastTime = std::chrono::high_resolution_clock().now();

	//所有任务必须执行完成,线程池才可以回收所有线程资源
	for (;;)
	{
		std::shared_ptr<Task> task;
		{
			//先获取锁,我们要注意控制锁的范围,取完任务,就释放锁
			std::unique_lock<std::mutex> lock(taskQueMtx_);

			std::cout << "tid:" << std::this_thread::get_id()
				<< "尝试获取任务..." << std::endl;

			//cached模式下,有可能已经创建了很多的线程,但是空闲时间超过60s,应该把多余的线程
			//结束回收掉(超过initThreadSize_数量的线程要进行回收)
			//当前时间 - 上一次线程执行的时间 > 60s
			
			//每一秒中返回一次   怎么区分:超时返回?还是有任务待执行返回
			//锁 + 双重判断
			while (taskQue_.size() == 0)
			{
				//线程池要结束,回收线程资源
				if (!isPoolRunning_)
				{
					threads_.erase(threadid);//std::this_thread::getid()
					std::cout << "threadid:" << std::this_thread::get_id() << " exit!"
						<< std::endl;
					exitCond_.notify_all();
					return;//线程函数结束,线程结束
				}

				if (poolMode_ == PoolMode::MODE_CACHED)
				{
					//条件变量,超时返回了
					if (std::cv_status::timeout ==
						notEmpty_.wait_for(lock, std::chrono::seconds(1)))
					{
						auto now = std::chrono::high_resolution_clock().now();
						auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);
						if (dur.count() >= THREAD_MAX_IDLE_TIME
							&& curThreadSize_ > initThreadSize_)//任务数量大于空闲线程数量
						{
							//开始回收当前线程
							//记录线程数量的相关变量的值修改
							//把线程对象从线程列表容器中删除   没有办法 threadFunc《=》thread对象
							//通过threadid => thread对象 => 删除
							threads_.erase(threadid);//std::this_thread::getid()
							curThreadSize_--;
							idleThreadSize_--;

							std::cout << "threadid:" << std::this_thread::get_id() << " exit!"
								<< std::endl;
							return;
						}
					}
				}
				else
				{
					//等待notEmpty条件
					notEmpty_.wait(lock);
				}

				//if (!isPoolRunning_)
				//{
				//	threads_.erase(threadid);//std::this_thread::getid()
				//	std::cout << "threadid:" << std::this_thread::get_id() << " exit!"
				//		<< std::endl;
				//	exitCond_.notify_all();
				//	return;//结束线程函数,就是结束当前线程了!
				//}
			}

			idleThreadSize_--;

			std::cout << "tid:" << std::this_thread::get_id()
				<< "获取任务成功..." << std::endl;

			//从任务队列种取一个任务出来
			task = taskQue_.front();
			taskQue_.pop();
			taskSize_--;

			//如果依然有剩余任务,继续通知其它得线程执行任务
			if (taskQue_.size() > 0)
			{
				notEmpty_.notify_all();
			}

			//取出一个任务,进行通知,通知可以继续提交生产任务
			notFull_.notify_all();
		} //就应该把锁释放掉
		
		//当前线程负责执行这个任务
		if (task != nullptr)
		{
			//task->run();//执行任务;把任务的返回值setVal方法给到Result,基类指针调用派生类对象的同名覆盖方法
			task->exec();//用户还是使用run方法
		}
		
		idleThreadSize_++;
		lastTime = std::chrono::high_resolution_clock().now();//更新线程执行完任务的时间
	}
}

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

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

相关文章

2024年【浙江省安全员-C证】复审模拟考试及浙江省安全员-C证作业模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 浙江省安全员-C证复审模拟考试参考答案及浙江省安全员-C证考试试题解析是安全生产模拟考试一点通题库老师及浙江省安全员-C证操作证已考过的学员汇总&#xff0c;相对有效帮助浙江省安全员-C证作业模拟考试学员顺利通…

C++进阶(14)类型转换、IO流

文章目录 一、类型转换C语言隐式类型转换强制类型转换 C类型转换的情况类型转换的函数&#xff08;4个&#xff09; 二、IO流1、缓冲区2、提高输入输出效率3、文件操作文件权限写操作 --- ofstream文本方式写入二进制方式写入 读操作 --- ifstream文本方式读取二进制方式读取 其…

如何使用DEV-C++做游戏?

我的B站视频做过关于python的小游戏开发&#xff0c;但很多小伙伴希望通过C做一些有趣的动画或游戏&#xff0c;该怎么实现呢&#xff1f; B站&#xff1a;bigbigli_大李 大家好&#xff0c;我是大李。 今天主要跟大家谈谈如何通过C做一些小游戏开发&#xff0c;这里我们就 使用…

JavaScript - Api学习 Day02(事件监听、流、委托)

事件监听 一、事件监听1.1 什么是事件、事件监听1.1.1 事件监听的基本流程1.1.2 重点关注以下三个核心要素 1.2 Event handling models 事件处理模型1.2.1 DOM Level 版本&#xff08;1&#xff09;DOM Level 0&#xff08;2&#xff09;DOM Level 2 1.2.2 事件类型&#xff08…

iPad协议08算法

微信协议就是基于微信IPad协议的智能控制系统&#xff0c;利用人工智能AI技术、云计算技术、虚拟技术、边缘计算技术、大数据技术&#xff0c;打造出智能桌面系统RDS、 智能聊天系统ACS 、智能插 件系统PLUGIN 、云计算服务CCS 、任务管理系统TM、设备管理服务DM、 应用管理系统…

ECCV 2024亮点:APGCC技术刷新人群计数与定位的SOTA

摘要 在ECCV 2024会议上&#xff0c;一项名为APGCC的新技术以其卓越的性能引起了广泛关注。这项技术通过创新的方法&#xff0c;显著提高了人群计数和定位的准确性和鲁棒性&#xff0c;为监控、事件管理和城市规划等领域带来了新的解决方案。 正文&#xff1a; 随着城市化进…

ant design pro 技巧之实现列表页多标签

ant design pro 如何去保存颜色ant design pro v6 如何做好角色管理ant design 的 tree 如何作为角色中的权限选择之一ant design 的 tree 如何作为角色中的权限选择之二ant design pro access.ts 是如何控制多角色的权限的ant design pro 中用户的表单如何控制多个角色ant des…

全国大学生数学建模比赛——时间序列(详细解读)

全国大学生数学建模比赛中&#xff0c;时间序列分析是一种重要的方法。以下是对时间序列在该比赛中的详细解读&#xff1a; 一、时间序列的概念 时间序列是按时间顺序排列的一组数据。在数学建模中&#xff0c;时间序列数据通常反映了某个现象随时间的变化情况。例如&#xf…

编程中数据字典介绍

目录 第一章、快速了解数据字典1.1&#xff09;数据字典介绍1.2&#xff09;主动数据字典1.2.1&#xff09;主动数据字典对表字段的描述1.2.2&#xff09;主动数据字典对表索引的描述1.2.3&#xff09;主动数据字典对表外键的描述1.3&#xff09;被动数据字典1.4&#xff09;数…

golang实现windows获取加密盘符的总大小

golang实现windows获取加密盘符的总大小 package mainimport ("fmt""syscall""unsafe" )type PartitionStyle uint32const (IOCTL_DISK_GET_DRIVE_LAYOUT_EX 0x00070050FILE_DEVICE_MASS_STORAGE uint32 0x0000002dIOCTL_STOR…

【生物特征识别论文分享】基于深度学习的掌纹掌静脉识别

&#xff08;待更新&#xff09;基于深度学习的生物特征识别&#xff08;手掌静脉、手背静脉、手指静脉、掌纹、人脸等&#xff09;论文模型总结 。具体方法包括&#xff1a;基于特征表征、基于传统网络设计与优化、基于轻量级网络设计与优化、基于Transformer设计与优化、基于…

Isaac Sim仿真平台学习(2)基础知识

目录 0.前言 1.isaac sim的组建 1.Isaac Lab的资料 2.PhysX 3.RTX 4.Digital Twins 5.Replicator 6.Omnigraph 0.前言 难得更新阿&#xff0c;今天黑猴发布了没有去玩&#xff0c;跑来更新博客&#xff0c;本来想着按宇树的go2开发指南去试试RL的&#xff0c;但可惜没成…

39_WAF的概念、功能,ModSecurity部署配置、LAMP环境部署、Ubuntu搭建DVWA靶机测试、测试WAF防御、OWASP规则集的部署

一、WAF的概念 WAF&#xff08; Web Application Firewall &#xff09;&#xff0c;即Web应用防火墙 通过执行一系列HTTP/HTTPS&#xff08;应用层的协议&#xff09;的安全策略为Web应用提供保护的一种网络安全产品。增加攻击者的难度和成本&#xff0c;但不是100%安全。工…

XRAY~漏洞扫描工具

有人说&#xff1a;“所有的漏扫工具都是人写出来的&#xff0c;既然是工具&#xff0c;肯定没有人厉害&#xff0c;但是&#xff0c;漏扫可以大大减少你的工作量” 4、⭐ XRAY xray 是一款功能强大的安全评估工具&#xff0c;由多名经验丰富的一线安全从业者呕心打造而成&…

五、2 移位操作符赋值操作符

1、移位操作符 2、赋值操作符 “ ”赋值&#xff0c;“ ”判断是否相等 1&#xff09;连续赋值 2&#xff09;复合赋值符

关于栈(顺序栈)的知识讲解

1.1 什么是栈 栈是只能在一端进行插入和删除操作的线性表(又称为堆栈)&#xff0c;进行插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。 特点&#xff1a;栈是先进后出FILO(First In Last Out) (LIFO(Last In First Out)) 1.2 顺序栈 1.2.1 特性 逻辑结构&#xff1…

推荐4款高效的视频剪辑神器。

很多人一开始&#xff0c;面对各种视频剪辑软件时会不知所措。不知道该选择哪一个&#xff0c;也知道如何使用。但是这几个软件&#xff0c;对于不太熟悉视频剪辑的朋友们来说简直就是神器&#xff0c;简单易上手&#xff0c;功能做的也非常的专业。 1、福昕剪辑软件 直达链接…

高性能 Web 服务器:让网页瞬间绽放的魔法引擎(下)

目录 一.Nginx 反向代理功能 1.缓存功能 2.http 反向代理负载均衡 二.实现 Nginx 四层负载均衡 三.实现 FastCGI 1.为什么会有FastCGI&#xff1f; 2.什么是PHP-FPM&#xff1f; 3.FastCGI配置指令 4.Nginx与php-fpm在同一服务器 5.Nginx配置转发 6. php的动态扩展模…

关于c++多线程中的互斥锁mutex

关于c多线程中的互斥锁mutex c中的多线程线程的基本概念C 标准库中的线程支持 多线程与主线程与join换一种方式理解线程互斥锁第一种第二种 子线程与互斥锁混合锁--看这个应该就明白了&#xff08;这个主要使用的是嵌套锁&#xff09;定义一个类创建线程 这个示例主要使用并列锁…

SAP负库存

业务示例 在系统中&#xff0c;对于一些物料而言&#xff0c;不能立即将收到的交货输入为收货。如果要使发货无论如何都是可以过帐的&#xff0c;则需要允许这些物料的负库存。 负库存 发货数量大于预订数量时&#xff0c;过帐该发货就会出现负库存。如果由于组织原因&#…