91网首页-91网页版-91网在线观看-91网站免费观看-91网站永久视频-91网站在线播放

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開(kāi)發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

.NET Core 線程(Thread)底層原理淺談

freeflydom
2025年2月10日 10:12 本文熱度 797

內(nèi)核態(tài),用戶態(tài),線程,進(jìn)程,協(xié)程基本概念不再贅述。

原生線程和用戶線程

  1. 原生線程
    在內(nèi)核態(tài)中創(chuàng)建的線程,只服務(wù)于內(nèi)核態(tài)

  2. 用戶線程
    由User Application創(chuàng)建的線程,該線程會(huì)在內(nèi)核態(tài)與用戶態(tài)中間來(lái)回穿梭
    比如Throw Exception,就會(huì)由CLR 線程觸發(fā),從用戶態(tài)切換到內(nèi)核態(tài),再切換回用戶態(tài)。

時(shí)鐘中斷與時(shí)間片

時(shí)鐘中斷的底層,是由主板上的硬件定時(shí)器產(chǎn)生,以固定的時(shí)間間隔(15.6ms)觸發(fā)。windows作為消費(fèi)端,來(lái)處理多線程任務(wù)調(diào)度/定時(shí)任務(wù)。

操作系統(tǒng)獲取到中斷后,再自行分配時(shí)間片,每個(gè)線程在一個(gè)時(shí)間片里獲得CPU的運(yùn)行時(shí)間,等時(shí)間片用完后,再由操作系統(tǒng)分配給下一個(gè)線程
windows 客戶端一個(gè)時(shí)間片為 2個(gè)時(shí)鐘中斷 (15.6*2=31.5ms)
windows 服務(wù)端一個(gè)時(shí)間片為 12個(gè)時(shí)鐘中斷 (15.6*12=187.2ms,主要是為了更高的吞吐量)

CLR via C# 一文中說(shuō)windows每30ms切換一次就是這個(gè)原因。

當(dāng)一個(gè)線程時(shí)間片用完后,操作系統(tǒng)會(huì)將新的時(shí)間片轉(zhuǎn)移給其它線程。以實(shí)現(xiàn)“多線程”效果。

單個(gè)核心在同一時(shí)間只能處理一個(gè)線程的任務(wù)。

眼見(jiàn)為實(shí)

  1. 中斷多久觸發(fā)一次?
    使用windbg進(jìn)入內(nèi)核態(tài),使用nt!KeMaximumIncrement命令查看看它的值

注意,單位為100ns,因此156250*100/1000/1000=15.625ms

Windows下CPU核的數(shù)據(jù)結(jié)構(gòu)

Windows會(huì)給每一個(gè) CPU核 分配一個(gè)_KPCR的內(nèi)存結(jié)構(gòu),用來(lái)記錄當(dāng)前CPU的狀態(tài)。并拓展了_KPRCB來(lái)記錄更多信息。
關(guān)鍵信息就是存儲(chǔ)著 CuurentThread/NextThread/IdleThread(空閑線程)

眼見(jiàn)為實(shí)

使用dt命令來(lái)查看

dt命令是一個(gè)非常有用的顯示類型信息的工具,主要用于查看和分析數(shù)據(jù)結(jié)構(gòu)的布局和內(nèi)容

CPU當(dāng)前正在執(zhí)行哪個(gè)線程?

使用!running命令,可以看到當(dāng)前 CPU核 正在執(zhí)行的線程

本質(zhì)上就是對(duì)_KPCR/_KPRCB的提煉簡(jiǎn)化,

Windows下線程的數(shù)據(jù)結(jié)構(gòu)

每個(gè)線程都有以下要素,這是創(chuàng)建線程無(wú)法避免的開(kāi)銷。

  1. 線程內(nèi)核對(duì)象(Thread Kernel Object)
    OS中創(chuàng)建的每一個(gè)線程都會(huì)分配數(shù)據(jù)結(jié)構(gòu)來(lái)承載描述信息
    Windows會(huì)給每一個(gè) Thread 分配一個(gè)_ETHREAD的內(nèi)存結(jié)構(gòu),用來(lái)記錄當(dāng)前線程的狀態(tài),其中就包括了線程上下文(Thread Context)

  2. 線程環(huán)境塊(Thread Environment Block, TEB)
    TEB是在用戶態(tài)中分配的內(nèi)存塊,主要包括線程的Exception,Local Storage等信息

  3. 用戶態(tài)線程棧(User-Mode Stack)
    我們常說(shuō)的??臻g就是指的這里,大名鼎鼎的OOM就出自于此

  4. 內(nèi)核態(tài)線程棧(Kernel-Mode Stack)
    處于安全隔離考慮,在內(nèi)核態(tài)中復(fù)制了一個(gè)同樣的??臻g。用來(lái)處理用戶態(tài)訪問(wèn)內(nèi)核態(tài)的代碼。

