Task 使用详细[基础操作,异步原则,异步函数,异步模式]


线程是创建并发的底层工具,对于开发者而言,想实现细粒度并发具有一定的局限性,比如将小的并发组合成大的并发,还有性能方面的影响。

Task可以很好的解决这些问题,Task是一个更高级的抽象概念,代表一个并发操作,但不一定依赖线程完成。

Task从Framework4.0开始引入,Framework4.5又添加了一些功能,比如Task.Run(),async/await关键字等,

在.NET Framework4.5之后,基于任务的异步处理已经成为主流模式, (Task-based Asynchronous Pattern,TAP)基于任务的异步模式。

在使用异步函数之前,先看下Task的基本操作。

一. Task 基本操作

1.1 Task 启动方式

Task.Run(()=>Console.WriteLine("Hello Task"));
Task.Factory.StartNew(()=>Console.WriteLine("Hello Task"));

Task.Run是Task.Factory.StartNew的快捷方式。

启动的都是后台线程,并且默认都是线程池的线程

Task.Run(() =>
{
    Console.WriteLine(
        $"TaskRun IsBackGround:{CurrentThread.IsBackground}, IsThreadPool:{CurrentThread.IsThreadPoolThread}");
});

Task.Factory.StartNew(() =>
{
    Console.WriteLine(
        $"TaskFactoryStartNew IsBackGround:{CurrentThread.IsBackground}, IsThreadPool:{CurrentThread.IsThreadPoolThread}");
});

如果Task是长任务,可以添加TaskCreationOptions.LongRunning参数,使任务不运行在线程池上,有利于提升性能。

Task.Factory.StartNew(() =>
{
    Console.WriteLine(
        $"TaskFactoryStartNew IsBackGround:{CurrentThread.IsBackground}, IsThreadPool:{CurrentThread.IsThreadPoolThread}");
}, TaskCreationOptions.LongRunning);

 1.2 Task 返回值/带参数

Task 有一个泛型子类Task<TResult>,允许返回一个值。

Task<string> task =Task.Run(()=>SayHello("Jack"));

string SayHello(string name)
{
    return "Hello " + name;
}

Console.WriteLine(task.Result);

通过任务的Result属性获取返回值,这是会堵塞线程,尤其是在桌面客户端程序中,谨慎使用Task.Result,容易导致死锁!

同时带参数的方式也不是很合理,后面可以被async/await方式直接替代。

1.3 Task 异常/异常处理

当任务中的代码抛出一个未处理异常时,调用任务的Wait()或者Result属性时,异常会被重新抛出。

var task = Task.Run(ThrowError);
try
{
    task.Wait();
}
catch(AggregateException ex)
{
    Console.WriteLine(ex.InnerException is NullReferenceException ? "Null Error!" : "Other Error");
}


void ThrowError()
{
    throw new NullReferenceException();
}

对于自治任务(没有wait()和Result或者是延续的任务),使用静态事件TaskScheduler.UnobservedTaskException可以在全局范围订阅未观测的异常。

以便记录错误日志

1.4 Task 延续

延续通常由一个回调方法实现,该方法会在任务完成之后执行,延续方法有两种

(1)调用任务的GetAwaiter方法,将返回一个awaiter对象。这个对象的OnCompleted方法告知任务当执行完毕或者出错时调用一个委托。

Task<string> learnTask = Task.Run(Learn);
var awaiter = learnTask.GetAwaiter();
awaiter.OnCompleted(() =>
{
    var result = awaiter.GetResult();
    Console.WriteLine(result);
});

string Learn()
{
    Console.WriteLine("Learn Method Executing");
    Thread.Sleep(1000);
    return "Learn End";
}

如果learnTask任务出现错误,延续代码awaiter.GetResult()将重新抛出异常,其中GetResult可以直接得到原始的异常,如果使用Result属性,只能解析AggergateException.

这种延续方法更适用于富客户端程序,延续可以提交到同步上下文,延续回到UI线程中。

当编写库文件,可以使用ConfigureAwait方法,延续代码会运行在任务运行的线程上,从而避免不必要的切换开销。

var awaiter =learnTask.ConfigureAwait(false).GetAwaiter(); 

(2)另一种方法使用ContiuneWith

Task<string> learnTask = Task.Run(Learn);
learnTask.ContinueWith(antecedent =>
{
    var result = learnTask.Result;
    Console.WriteLine(result);
});

