UE4多线程

PrimeNumberWorker.h

 

class UMyGameInstance;

//~~~~~ Multi Threading ~~~
class FPrimeNumberWorker : public FRunnable
{
	static  FPrimeNumberWorker* Runnable;
	FRunnableThread* Thread;
	TArray<uint32>* PrimeNumbers;
	AActor* mmyActor;
	FThreadSafeCounter StopTaskCounter; //用于多线程间的判断交互,volatile int32 Counter;
	int32 FindNextPrimeNumber();

	FCriticalSection QueueCritical; //互斥锁
	FEvent* ThreadSuspendedEvent;   //线程悬挂和唤醒事件

public:
	bool IsFinished();
	void Suspend();
	void Resume();

	//Constructor / Destructor
	FPrimeNumberWorker(TArray<uint32>& TheArray, AActor* myActor, const int32 PrimesFoundCount); //第三个变量为测试
	virtual ~FPrimeNumberWorker();

	// Begin FRunnable interface.
	virtual bool Init();
	virtual uint32 Run();
	virtual void Stop();
	// End FRunnable interface

	/** Makes sure this thread has stopped properly */
	void EnsureCompletion();
	FCriticalSection* GetCriticalSection();

	/*
		Start the thread and the worker from static (easy access)!
		This code ensures only 1 Prime Number thread will be able to run at a time.
		This function returns a handle to the newly started instance.
	*/

	static FPrimeNumberWorker* JoyInit(TArray<uint32>& TheArray, AActor* myActor, const int32 PrimesFoundCount);//第三个变量为测试
	static FPrimeNumberWorker* Get();
	static void Shutdown();
	static bool IsThreadFinished();


public:
	int32	mPrimesFoundCount;
	

	//~~~ Thread Core Functions ~~~
};

PrimeNumberWorker.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "TTTTT.h"
#include "PrimeNumberWorker.h"

//***********************************************************
//Thread Worker Starts as NULL, prior to being instanced
//		This line is essential! Compiler error without it
FPrimeNumberWorker* FPrimeNumberWorker::Runnable = nullptr;
//***********************************************************
FPrimeNumberWorker::FPrimeNumberWorker(TArray<uint32>& TheArray, AActor* myActor, const int32 PrimesFoundCount)
	: mmyActor(myActor)
	, StopTaskCounter(0)
	, mPrimesFoundCount(PrimesFoundCount)//Test
{
	ThreadSuspendedEvent = FPlatformProcess::GetSynchEventFromPool();
	PrimeNumbers = &TheArray;
	Thread = FRunnableThread::Create(this, TEXT("FPrimeNumberWorker"), 0, TPri_BelowNormal); //windows default = 8mb for thread, could specify more
}

FPrimeNumberWorker::~FPrimeNumberWorker()
{
	delete Thread;
	Thread = nullptr;
	FPlatformProcess::ReturnSynchEventToPool(ThreadSuspendedEvent);
	ThreadSuspendedEvent = nullptr;
}

bool FPrimeNumberWorker::Init()
{
	PrimeNumbers->Empty();
	//PrimeNumbers->Add(2);
	//PrimeNumbers->Add(3);
	if (mmyActor)
	{
		UE_LOG(LogTemp, Warning, TEXT("**********************************"));
		UE_LOG(LogTemp, Warning, TEXT("Prime Number Thread Started!"));
		UE_LOG(LogTemp, Warning, TEXT("**********************************"));
	}
	return true;
}

uint32 FPrimeNumberWorker::Run()
{
	//Initial wait before starting
	FPlatformProcess::Sleep(0.03);
	// While not told to stop this thread
	//		and not yet finished finding Prime Numbers
	while (StopTaskCounter.GetValue() == 0 && !IsFinished())
	{
		FScopeLock* QueueLock = new FScopeLock(&QueueCritical); //锁住
																//***************************************
																//不要 spawning / modifying / deleting UObjects / AActors 等等之类的事
																//这里做多线程间共享信息的 modify,如:PrimeNumbers->Add
																//***************************************
		PrimeNumbers->Add(FindNextPrimeNumber());
		mPrimesFoundCount++;

		//***************************************
		//Show Incremental Results in Main Game Thread!

		//	Please note you should not create, destroy, or modify UObjects here.
		//	  Do those sort of things after all thread are completed.

		//	  All calcs for making stuff can be done in the threads
		//	     But the actual making/modifying of the UObjects should be done in main game thread.
		UE_LOG(LogTemp, Warning, TEXT("%s"), *FString::FromInt(PrimeNumbers->Last()));
		UE_LOG(LogTemp, Warning, TEXT("%d"), mPrimesFoundCount);
		//***************************************

		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		//prevent thread from using too many resources
		//FPlatformProcess::Sleep(0.01);
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~




		PrimeNumbers->Add(FindNextPrimeNumber());
		UE_LOG(LogTemp, Warning, TEXT("--- FPrimeNumberWorker::Run, lock"));
		//Suspend();
		//prevent thread from using too many resources
		FPlatformProcess::Sleep(1.0f); //这里睡眠3秒是为了让GameThread中的AActor::MyAsyncThread中的日志打不出来
		delete QueueLock;//解锁
		UE_LOG(LogTemp, Warning, TEXT("--- FPrimeNumberWorker::Run, unlock"));
		//FPlatformProcess::Sleep(2.0f); //这里睡眠2秒是为了让GameThread中获取到 互斥锁QueueCritical 并锁住
	}
	Stop();
	//Run FPrimeNumberWorker::Shutdown() from the timer in Game Thread that is watching
	//to see when FPrimeNumberWorker::IsThreadFinished()
	return 0;
}