眼見(jiàn)為實(shí)

  1. 線程內(nèi)核對(duì)象
    使用命令dt nt!_ETHREAD

  2. TEB
    使用命令dt nt!_TEB

線程上下文切換的本質(zhì)

上下文切換的本質(zhì)就是,備份被切換線程寄存器的值,到該線程的上下文中。再?gòu)那袚Q后的線程中,讀取上下文到寄存器中。

舉個(gè)簡(jiǎn)單的例子就是,我跟你輪流打游戲,我玩的時(shí)候要先加載我的存檔,輪到你玩的時(shí)候,我再保存我的存檔。你玩的時(shí)候重復(fù)這一過(guò)程。

線程切換的成本

上下文切換是凈開(kāi)銷,不會(huì)帶來(lái)任何性能上的收益。因此優(yōu)化程序的一個(gè)思路就是降低上下文切換

  1. 顯式成本
    保存寄存器的值到內(nèi)存,從內(nèi)存讀取寄存器。
    寄存器的數(shù)量越多成本就越高,以AMD 7840HS處理器為例,總共有17個(gè)寄存器

  2. 隱式成本
    如果線程切換是在同一個(gè)進(jìn)程中,它們共享用戶態(tài)的虛擬內(nèi)存空間。所以當(dāng)線程切換的時(shí)候,就有可能命中CPU的緩存(比如線程之間共享的變量,代碼)。
    如果在不同的進(jìn)程中,線程的切換則會(huì)導(dǎo)致用戶態(tài)的虛擬內(nèi)存空間都失效,進(jìn)而導(dǎo)致CPU緩存失效。

眼見(jiàn)為實(shí)

說(shuō)了這么多理論,不如直接看源碼。

/*主代碼入口*/
PUBLIC KiSwapContext
.PROC KiSwapContext
    /* Generate a KEXCEPTION_FRAME on the stack */
	/* 核心邏輯:把寄存器全部備份一遍 */
    GENERATE_EXCEPTION_FRAME
    /* Do the swap with the registers correctly setup */
	/* 將新線程的地址,交換到R8寄存器上 */
    mov r8, gs:[PcCurrentThread] /* Pointer to the new thread */
    call KiSwapContextInternal
    /* Restore the registers from the KEXCEPTION_FRAME */
	/* 把之前保存的寄存器值恢復(fù)到CPU寄存器 */
    RESTORE_EXCEPTION_STATE
    /* Return */
    ret
.ENDP
MACRO(GENERATE_EXCEPTION_FRAME)
    /* Allocate a KEXCEPTION_FRAME on the stack */
    /* -8 because the last field is the return address */
    sub rsp, KEXCEPTION_FRAME_LENGTH - 8
    .allocstack (KEXCEPTION_FRAME_LENGTH - 8)
    /* Save non-volatiles in KEXCEPTION_FRAME */
    mov [rsp + ExRbp], rbp
    .savereg rbp, ExRbp
    mov [rsp + ExRbx], rbx
    .savereg rbx, ExRbx
    mov [rsp +ExRdi], rdi
    .savereg rdi, ExRdi
    mov [rsp + ExRsi], rsi
    .savereg rsi, ExRsi
	......省略
ENDM
MACRO(RESTORE_EXCEPTION_STATE)
    /* Restore non-volatile registers */
    mov rbp, [rsp + ExRbp]
    mov rbx, [rsp + ExRbx]
    mov rdi, [rsp + ExRdi]
    mov rsi, [rsp + ExRsi]
    mov r12, [rsp + ExR12]
    mov r13, [rsp + ExR13]
    mov r14, [rsp + ExR14]
    mov r15, [rsp + ExR15]
    movaps xmm6, [rsp + ExXmm6]
	......省略
    /* Clean stack and return */
    add rsp, KEXCEPTION_FRAME_LENGTH - 8