string Learn()
{
    Console.WriteLine("Learn Method Executing");
    Thread.Sleep(1000);
    return "Learn End";
}

当任务出现错误时,必须处理AggregateException, ContiuneWith更适合并行编程场景。

1.5 TaskCompletionSource类使用

从如下源码中可以看出当实例化TaskCompletionSource时,构造函数会新建一个Task任务。

public class TaskCompletionSource
{
  private readonly Task _task;
  
  /// <summary>Creates a <see cref="TaskCompletionSource"/>.</summary>
  public TaskCompletionSource() => _task = new Task();
  
  /// <summary>
        /// Gets the <see cref="Tasks.Task"/> created
        /// by this <see cref="TaskCompletionSource"/>.
        /// </summary>
        /// <remarks>
        /// This property enables a consumer access to the <see cref="Task"/> that is controlled by this instance.
        /// The <see cref="SetResult"/>, <see cref="SetException(Exception)"/>, <see cref="SetException(IEnumerable{Exception})"/>,
        /// and <see cref="SetCanceled"/> methods (and their "Try" variants) on this instance all result in the relevant state
        /// transitions on this underlying Task.
        /// </remarks>
  public Task Task => _task;
}

它的真正的作用是创建一个不绑定线程的任务。

eg: 可以使用Timer类,CLR在定时之后触发一个事件,而无需使用线程。

实现通用Delay方法:

Delay(5000).GetAwaiter().OnCompleted(()=>{ Console.WriteLine("Delay End"); });

Task Delay(int millisecond)
{
    var tcs = new TaskCompletionSource<object>();
    var timer = new System.Timers.Timer(millisecond) { AutoReset = false };
    timer.Elapsed += delegate
    {
        timer.Dispose();
        tcs.SetResult(null);
    };
    timer.Start();
    return tcs.Task;
}

这个方法类似Task.Delay()方法。

二. 异步原则(补充)

同步操作:先完成其工作再返回调用者

异步操作:大部分工作则是在返回调用者之后才完成的,也称非阻塞方法。

异步编程的原则:

(1)以异步的方式编写运行时间很长(或者可能很长)的函数,会在一个新的线程或者任务上调用这些函数,从而实现需要的并发性。

(2)异步方法的并发性是在长时间运行的方法内启动的,而不是从这个方法外启动的。

  • I/O密集的并发性的实现不需要绑定线程(如1.5节的例子所示),因此可以提高可伸缩性和效率。
  • 富客户端应用程序可以减少工作线程的代码,因此可以简化线程安全性的实现。

Task支持延续,因此非常适合进行异步编程的,如1.5节的Delay方法。

在计算密集的方法中,我们使用Task.Run创建线程相关的异步性。但是异步编程的不同点在于,更希望将异步放在底层调用图上,

因此富客户端应用程序的高层方法就可以一直在UI线程上运行,访问控件、共享状态而不用担心会出现线程安全问题。

看Task.Run的例子:

//粗粒度并发
Task.Run(() => DisplayPrimeCounts());

/// <summary>
/// 显示素数个数
/// </summary>
void DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
        Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
                          " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));

    Console.WriteLine("Done!");
}

/// <summary>
/// 获取素数个数
/// </summary>
int GetPrimesCount(int start, int count)
{
    return
        ParallelEnumerable.Range(start, count).Count(n =>
            Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
}

这是一种粗粒度并发,如果想实现细粒度并发,需要编写异步的方法。

看异步版本:

DisplayPrimeCountsAsync();

Task DisplayPrimeCountsAsync()
{
    var machine = new PrimesStateMachine();
    machine.DisplayPrimeCountsFrom(0);
    return machine.Task;
}

class PrimesStateMachine        
{
    TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
    public Task Task { get { return _tcs.Task; } }

    /// <summary>
    /// 异步显示素数个数
    /// </summary>
    /// <param name="i"></param>
    public void DisplayPrimeCountsFrom(int i)
    {
        var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            Console.WriteLine(awaiter.GetResult()+" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
            if (i++ < 10) DisplayPrimeCountsFrom(i);
            else { Console.WriteLine("Done"); _tcs.SetResult(null); }
        });
    }

    /// <summary>
    /// 异步获取素数个数
    /// </summary>
    /// <param name="start"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() =>
            ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
}