bool FPrimeNumberWorker::IsFinished()
{
	return PrimeNumbers->Num() == 5;
}

void FPrimeNumberWorker::Suspend()
{
	ThreadSuspendedEvent->Wait();
}

void FPrimeNumberWorker::Resume()
{
	ThreadSuspendedEvent->Trigger();
}

void FPrimeNumberWorker::Stop()
{
	StopTaskCounter.Increment();
}

FPrimeNumberWorker* FPrimeNumberWorker::JoyInit(TArray<uint32>& TheArray, AActor* myActor, const int32 PrimesFoundCount)
{
	//Create new instance of thread if it does not exist
	//		and the platform supports multi threading!
	if (!Runnable && FPlatformProcess::SupportsMultithreading())
	{
		Runnable = new FPrimeNumberWorker(TheArray, myActor, PrimesFoundCount);
	}
	bool isSupport = FPlatformProcess::SupportsMultithreading();
	FString msg = isSupport ? "SupportsMultithread" : "dont SupportsMultithreading";
	UE_LOG(LogTemp, Warning, TEXT("--- FPrimeNumberWorker::JoyInit, msg:%s"), *msg);
	return Runnable;
}

FPrimeNumberWorker* FPrimeNumberWorker::Get()
{
	return Runnable;
}

void FPrimeNumberWorker::EnsureCompletion()
{
	Stop();
	Thread->WaitForCompletion();
}

FCriticalSection* FPrimeNumberWorker::GetCriticalSection()
{
	return &QueueCritical;
}

void FPrimeNumberWorker::Shutdown()
{
	if (Runnable)
	{
		Runnable->EnsureCompletion();
		delete Runnable;
		Runnable = nullptr;
	}
}

bool FPrimeNumberWorker::IsThreadFinished()
{
	if (Runnable) return Runnable->IsFinished();
	return true;
}

int32 FPrimeNumberWorker::FindNextPrimeNumber()
{
	int32 TestPrime = 123;
	return TestPrime;
}

ThreadActor.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"
#include "ThreadActor.generated.h"

UCLASS()
class TTTTT_API AThreadActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AThreadActor();
	~AThreadActor();
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	TArray<uint32> PrimeNumbers;
	UFUNCTION(BlueprintCallable, Category = "My|ThreadActor")
		void MyAsyncSuspend();
	UFUNCTION(BlueprintCallable, Category = "My|ThreadActor")
		void MyAsyncResume();
	UFUNCTION(BlueprintCallable, Category = "My|ThreadActor")
		void MyAsyncThread();

	void BeginDestroy() override;

};

ThreadActor.cpp

 

 

// Fill out your copyright notice in the Description page of Project Settings.

#include "TTTTT.h"
#include "ThreadActor.h"
#include "PrimeNumberWorker.h"

// Sets default values
AThreadActor::AThreadActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}
AThreadActor::~AThreadActor()
{
	//FPrimeNumberWorker::Shutdown();
}
// Called when the game starts or when spawned
void AThreadActor::BeginPlay()
{
	Super::BeginPlay();
	//FPrimeNumberWorker::JoyInit(PrimeNumbers, 50000, GetWorld()->GetFirstPlayerController());
}

// Called every frame
void AThreadActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AThreadActor::MyAsyncSuspend()
{
	FPrimeNumberWorker::Get()->Suspend();
}

void AThreadActor::MyAsyncResume()
{
	FPrimeNumberWorker::Get()->Resume();
}