ENDM

https://github.com/reactos/reactos/blob/master/ntoskrnl/ke/amd64/ctxswitch.S

線程調(diào)度模型(究極簡(jiǎn)化版)

在上面說(shuō)到的邏輯核數(shù)據(jù)結(jié)構(gòu)_KPRCB中,有三個(gè)屬性。
單鏈表的DeferredReadyListHead,雙鏈表的WaitListHead, 二維數(shù)組形態(tài)的DispatcherReadyListHead。

簡(jiǎn)單來(lái)說(shuō),當(dāng)線程切換時(shí),邏輯核從DispatcherReadyListHead根據(jù)線程優(yōu)先級(jí)切換高優(yōu)先級(jí)線程。如果線程主動(dòng)放棄了時(shí)間片(thread.yield/thread.sleep),則會(huì)把線程放入DeferredReadyListHead。WaitListHead則用于存放那些正在等待某些事件發(fā)生的線程,如等待 I/O 操作完成、等待某個(gè)信號(hào)量或者等待互斥體等

眼見(jiàn)為實(shí)

直接看源碼

可以看到DispatcherReadyListHead大小為32,主要是因?yàn)閣indows將線程優(yōu)先級(jí)設(shè)為了0-31不同的級(jí)別。

https://github.com/reactos/reactos/blob/master/sdk/include/ndk/amd64/ketypes.h

線程優(yōu)先級(jí)

Windows\Linux作為搶占式操作系統(tǒng),無(wú)法保證線程一直運(yùn)行。因此使用線程優(yōu)先級(jí)來(lái)讓用戶有一定的控制權(quán)。
windows每個(gè)線程都有0(最低)~31(最高)的優(yōu)先級(jí),存儲(chǔ)在DispatcherReadyListHead中,OS為線程分配時(shí)間片時(shí),就是優(yōu)先為高優(yōu)先級(jí)線程分配時(shí)間.
只要一直存在31優(yōu)先級(jí)的線程,就永遠(yuǎn)不可能調(diào)用0~30優(yōu)先級(jí)的線程。這稱為“線程饑餓”
Linux 使用 nice 值來(lái)表示優(yōu)先級(jí),范圍是從 - 20 到 19。nice 值越小,優(yōu)先級(jí)越高,默認(rèn)的 nice 值是 0

C#線程結(jié)構(gòu)模型

C#線程的底層是CLR托管線程,而CLR的承載是操作系統(tǒng)線程。因此它們都有一一對(duì)應(yīng)的關(guān)系。

分別對(duì)應(yīng)C#線程(Thread.CurrentThread.ManagedThreadId),CLR線程,OS線程

線程在創(chuàng)建過(guò)程中會(huì)經(jīng)歷兩個(gè)階段

        static void Main(string[] args)
        {
            var testThread = new Thread(DoWork);// 這個(gè)階段只會(huì)在CLR中創(chuàng)建Thread,在OS上沒(méi)有創(chuàng)建
            testThread.Start();//CLR底層會(huì)調(diào)用系統(tǒng)api,創(chuàng)建OS線程
        }

clr 保留了Lowest,BelowNormal,Normal,AboveNormal,Highest 5個(gè)線程優(yōu)先級(jí)

前臺(tái)線程與后臺(tái)線程

注意,這僅僅是CLR的概念,在OS層面是沒(méi)有此概念的。
前臺(tái)線程:適用于關(guān)鍵性任務(wù),進(jìn)程會(huì)等待所有前臺(tái)線程執(zhí)行完畢后,才會(huì)正常退出。Thread默認(rèn)是前臺(tái)線程
后臺(tái)線程:適用于非關(guān)鍵性任務(wù),進(jìn)程不會(huì)等待后臺(tái)線程執(zhí)行完畢,直接退出。ThreadPool默認(rèn)是后臺(tái)線程

思考一個(gè)問(wèn)題,托管線程調(diào)用非托管代碼,非托管代碼調(diào)用托管代碼。它們用什么線程來(lái)調(diào)用?
前者取決于線程創(chuàng)建方式(Thread/ThreadPool),后者為后臺(tái)線程,因?yàn)閚ative thread要綁定managed thread,由線程池創(chuàng)建