可以看到改造异步后的实现方式,很复杂。 GetPrimesCountAsync改为方法内部启动异步,DisplayPrimeCountsFrom通过TaskCompletionSource实现异步。

这时async和await登场!

async和await关键字极大的简化了程序的复杂度。

async/await版本:

DisplayPrimeCountsAsync();

/// <summary>
/// 异步显示素数个数
/// </summary>
async Task DisplayPrimeCountsAsync()
{
    for (int i = 0; i < 10; i++)
        Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) +
                          " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));

    Console.WriteLine("Done!");
}

/// <summary>
/// 异步获取素数个数
/// </summary>
Task<int> GetPrimesCountAsync(int start, int count)
{
    return Task.Run(() =>
        ParallelEnumerable.Range(start, count).Count(n =>
            Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
}

从编程形式上,有点类似同步方法一样直观简洁。其实async/await编译器也是将其转换为一个状态机。通常我们称之为C#语法糖。

 编译器背后的原理可以参考这篇文章:https://www.cnblogs.com/zh7791/p/9951478.html

三. 异步函数

这章开始进入异步函数的使用,由上面一章已经引出async/await关键字。可以使用同步的代码风格编写异步代码,极大地降低了异步编程的复杂度。

简单捋下async/await

如下语句中使用了await附加了延续,statement(s)是expression的延续。

这个“等待”被编译器转化为如下同等功能的代码。

 

 这是如果想要成功编译就必须添加async修饰符,如下图提示。

async修饰符会指示编译器将await作为一个关键字而非标识符,来避免二义性(C#5之前有可能作为标识符使用),添加async修饰符的方法称为异步函数。

3.1 富客户端异步函数Demo

通过WPF的例子展示异步函数在富客户端应用程序中的作用:在执行计算密集的方法时,仍然保持UI的响应,不堵塞UI线程。

 先看同步调用的情况:

private void ExecuteTaskOnClick(object sender, RoutedEventArgs e)
{
    TextBoxMessage.Text = "Call Worker" + Environment.NewLine;
    DoSomething();//同步调用
}

private void DoSomething()
{
    Thread.Sleep(3000);//模拟计算密集耗时
    TextBoxMessage.Text += "Calculate Done" + Environment.NewLine;
}

上图可以清楚的看到,当使用同步调用耗时方法时,UI线程无法响应用户事件请求,TextBox的信息显示也是等耗时方法结束后才更新。

原因是在耗时方法执行期间,UI线程已经被阻塞,UI线程接收的处理请求都会进入请求队列,无法及时响应(包括鼠标键盘的事件请求,控件更新),很影响用户体验。

下面看异步版本:

btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();

private async void ExecuteTaskAsync()
{
    btnExecuteTaskAsync.IsEnabled = false;
    TextBoxMessage.Text = "Call Worker Async" + Environment.NewLine;
    await DoSomethingAsync();//异步调用
    TextBoxMessage.Text += "Calculate Async Done" + Environment.NewLine;
    btnExecuteTaskAsync.IsEnabled = true;
}

private async Task DoSomethingAsync()
{
    await Task.Run(() =>
    {
        Thread.Sleep(3000); //模拟计算密集耗时
    });
}

更改为异步版本后,在执行耗时任务时,UI线程没有被堵塞,可以正常响应用户事件和控件更新,提高了用户体验。

3.2 异步调用执行过程

根据3.1节的例子,整个调用过程如下:

当用户点击按钮时触发事件,事件调用ExecuteTaskAsync 方法,ExecuteTaskAsync 方法调用DoSomethingAsync方法,而后调用await,而await会使执行点返回给调用者,

当DoSomethingAsync方法完成(或者出现错误)时,执行点会从停止之处恢复执行DoSomethingAsync后面的代码。

ExecuteTaskAsync 方法则会'租用'UI线程的时间,即ExecuteTaskAsync 方法在消息循环1中是以伪并发的方式执行的(执行会在UI线程的其他事件处理中穿插进行)。

在整个伪并发的过程中,只有await的过程中才会进行抢占,这就简化了线程的安全性。DoSomethingAsync会运行在工作线程上,正真的并发发生在DoSomethingAsync方法的Task.Run部分,在Task.Run部分尽量避免访问共享状态和UI组件。

本小节结尾完善一下上面的例子代码:

btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();

private async void ExecuteTaskAsync()
{
    try
    {
        btnExecuteTaskAsync.IsEnabled = false;
        TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;
        TextBoxMessage.Text += await DoSomethingAsync(); //异步调用
        btnExecuteTaskAsync.IsEnabled = true;
    }
    catch (Exception e)
    {
        TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;
    }
    finally
    {
        btnExecuteTaskAsync.IsEnabled = true;
    }
}

private async Task<string> DoSomethingAsync()
{
    await Task.Delay(3000); //模拟计算密集耗时
    return "Calculate Async Done";
} 

 增加了ExecuteTaskAsync方法的异常处理,给DoSomethingAsync方法添加了返回值Task<TResult>

还有一些关于优化方面的内容,简单提一下:

同步完成:执行过程在await之前就返回给调用者,同时这个方法会返回一个已经结束的任务。编译器会在同步完成的情况下跳过延续代码,会awaiter的IsCompleted属性来实现这种优化。

避免大量回弹: 对于一个在循环中多次调用的异步方法,通过调用ConfigureAwait方法可以避免该方法重复回弹到UI消息循环中。

它会阻止任务将延续提交到同步上下文中,将开销降低到了上下文切换的级别,该优化比较适合编写程序库。

四. 异步模式

 4.1 取消操作

在并发操作启动之后,需要能够取消任务,看如下示例:

private CancellationTokenSource? cts;
btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();
btnCancel.Click += (sender, args) => ExecuteCancelTask();

private async void ExecuteTaskAsync()
{
    cts = new CancellationTokenSource();
    try
    {
        btnExecuteTaskAsync.IsEnabled = false;
        TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;
        TextBoxMessage.Text += await DoSomethingAsync(cts.Token); //异步调用
        btnExecuteTaskAsync.IsEnabled = true;
    }
    catch (OperationCanceledException)
    {
        TextBoxMessage.Text += "任务已经取消!" + Environment.NewLine;
    }
    catch (Exception e)
    {
        TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;
    }
    finally
    {
        btnExecuteTaskAsync.IsEnabled = true;
    }
}

private async Task<string> DoSomethingAsync(CancellationToken cancellationToken)
{
    for (int i = 0; i < 3; i++)
    {
        await Task.Delay(1000); //模拟计算密集耗时
        cancellationToken.ThrowIfCancellationRequested();
    }
    return "Calculate Async Done";
}

private void ExecuteCancelTask()
{
    cts?.Cancel();
}

在第3章结尾示例的基础上,添加了异步函数可取消功能。

通过实例化CancellationTokenSource类,可以得到取消令牌Token,当取消令牌调用Cancel()方法时,就会将IsCancellationRequested属性设置为True,同时任务会抛出OperationCanceledException。

在设计上将检查方法取消操作和启动取消操作分离开来,具有一定的安全性。

检查取消在CancellationTaken类上,取消动作在CancellationTokenSource类上。

看实际效果:

4.2 进度报告

 一些异步操作需要在运行时报告其执行进度。一种简单的方案时向异步方法传入一个Action委托,在进度发生变化时就触发方法,在上面例子上添加了进度报告,如下:

private async void ExecuteTaskAsync()
{
    cts = new CancellationTokenSource();
    try
    {
        btnExecuteTaskAsync.IsEnabled = false;
        TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;
        var result = await DoSomethingAsync(
            (percent) => { TextBoxMessage.Text += "Current progress is " + percent + Environment.NewLine; },
            cts.Token); //异步调用
        TextBoxMessage.Text += result;
        btnExecuteTaskAsync.IsEnabled = true;
    }
    catch (OperationCanceledException)
    {
        TextBoxMessage.Text += "任务已经取消!" + Environment.NewLine;
    }
    catch (Exception e)
    {
        TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;
    }
    finally
    {
        btnExecuteTaskAsync.IsEnabled = true;
    }
}

private async Task<string> DoSomethingAsync(Action<string> progressReport, CancellationToken cancellationToken)
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(500); //模拟计算密集耗时
        progressReport($"{i * 10}%".ToString());
        cancellationToken.ThrowIfCancellationRequested();
    }
    return "Calculate Async Done";
}

实现是简单,但是在富客户端应用程序中,有潜在的线程安全问题,由并发性对外暴露所产生的风险。

CLR拥有一对专门针对进度报告的类型:IProgress<T>接口和Progress<T>类 ,它们的作用包装一个委托,以便是UI应用程序可以通过同步上下文安全地报告进度。

private async void ExecuteTaskAsync()
{
    cts = new CancellationTokenSource();
    try
    {
        btnExecuteTaskAsync.IsEnabled = false;
        TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;
        //通过Progress<T>构造函数接受一个Action<T>委托并对其进行包装
        var result = await DoSomethingAsync(new Progress<string>((percent) =>
            {
                TextBoxMessage.Text += "Current progress is " + percent + Environment.NewLine;
            })
            , cts.Token); //异步调用
        TextBoxMessage.Text += result;
        btnExecuteTaskAsync.IsEnabled = true;
    }
    catch (OperationCanceledException)
    {
        TextBoxMessage.Text += "任务已经取消!" + Environment.NewLine;
    }
    catch (Exception e)
    {
        TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;
    }
    finally
    {
        btnExecuteTaskAsync.IsEnabled = true;
    }
}

private async Task<string> DoSomethingAsync(IProgress<string> progressReport, CancellationToken cancellationToken)
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(500); //模拟计算密集耗时
        progressReport.Report($"{i * 10}%".ToString());
        cancellationToken.ThrowIfCancellationRequested();
    }
    return "Calculate Async Done";
}