void AThreadActor::MyAsyncThread()
{
	FTimerHandle mTimer;
	PrimeNumbers.Empty();
	FPrimeNumberWorker::JoyInit(PrimeNumbers, this, 500);
	GetWorldTimerManager().SetTimer(mTimer, [&]()->void {
		FPrimeNumberWorker* pnw = FPrimeNumberWorker::Get();
		if (!pnw) return;

		FCriticalSection* cs = pnw->GetCriticalSection(); //获取FPrimeNumberWorker到中的互斥锁QueueCritical
		//FScopeLock QueueLock(cs);//锁住,等作用域过后QueueLock自动析构解锁     卡住
		UE_LOG(LogTemp, Warning, TEXT("--- AMyActor::MyAsyncThread, PrimeNumbers.Num=%d"), PrimeNumbers.Num());
		if (pnw->IsThreadFinished())
			FPrimeNumberWorker::Shutdown();
	}, 1.0f, true);
}

void AThreadActor::BeginDestroy()
{
	Super::BeginDestroy();
	FPrimeNumberWorker::Shutdown();
	UE_LOG(LogTemp, Warning, TEXT("Game exit!"));
}

多线程Task:

Actor.h

	/* Calculates prime numbers in the game thread */
	UFUNCTION(BlueprintCallable, Category = MultiThreading)
		void CalculatePrimeNumbers();


	/* Calculates prime numbers in a background thread */
	UFUNCTION(BlueprintCallable, Category = MultiThreading)
		void CalculatePrimeNumbersAsync();


	/* The max prime number */
	UPROPERTY(EditAnywhere, Category = MultiThreading)
		int32 MaxPrime;

Actor.cpp

#include "MyBlueprintFunctionLibrary.h" 
#include "Runtime/Core/Public/Async/AsyncWork.h"
#include "ExampleAutoDeleteAsyncTask.h"
void AMyActor::CalculatePrimeNumbers()
{
	UMyBlueprintFunctionLibrary::CalculatePrimeNumbers(MaxPrime);
}

void AMyActor::CalculatePrimeNumbersAsync()
{
	auto task = new FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>(5);
	if (task)
	{
		task->StartSynchronousTask();
	}
}

MyBlueprintLibrary.h:

public:
		UFUNCTION(BlueprintCallable, Category = "Damage")
	static	void CalculatePrimeNumbers(int32 UpperLimit);
	

MyBlueprintLibrary.cpp:

void UMainFunctionLibrary::CalculatePrimeNumbers(int32 UpperLimit)
{
    //Calculating the prime numbers...
    for (int32 i = 1; i <= UpperLimit; i++)
    {
        bool isPrime = true;
        for (int32 j = 2; j <= i / 2; j++)
        {
            if (FMath::Fmod(i, j) == 0)
            {
                isPrime = false;
                break;
            }
        }

        if (isPrime) 
            GLog->Log("Prime number #" + FString::FromInt(i) + ": " + FString::FromInt(i));
    }
}

MyTask.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once


#include "MyBlueprintFunctionLibrary.h"
#include "Runtime/Core/Public/Async/AsyncWork.h"
/**
 * 
 */

class PAKPROJ_API ExampleAutoDeleteAsyncTask : public FNonAbandonableTask
{
	int32 MaxPrime;
public:
	//friend class FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>;

	ExampleAutoDeleteAsyncTask(int32 MaxPrime)
	{
		this->MaxPrime = MaxPrime;
	}
	FORCEINLINE TStatId GetStatId() const
	{
		RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
	}

	//核心接口,  这里便是任务的具体执行内容.
	void DoWork()
	{
		UMyBlueprintFunctionLibrary::CalculatePrimeNumbers(MaxPrime);
	}

};

上面是FAutoDeleteAsyncTask  下面是FAsyncTask的例子,FAsyncTask是需要手动删除的

FAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>( 5 );
MyTask->StartBackgroundTask();
 
//--or --
 
MyTask->StartSynchronousTask();
 
//to just do it now on this thread
//Check if the task is done :
 
if (MyTask->IsDone())
{
}
 
//Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.
//Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.
 
MyTask->EnsureCompletion();
delete MyTask;

 

task graph

task.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include <Array.h>
#include <TaskGraphInterfaces.h>


//Multi thread Test, finding prime number
namespace VictoryMultiThreadTest
{
	//Multi-threaded link to UObjects, do not create,modify,destroy UObjects / AActors via this link!
	//AMyTaskActor* ThePC;

	//~~~~~~~~~~~~~~~~~~~~~~~~~~
	//~~~~~~~~~~~~~~~~~~~~~~~~~~
	//~~~~~~~~~~~~~~~~~~~~~~~~~~
	//   OUTPUT RESULTS OF TASK THREADS
	TArray<int32> PrimeNumbers;