協(xié)程與虛擬線程

目前.NET 9 還不支持該特性
https://steven-giesel.com/blogPost/59752c38-9c99-4641-9853-9cfa97bb2d29

線程本地存儲(chǔ)(Thread Local Storage, TLS)

TLS用于實(shí)現(xiàn)按照線程隔離的線程本地變量,其修改的值只對(duì)修改的線程可見(jiàn)。

  1. 原生實(shí)現(xiàn)
    OS原生支持TLS,比如在windows上通過(guò)TlsAlloc/TlsGetValue/TlsSetValue實(shí)現(xiàn)對(duì)TLS數(shù)據(jù)的分配/修改/賦值
    OS使用分段寄存器(比如gs寄存器)存儲(chǔ)指向TLS數(shù)據(jù)的地址。利用上下文切換機(jī)制,每個(gè)native thread可以獨(dú)立訪問(wèn)gs寄存器。進(jìn)而定位到關(guān)聯(lián)的TLS

  2. .Net實(shí)現(xiàn)
    C#的TLS是基于C++做的封裝,TLS中只存儲(chǔ)了一個(gè)ThreadLocalInfo對(duì)象,最后借助它與Thread的關(guān)聯(lián)。來(lái)得知存儲(chǔ)在托管堆中的線程本地變量

兩者總體思路都是使用一段內(nèi)存來(lái)存儲(chǔ)本地變量,當(dāng)線程切換時(shí),切換本地變量存儲(chǔ)地址的指針。

眼見(jiàn)為實(shí)

使用~命令得出每個(gè)線程棧的范圍

在使用!teb 觀察其內(nèi)存布局,可以看到TLS Storage指向的內(nèi)存空間,其訪問(wèn)模式為BaseAddress+偏移量模式

ThreadStatic Attribute底層實(shí)現(xiàn)

在.NET中,原生線程通過(guò)ThreadLocalInfo來(lái)關(guān)聯(lián)托管線程的mapping關(guān)系,托管線程關(guān)聯(lián)TLB(Thread Local lock)表,TLB再關(guān)聯(lián)TLM(Thread Local Module),TLM再關(guān)聯(lián)托管堆,托管堆中才是存儲(chǔ).NET TLS真正的地方。

使用ThreadLocal的理由

    internal class Program
    {
        [ThreadStatic]
        public static Person _person = new Person() { Age = 18 };
        static void Main(string[] args)
        {
            Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine($"ThreadId={Thread.CurrentThread.ManagedThreadId},{_person.Age++}");
                    Thread.Sleep(1000);
                }
            });
            Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine($"ThreadId={Thread.CurrentThread.ManagedThreadId},{_person.Age++}");
                    Thread.Sleep(1000);
                }
            });
            Console.ReadLine();
        }
    }
    public class Person
    {
        public int Age;
    }

小伙伴可以運(yùn)行一段這段代碼,就知道為什么要使用ThreadLocal而不使用ThreadStatic Attribute。

tips:靜態(tài)構(gòu)造函數(shù)只能運(yùn)行一次。

AsyncThreadLocal

    public class AsyncLocalDemo
    {
        private static ThreadLocal<int> tls = new ThreadLocal<int>();
        private static AsyncLocal<int> asyncTls=new AsyncLocal<int>();
        public async Task Example(int i,int j)
        {
            Console.WriteLine($"current thread Id={Thread.CurrentThread.ManagedThreadId}");
            tls.Value = i;
            asyncTls.Value = j;
			
			//ExecutionContext.SuppressFlow();//阻止上下文流動(dòng),會(huì)導(dǎo)致AsyncLocal失效
            await Task.Delay(1000);
            Console.WriteLine($"current thread Id={Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"tls={tls.Value}");
            Console.WriteLine($"asyncTls={asyncTls.Value}");
        }
    }

運(yùn)行此段代碼,會(huì)發(fā)現(xiàn)TLS失效。其原因是: 線程1持有的TLS,在經(jīng)過(guò)await后,接著往下處理的線程變?yōu)?span style="color: rgb(255, 0, 0);">線程11,線程11并不能讀取到線程1的TLS,所以會(huì)失效

AsyncLocal原理