对上面的例子稍作改造,就实现使用IProgress<T>和Progress<T>来完成进度报告。

4.3  基于任务的异步模式TAP

一个TAP方法:

  • 返回一个“热”Task或者Task<TResult>
  • 拥有Async后缀,除一些特殊情况或者是任务组合器
  • 若支持取消和进度报告,则需要拥有接受CancellationTaken或者IProgress<T>的重载。
  • 快速返回调用者
  • 对于I/O密集型任务不绑定线程

 

本文主要参考书籍: C#7.0核心技术指南

 

注1:UI线程上的消息循环的伪代码如下:

 

文章来源:https://www.cnblogs.com/YourDirection/p/15765209.html

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:iNeuOS工业互联网操作系统,增加DTU与平台实时交互的应用场景
下一篇:C# ASP.NET Core开发学生信息管理系统(一)
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

Task 使用详细[基础操作异步原则异步函数异步模式]
.net异步Task转同步
vue中异步函数async和await的用法
ABP VNext框架基础知识介绍(1)--框架基础类继承关系
HttpContext.Current:异步模式下的疑似陷阱之源
C# 从做早餐看同步异步
Linq to SQL语句基础讲解
Javascript 中通过 yield 和 promise 使异步变同步
重新认识Docker Compose之Sidecar模式
winform使用webBrowser,C#直接调用js函数,并且支持array数组
.NET 高效依赖注入:使用 Lazy<T> 和工厂模式优化性能与内存占用
单表演练之产品资料,辅助组件ViewDataBinding模式
化繁为简,用几个例子介绍JavaScript异步处理async awite
EFCore异步查询报错
Devexpress 表格GridView列RepositoryItemPictureEdit异步加载远程URL图片
.Net Core 5.x Api开发笔记 -- 基础日志(Log4Net)(八)
.NET使用TaskCompletionSource将事件转移到异步方法
使用Hot Chocolate和.NET 6构建GraphQL应用(3) —— 实现Query基础功能
66. SAP S/4HANA EWM 入门:仓库结构、基础流程与企业组织定义
函数防抖、节流