	// This is the array of thread completions to know if all threads are done yet
	FGraphEventArray		VictoryMultithreadTest_CompletionEvents;
	//~~~~~~~~~~~~~~~~~~~~~~~~~~
	//~~~~~~~~~~~~~~~~~~~~~~~~~~
	//~~~~~~~~~~~~~~~~~~~~~~~~~~

	//~~~~~~~~~~~~~~~
	//Are All Tasks Complete?
	//~~~~~~~~~~~~~~~
	bool TasksAreComplete()
	{
		//Check all thread completion events
		for (int32 Index = 0; Index < VictoryMultithreadTest_CompletionEvents.Num(); Index++)
		{
			//If  ! IsComplete()
			if (!VictoryMultithreadTest_CompletionEvents[Index]->IsComplete())
			{
				return false;
			}
		}
		return true;
	}
	//~~~~~~~~~~~
	//Actual Task Code
	//~~~~~~~~~~~
	int32 FindNextPrimeNumber()
	{
		//Last known prime number  + 1
		int32 TestPrime = PrimeNumbers.Last();

		bool NumIsPrime = false;
		while (!NumIsPrime)
		{
			NumIsPrime = true;

			//Try Next Number
			TestPrime++;

			//Modulus from 2 to current number - 1 
			for (int32 b = 2; b < TestPrime; b++)
			{
				if (TestPrime % b == 0)
				{
					NumIsPrime = false;
					break;
					//~~~
				}
			}
		}

		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		//Did another thread find this number already?
		if (PrimeNumbers.Contains(TestPrime))
		{
			return FindNextPrimeNumber(); //recursion
		}
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		//Success!
		return TestPrime;
	}




	//~~~~~~~~~~~
	//Each Task Thread
	//~~~~~~~~~~~
	class   FVictoryTestTask
	{

	public:
		FVictoryTestTask() //send in property defaults here
		{
			//can add properties here
		}

		/** return the name of the task **/
		static const TCHAR* GetTaskName()
		{
			return TEXT("FVictoryTestTask");
		}
		FORCEINLINE static TStatId GetStatId()
		{
			RETURN_QUICK_DECLARE_CYCLE_STAT(FVictoryTestTask, STATGROUP_TaskGraphTasks);
		}
		/** return the thread for this task **/
		static ENamedThreads::Type GetDesiredThread()
		{
			return ENamedThreads::AnyThread;
		}


		/*
		namespace ESubsequentsMode
		{
			enum Type
			{
				//Necessary when another task will depend on this task.
				TrackSubsequents,
				//Can be used to save task graph overhead when firing off a task that will not be a dependency of other tasks.
				FireAndForget
			};
		}
		*/
		static ESubsequentsMode::Type GetSubsequentsMode()
		{
			return ESubsequentsMode::TrackSubsequents;
		}

		//~~~~~~~~~~~~~~~~~~~~~~~~
		//Main Function: Do Task!!
		//~~~~~~~~~~~~~~~~~~~~~~~~
		void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
		{
			//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

			PrimeNumbers.Add(FindNextPrimeNumber());

			//***************************************
			//Show Incremental Results in Main Game Thread!

			//	Please note you should not create, destroy, or modify UObjects here.
			//	  Do those sort of things after all thread are completed.

			//	  All calcs for making stuff can be done in the threads
			//	     But the actual making/modifying of the UObjects should be done in main game thread,
			//		 which is AFTER all tasks have completed :)

			//ThePC->ClientMessage(FString("A thread completed! ~ ") + FString::FromInt(PrimeNumbers.Last()));
			UE_LOG(LogTemp, Warning, TEXT("A thread completed! ~ %s~"), *FString::FromInt(PrimeNumbers.Last()));
			//***************************************
		}
	};



	//~~~~~~~~~~~~~~~~~~~
	//  Multi-Task Initiation Point 
	//~~~~~~~~~~~~~~~~~~~

	PRAGMA_DISABLE_OPTIMIZATION
	void FindPrimes(const int32 TotalToFind)
	{
		PrimeNumbers.Empty();
		PrimeNumbers.Add(2);
		PrimeNumbers.Add(3);

		//~~~~~~~~~~~~~~~~~~~~
		//Add thread / task for each of total prime numbers to find
		//~~~~~~~~~~~~~~~~~~~~
		
		for (int32 b = 0; b < TotalToFind; b++)
		{
			VictoryMultithreadTest_CompletionEvents.Add(TGraphTask<FVictoryTestTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady()); //add properties inside ConstructAndDispatchWhenReady()
		}
		
	}
	PRAGMA_ENABLE_OPTIMIZATION

}