為什么AsyncLocal能成功呢?上源碼

    public sealed class AsyncLocal<T> : IAsyncLocal
    {
		//當(dāng)value發(fā)生change時(shí),注入一個(gè)委托,賦予它類似AOP的能力。
        private readonly Action<AsyncLocalValueChangedArgs<T>>? _valueChangedHandler;
		
        public AsyncLocal(Action<AsyncLocalValueChangedArgs<T>>? valueChangedHandler)
        {
            _valueChangedHandler = valueChangedHandler;
        }
        public T Value
        {
            get
            {
				//以自身為key,從ExecutionContext拿出值
                object? value = ExecutionContext.GetLocalValue(this);
                if (typeof(T).IsValueType && value is null)
                {
                    return default;
                }
                return (T)value!;
            }
            set
            {
				//以自身為key,從ExecutionContext設(shè)置值
                ExecutionContext.SetLocalValue(this, value, _valueChangedHandler is not null);
            }
        }
    }

可以看到,AsyncLocal本身邏輯很簡(jiǎn)單。其核心是使用ExecutionContext實(shí)現(xiàn)get/set,那么ExecutionContext究竟是何方神圣呢?

C#中每一個(gè)線程都會(huì)綁定一個(gè)ExecutionContext,可以使用Thread.CurrentThread.ExecutionContext來(lái)查看。

理想情況下,當(dāng)一個(gè)線程使用另一個(gè)線程執(zhí)行任務(wù)時(shí),前者的執(zhí)行ExecutionContext會(huì)被copy到后者中來(lái)。這個(gè)過(guò)程被稱為上下文流動(dòng)

因此,AsyncLocal能夠執(zhí)行成功秘訣就在于,當(dāng)線程切換的時(shí)候,線程1所存儲(chǔ)AsyncTLS流動(dòng)到了到了線程11。因此線程11能夠讀取到線程1的值。

眼見(jiàn)為實(shí):上下文流動(dòng)

		public sealed partial class Thread : CriticalFinalizerObject
		{
			private void Start(bool captureContext, bool internalThread = false)
        	{
            	ThrowIfNoThreadStart(internalThread);
            	StartHelper? startHelper = _startHelper;
            	if (startHelper != null)
            	{
                	startHelper._startArg = null;
                	startHelper._executionContext = captureContext ? ExecutionContext.Capture() : null;//關(guān)鍵點(diǎn),captureContext為ture時(shí),講當(dāng)前上下文復(fù)制給新線程
            	}
            	StartCore();
        }
		}
		public sealed class ExecutionContext : IDisposable, ISerializable
		{
				public static ExecutionContext? Capture()
        	{
            	ExecutionContext? executionContext = Thread.CurrentThread._executionContext;
            	if (executionContext == null)
            	{
                	executionContext = Default;
            	}
            	else if (executionContext.m_isFlowSuppressed)
            	{
                	executionContext = null;
            	}
            	return executionContext;
        	}
		}

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/AsyncLocal.cs
https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs

TLS為何不會(huì)內(nèi)存泄露?

眾所周知,在JAVA的世界中。使用TLS,如果不及時(shí)釋放是會(huì)造成內(nèi)存泄露的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocalMemoryLeakExample {
    // 創(chuàng)建一個(gè)ThreadLocal對(duì)象,用于存儲(chǔ)每個(gè)線程獨(dú)有的大對(duì)象(這里簡(jiǎn)單用一個(gè)數(shù)組模擬大對(duì)象)
    private static final ThreadLocal<int[]> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        // 創(chuàng)建一個(gè)固定大小的線程池,包含3個(gè)線程
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 循環(huán)提交任務(wù)到線程池
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                // 為每個(gè)線程創(chuàng)建一個(gè)較大的數(shù)組對(duì)象,并存儲(chǔ)到ThreadLocal中
                int[] largeArray = new int[10000];
                threadLocal.set(largeArray);
				
                // 如果沒(méi)有清理ThreadLocal中的數(shù)據(jù),就會(huì)造成內(nèi)存泄露
				//threadLocal.remove();
            });
        }
        // 關(guān)閉線程池,等待所有任務(wù)完成
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        // 此時(shí),由于沒(méi)有清理ThreadLocal中的數(shù)組對(duì)象,并且線程池中的線程會(huì)被復(fù)用
        // 這些大數(shù)組對(duì)象會(huì)一直占用內(nèi)存,隨著任務(wù)不斷執(zhí)行,內(nèi)存占用會(huì)越來(lái)越大,導(dǎo)致內(nèi)存泄漏
    }
}