热门标签
.NET Core .NET Reactor ag-grid AI发布 api安全 ASP.NET Core C#DLL加密 C#播放声音 C#代码混淆 C#代码加密 ChromeDriver Codex DateTime DBeaver devexpress devTool DLL混淆 edge.js EF EFCore Electron element-ui el-form el-table excel FastReport FileStream FolderBrowerDialog FolderSelectDialog form提交 git gridcontrol gridview input javascript json字符串 JS转换对象JSON jwt JWT授权 linq log Math MCP mitmproxy MVC MySQL Navicat netstat nginx node_modules NSwag Nuget Nuget镜像 number PowerShell pyinstaller python pythoncom python爬虫 python抓包 pywin32 redis Requests-html RestSharp Selenium sql SQL Server Swagger to-cms Visual Studio VSCode vue VueRouter vue路由 VUE页面通讯 Webpack Windows Windows服务 winform wmi xlrd yaml YESCMS YESWEB开发框架 白象 表单提交 播放声音 打开URL 代码混淆 弹窗提醒 端口占用 对象转换 分布式 公共字典 机器码 进程排查 静态资源 开发指南 路由参数 密钥 配置教程 配置文件 权限 人工智能 任务 任务调度 日期间隔 日志 日志记录 省市区 授权验证 数据库 四舍五入 文案 文件读取 文件夹选择 文件目录选择 问题排查 行政区域数据 页面通讯 中间件 CSharp 事务锁 工单系统 并发控制 重复提交 CMS Markdig Markdown markdown-it marked 技术选型 VS Code 开发工具 源代码管理 版本控制 Docker PostgreSQL 时区 部署排查 CMS架构 EF Core 主题系统 二次开发 插件系统 容器 运维命令 镜像清理 Linux NAS 远程挂载 飞牛 fnOS S/4HANA SAP GUI SAP HANA SAP R/3 SAP入门 SAP版本 ERP SAP SAP MM 库存管理 物料管理 采购管理 入门教程 SAP S/4HANA SPRO 企业结构 采购组织 MM01 物料主数据 物料类型 BP分组 业务伙伴 供应商主数据 ME41 RFQ 库存物料 采购流程 ME51 消耗性物料 科目分配 采购申请 AC03 ML81N 外部服务 服务主数据 Business Partner SAP培训 ME51N MM模块 Lean Services MM-SRV 外部服务采购 PIR 供应来源 采购主数据 采购信息记录 ME31K 框架协议 计划协议 采购合同 ME01 供应来源确定 货源清单 MEQ1 供应源确定 配额安排 配额评分 MD04 MD21 MRP 计划文件 需求计划 批量程序 MD01N MD02 MRP Live MD05 MM 物料计划 优化采购 供应源 采购订单 ME2A 供应商确认 采购监控 Flexible Workflow 凭证释放 采购审批 释放策略 实地盘点 物料凭证 货物移动 MIGO 收货 移动类型 已撤回 供应商退货 货物发出 STO 库存转储 转移过账 生产订单 预留 GR/IR MIRO 供应商发票 物流发票校验 OMR2 税码 FI PP SD 实操教程 MRBR OMR6 发票差异 交货成本 后续借记 MI01 实物盘点 盘点差异 公司代码 工厂 组织结构 OMS2 主数据定制 自动科目确定 BP角色 CVI 伙伴确定 编号范围 凭证类型 字段选择 FBN1 OMBT OMC2 会计凭证 OMJJ BOM 委外加工 项目类别L MRKO 供应商寄售 特殊库存K MRKON PIPE Pipeline 特殊库存P ERS MRIS 发票计划 周期性结算 里程碑付款 变更追踪 版本管理 采购凭证 SFTP WebDAV 网盘 飞牛fnOS AMPL HERS MPN 中文教程 库存确定 可用性检查 缺件检查 Output Management 消息确定 输出确定 分割评估 库存计价 评估类别 评估类型 PB00 RM0000 条件技术 采购定价 MM-FI集成 OBYC 库存估价 文本类型 文本采用 EFB EVO MSV SU3 用户参数 发票校验 合同参照 履约保留款 特别总账 预付款 Fiori Launchpad SAP Fiori 应用导航 用户体验 LSMW LTMC Migration Cockpit 数据迁移 BRFplus OPD Output Control My Inbox 审批流程 灵活工作流 SAP PP 外部加工 SAP QM 检验批 质量信息记录 采购收货 SAP PM 维护BOM 维护订单 SAP SD SAP Service 端到端流程 MM模块培训 FI-MM集成 供应商管理 审批配置 FICO入门 SAP FICO 财务配置 供应商税务 预扣税 House Bank 银行对账 客户清账 应收账款 FI控制 验证与替代 印度 GST 税务配置 F110 FBZP EWM入门 SAP EWM 仓库管理 OX14 成本核算 物料评估 后勤配置 物料组 价值更新 数量更新 PP-PI 流程制造 生产计划 容差配置 SAP事务码 SAP基础 TCODE Basis 事务代码 MMNR 编号区间 采购实操 组织架构 OMSF SAP实操 FI配置
联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码