Actor.cpp

void AMyTaskActor::VictoryCheckAllThreadsDone()
{
	if (VictoryMultiThreadTest::TasksAreComplete())
	{
	
		//Clear Timer
		GetWorldTimerManager().ClearTimer(CountdownTimerHandle);
		
		UE_LOG(LogTemp, Warning, TEXT("Multi Thread Test Done!"));
		//UE_LOG(LogTemp, Warning, TEXT("--- AMyActor::MyAsyncThread,Num=%d"), PrimeNumbers.Num());
		//UE_LOG(LogTemp, Warning, TEXT("%s"), *FString::FromInt(PrimeNumbers->Last()));
		UE_LOG(LogTemp, Warning, TEXT("Prime Numbers Found:"));
		for (int32 v = 0; v < VictoryMultiThreadTest::PrimeNumbers.Num(); v++)
		{
			UE_LOG(LogTemp, Warning, TEXT("%s~"), *FString::FromInt(VictoryMultiThreadTest::PrimeNumbers[v]));

		}
		
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("not in settime"));
	}
}

void AMyTaskActor::StartThreadTest()
{
	//VictoryMultiThreadTest::ThePC = this;
	VictoryMultiThreadTest::FindPrimes(50000); //first 50,000 prime numbers

	//Start a timer to check when all the threads are done!
	GetWorldTimerManager().SetTimer(CountdownTimerHandle,this,
		&AMyTaskActor::VictoryCheckAllThreadsDone, 1.0f, true);
}

 

在GameThread线程之外的其他线程中,不允许做一下事情

 

  • 不要 spawning / modifying / deleting UObjects / AActors
  • 不要使用定时器 TimerManager
  • 不要使用任何绘制接口,例如 DrawDebugLine

  • 如果想在主线程中异步处理(也就是分帧处理),可以使用一下接口(在 Async.h 中)

        AsyncTask(ENamedThreads::GameThread, [&]() {
            UE_LOG(LogMyTest, Warning, TEXT("--- UMyGameInstance::MyAsyncTask"));
            SpawnActor(3);
        });
    • 1
    • 2
    • 3
    • 4
    •  
    1. 测试互斥。 上面的 FPrimeNumberWorker 里面的代码方法是有加锁的,在控制台输入指令 MyAsyncThread 可测试。注释掉 FScopeLock QueueLock(cs); 这一行代码就能看出没有互斥锁的区别。
    2. 测试线程的悬挂和唤醒。 
      1. 取消注释 FPrimeNumberWorker 里面的 //Suspend();
      2. 注释掉 MyAsyncThread 测试方法中的的定时器 GetTimerManager(因为其他线程中在 未解锁 的 情况下 悬挂 的话,主线程获取不到这个锁会一直等待,也就是看起来像卡死了)
      3. 在控制台输入指令 MyAsyncThread,就可以 Run 方法的线程悬挂住该线程
      4. 在控制台输入指令 MyAsyncResume,就可以 唤醒之前调用 ThreadSuspendedEvent->Wait(); 的线程。

线程唤醒、悬挂

  • FEvent* ThreadSuspendedEvent;
  • 线程悬挂。A线程中调用 ThreadSuspendedEvent->Wait();,A线程就会悬挂主,必须由B线程调用 ThreadSuspendedEvent->Trigger(); 才能唤醒A线程。
  • 所以如果直接在主线程(GameThread)中调用 ThreadSuspendedEvent->Wait();,那就看起像卡死了。

编辑器模式下进行多线程开发注意事项

  • PIE模式下,与新开的线程交互正常;如果退出PIE模式,PIE中的实例对象都会被标记为坐等回收的对象 InValid,此时如果新开的线程还在Run中跑用到PIE中的实例对象,将造成 编辑器崩溃。

  • 最好在你的游戏退出时 FPrimeNumberWorker ShutDown一下,方便下次测试。

    void UMyGameInstance::Shutdown()
    {
        FPrimeNumberWorker::Shutdown();
        Super::Shutdown();
    }
  • 参考:https://wiki.unrealengine.com/Multi-Threading:_How_to_Create_Threads_in_UE4
  • 转载:http://blog.csdn.net/yangxuan0261/article/details/54574680
  • https://blog.csdn.net/noahzuo/article/details/51372972

https://blog.csdn.net/tuanxuan123/article/details/52780629

https://www.cnblogs.com/sevenyuan/p/9273065.html

https://wiki.unrealengine.com/Multi-Threading:_Task_Graph_System

 

 

 

 

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值