簡(jiǎn)單來(lái)說(shuō),就是線程池的復(fù)用會(huì)導(dǎo)致Thread一直存在引用關(guān)系,因此線程不會(huì)銷毀。而線程不銷毀,因此一直保持著對(duì)ThreadLocal的引用。所以GC就不會(huì)釋放這個(gè)對(duì)象,隨著線程被不斷復(fù)用,ThreadLocal慢慢累積,從而導(dǎo)致內(nèi)存泄露。

在C#的世界中,ThreadLocal并不會(huì)造成內(nèi)存泄露,主要仰仗Disposable模式,讓我們?cè)谠创a中一探究竟。

public class ThreadLocal<T> : IDisposable
{
	//初始化默認(rèn)值的方法
	private Func<T>? _valueFactory;
	
	//存儲(chǔ)著每個(gè)線程的不同值
	[ThreadStatic]
	private static LinkedSlotVolatile[]? ts_slotArray;
	
	//當(dāng)前線程所對(duì)應(yīng)的索引,取值類似ts_slotArray[s_idManager]
	private static readonly IdManager s_idManager = new IdManager();
	
	//線程結(jié)束后清理:該對(duì)象的析構(gòu)函數(shù)會(huì)枚舉ts_slotArray,并把結(jié)束線程的元素移出鏈表。達(dá)到?jīng)]有引用的目的
	[ThreadStatic]
	private static FinalizationHelper? ts_finalizationHelper;
	
	public T Value
	{
		get
		{ 
			//省略
		}
		set
		{
			SetValueSlow(value, slotArray);
		}
	}
	private void SetValueSlow(T value, LinkedSlotVolatile[]? slotArray)
	{
		if (slotArray == null)
		{
			slotArray = new LinkedSlotVolatile[GetNewTableSize(id + 1)];
			ts_finalizationHelper = new FinalizationHelper(slotArray);
			ts_slotArray = slotArray;
		}
		if (slotArray[id].Value == null)
		{
			CreateLinkedSlot(slotArray, id, value);//創(chuàng)建一個(gè)雙向鏈表,用于記錄所有寫入過(guò)ts_slotArray的元素。
		}
	}
	//Dispose 會(huì)自動(dòng)將元素設(shè)為null
	protected virtual void Dispose(bool disposing)
	{
		for (LinkedSlot? linkedSlot = _linkedSlot._next; linkedSlot != null; linkedSlot = linkedSlot._next)
		{
			linkedSlot._slotArray = null;
			slotArray[id].Value!._value = default;
			slotArray[id].Value = null;
		}
	}
}

轉(zhuǎn)自https://www.cnblogs.com/lmy5215006/p/18556052


該文章在 2025/2/10 10:12:12 編輯過(guò)
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國(guó)內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場(chǎng)、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場(chǎng)作業(yè)而開(kāi)發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉(cāng)儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購(gòu)管理,倉(cāng)儲(chǔ)管理,倉(cāng)庫(kù)管理,保質(zhì)期管理,貨位管理,庫(kù)位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

主站蜘蛛池模板: 91成人精品视频 | 日本精品在线播放 | 午夜日韩高清 | 美式影院 | 国产在线视频精 | 国产乱了真实 | 人成在线免费视频 | 国产原创精品 | 久色福利 | 91午夜男人视频 | 欧美一级a欧美特黄 | 国产女主播精 | 国产一区视频观看 | 国产精品成熟老女人 | 91精品在线观看 | 国产初次视频观看 | 九九福利 | 国产不卡在线观看 | 日本乱理伦片在线观 | 99热有精品. | 国产激情视频网站 | 日本欧美国产一区 | 日本精品a在线观看 | 日韩精品综合 | 国产精品夫妻在线 | 国产剧视频在线播放 | 18日韩亚洲 | 欧美综合图 | 日本一a精品网站 | 成人一区日韩 | 日产国产一区二区 | 日韩中文在线播放 | 每日更新国产 | 国产精品亚洲va | 日本免费| 国产高清激情视频 | 九七精品人| 日本中文字幕第一页 | 欧美另类日韩成人 | 尤物邪恶 | 国产精品成人免费福 |