【Window】创建线程的3种方式

12 篇文章 4 订阅

第一节:【Window】创建线程的3种方式
第二节:【Window】线程同步概述
第三节:【Window】线程同步方式1——临界区(关键代码段)
第四节:【Window】线程同步方式2——互斥量
第五节:【Window】线程同步方式3——事件
第六节:【Window】线程同步方式4——信号量

在这里插入图片描述

一、 线程的状态

线程有挂起状态、执行状态、阻塞状态和等待状态。

下面分别介绍:

  • 挂起状态:线程创建后并没有直接执行或是调用函数挂起了线程。被挂起了的线程没有执行的能力,只有调用启动函数了之后才能执行。
  • 执行状态:在线程的时间片内,拥有CPU资源的时候,这是,线程便开始执行。
  • 阻塞状态:由于进行大量输入输出操作或发生执行错误时,线程失去执行状态,只有等待问题解除之后,线程才能进入等待状态。
  • 等待状态:线程启动或时间片抢占失败是等待其他线程执行,在此期间,线程随时可能被执行。

二、创建线程

2.1 创建线程方式1——CreateThread

2.1.1 说明

线程内核对象就是一个包含了线程状态信息的数据结构。每次对CreateThread 的成功调用,系统都会在内部为其分配一个内核对象。

线程上下文反应了线程上次执行的寄存器状态,来保证线程之间切换(即还原现场)。

计数器,调用一次OpenThread(CreateThread ),计数器加1,CloseThread(CloseHandle)计数器减一。当计数器值为0时,没有线程使用该内核对象,系统收回内存。计数器的初始值是2(主线程是1,创建的线程是2)。

2.1.2 函数说明

  1. 头文件
    因为C++不像Java一样需要进行跨平台优化,所以我们使用最简单的方法来实现多线程技术——windows.h中的CreateThread以及相关函数和类。首先,以如下的方式引用头文件:

    #include<windows.h>
    
  2. 函数原型:

    HANDLE  CreateThread(
       LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全性描述(一个结构体,一般是NULL)
       SIZE_T dwStackSize,                      //一种数值(栈深度,一般是0)   
       LPTHREAD_START_ROUTINE lpStartAddress,  //启动函数
       _In_opt_ __drv_aliasesMem LPVOID lpParameter, // 附加参数(一般为NULL)
        _In_ DWORD dwCreationFlags,          //运行参数(是否在创建完成后就启动线程
        _Out_opt_ LPDWORD lpThreadId   // 返回句柄(一般是0,或者是一个DWORD型变量的地址,别忘了&)
    );
    
  3. 参数说明

    lpThreadAttributes指向SECURITY_ATTRIBUTES结构的指针,决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。
    dwStackSize设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。
    pStartAddress指向线程函数的指针,函数名称没有限制,但是必须以下列形式声明:DWORD WINAPI 函数名 (LPVOID lpParam) ,格式不正确将无法调用成功。
    Parameter向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
    CreationFlags控制线程创建的标志,可取值如下:1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程(就绪状态),直到线程被唤醒时才调用。2)0:表示创建后立即激活。3)STACK_SIZE_PARAM_IS_A_RESERVATION:dwStackSize参数指定初始的保留堆栈的大小,如果STACK_SIZE_PARAM_IS_A_RESERVATION标志未指定,dwStackSize将会设为系统预留的值
    lpThreadId保存新线程的id,是指向线程id的指针,如果为空,线程id不被返回

    第三个参数 ——启动函数:

    c LPTHREAD_START_ROUTINE lpStartAddress

    我们一般这样写:
    (LPTHREAD_START_ROUTINE) ThreadStart
    意思就是在线程启动的时候调用ThreadStart,之后他就不管了,也就是说这个函数? 就是线程主函数相当于main的意思。也就是说在这个函数中调用的类资源或函数资源 都是属于这个线程的。除了static的存储类。

    倒数第二个参数——运行参数。

    这是实际上是一个bool类型的值,用于标示是否在创建线程后立刻执行,如果为true,也就是0,那么就会立刻执行,否则将会挂起,等待启动

  4. 返回值
    还有我要说一下HANDLE这个类型,它其实是一个指针,也是CreateThread的返回值。也就是一个线程句柄,用于标示一个线程。当然,其他对于线程的操作都需要使用这个指针。

    函数成功,返回线程句柄,否则返回NULL。如果线程创建失败,可通过GetLastError函数获得错误信息。

  5. 注:
    5.1 如果线程函数return,返回值会隐式调用ExitThread函数,可以使用GetExitCodeThread函数获得该线程函数的返回值。
    5.2 使用CreateThread创建的线程具有THREAD_PRIORITY_NORMAL线程优先级。可以使用GetThreadPriority和SetThreadPriority函数获取和设置线程优先级值。
    5.3 当一个线程结束时,这个线程的对象将获得有信号状态,使得任何等待这个对象的线程都能够成功并继续执行下去。
    5.4 系统中的线程对象一直存活到线程结束,并且所有指向它的句柄都需要通过调用CloseHandle关闭。
    5.5 如果一个线程调用了CRT,应该使用_beginthreadex 和_endthreadex(需要使用多线程版的CRT)。

2.1.3 线程状态切换

1. 启动线程

如果调用这个函数,将会启动HANDLE参数所代表的线程.

DWORD ResumeThread(HANDLE hThread); //启动线程
//说明:DWORD是一个数值,代表句柄,无需关注;
//参数表示要启动的线程的句柄,也就是刚才介绍的由CreateThread返回的HANDLE
2. 挂起线程

下面我们看看如何挂起线程,使线程进入挂起状态:

DWORD SuspendThread(HANDLE hThread); //挂起线程
  //说明:DWORD是一个整数值,代表一个句柄,无需过分关注
  //参数:一个HANDLE线程指针,由CreateThread创建
3. 停止线程

挂起线程后可以进行释放以便停止线程:

delete HANDLE //释放指针资源
//说明:HANDLE是一个HANDLE型指针,代表释放一个线程的资源,使线程死亡

实际上,停止一个线程还有一种方法——强行停止,但是已经不建议使用现在都是使用挂起+delete的方法,因为使用强行停止会有很多的安全问题,但也是一个功能,所以在这里为大家介绍一下:

	BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode); //强行停止线程
	//说明:返回值代表是否成功
	//参数:HANDLE指针代表需要结束的线程,DWORD数值代表该线程的退出值
	//功能:在任何位置结束任何线程
4. 等待状态

挂起线程可能为了等待重要操作然后再执行线程,以下函数将解除线程挂起状态,使线程进入等待状态:

DWORD ResumeThread(HANDLE hThread); //使线程脱离挂起状态
//说明:返回值也是句柄
//参数:HANDLE类型指针,表示要继续的线程,或刚创建而没有启动的线程
//注意:如果对等待状态下的线程使用本函数,可能会抛出异常或无效果,具体请见MSDN

2.1.4 简单的例程

//多线程抢占输出
#include <iostream>
#include <windows.h>

using namespace std;

void ThreadUser() { //线程入口
	cout << "子线程开始" << endl;
	for (int i = 0; i < 10; i++) { //抢占循环
		cout << "子线程第" << i << "次循环抢占;" << endl; //输出信息
		Sleep(100); //抢占延时
	}
	cout << "子线程结束" << endl;
}

int main() {
	cout << "主线程开始" << endl;
	HANDLE h; //线程句柄
	h=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadUser, NULL, 1, 0); //创建子线程
	ResumeThread(h);  //启动子线程
	for (int i = 0; i < 10; i++) { //抢占循环
		cout << "主线程第" << i << "次循环抢占;" << endl; //输出信息
		Sleep(100); //抢占延时
	}
	Sleep(1000); //等待子线程
	CloseHandle(h); 
	cout << "主线程结束" << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
学习:

原博主写的代码有错误,做点小更改。
https://www.cnblogs.com/zhangyutong926/p/3652632.html

#include <Windows.h>
#include <iostream>
using namespace std;

typedef struct  _STRUCT_DATA_
{
	int id; //用于标识出票id
	int tickets;
}_DATA, *_pDATA;

DWORD WINAPI Fun1(LPVOID lpParam);
DWORD WINAPI Fun2(LPVOID lpParam);

void main()
{
	HANDLE hThread1;
	HANDLE hThread2;

	_DATA stru_data;
	stru_data.id = 0;
	stru_data.tickets = 20;

	hThread1 = CreateThread(NULL, 0, Fun1, &stru_data, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2, &stru_data, 0, NULL);

	CloseHandle(hThread1);
	CloseHandle(hThread2);

	Sleep(4000);
}

DWORD WINAPI Fun1(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		if (data->tickets > 0)
		{

			Sleep(1);
			cout << "fun1: " << data->id++;
			cout << "  ···thread  1: sell ticket: " << data->tickets-- << endl;
		}
		else
			break;
	}
	return 0;
}

DWORD WINAPI Fun2(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun2: " << data->id++ ;
			cout << "   ***thread  2 :sell ticket: " << data->tickets-- << endl;
		}
		else
			break;
	}
	return 0;
}

在这里插入图片描述





2.2 创建线程方式2——_beginthread

并不是Windows标准API,创建线程函数,该函底层调用CreateThread。

2.2.1 函数说明

  1. 头文件

    #include <process.h>
    
  2. 函数原型

    unsigned long _beginthread(  
        void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式 
        unsigned stack_size, //是线程堆栈大小,一般默认为0 
        void *arglist //向线程传递的参数,一般为结构体
     );
    
    typedef unsigned int uintptr_t;
    typedef unsigned (__stdcall* _beginthreadex_proc_type)(void*);
    
    uintptr_t  _beginthreadex(
        void*      _Security,  //线程安全属性
        unsigned   _StackSize, //线程堆栈大小
        _beginthreadex_proc_type _StartAddress, //线程函数地址
        void*      _ArgList,  //传递给线程函数的地址
        unsigned   _InitFlag, //指定线程是否立即启动
        unsigned*  _ThrdAddr  //存储线程id号
        );
    

2.2.2 安全属性

SECURITY_ATTRIBUTES结构包含一个对象的安全描述符,并指定检索到指定这个结构的句柄是否是可继承的。这个结构为很多函数创建对象时提供安全性设置。如:CreateFile,CreatePipe,CreateProcess,RegCreateKeyEx,RegSaveKeyEx

typedef struct _SECURITY_ATTRIBUTES {
    DWORD nLength;               //结构体的大小,可用SIZEOF取得 
    LPVOID lpSecurityDescriptor; //安全描述符
    BOOL bInheritHandle;         //安全描述的对象能否被新创建的进程继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

2.2.3 线程终止

_endthreadex()和_endthrea()显示式结束一个线程。然而,当线程函数返回时,_endthread和_endthreadex 被自动调用。endthread和_endthreadex的调用有助于确保分配给线程的资源的合理回收。

线程函数退出。
线程使用的堆栈被释放。
dwExitCode设置为线程函数的返回值。
递减内核中的code值,让内核的引用计数减一。

  • 结束线程调用,终止自己

    VOID WINAPI ExitThread(
      __in DWORD dwExitCode	  // 线程结束时的退出码
    );
    
  • 由当前线程终止其他线程

    BOOL WINAPI TerminateThread(
      __in_out HANDLE hThread,  // 终止的线程句柄
      __in DWORD dwExitCode     // 退出码
    );
    
  • 释放线程空间、释放线程TLS空间、调用ExiteThread结束线程

    void _endthread(void); 	
    // retval:设定的线程结束码,与ExiteThread函数的参数功能一样,
    //其实这个函数释放线程TLS空间,再调用ExiteThread函数,但没有释放线程空间。
    void _endthreadex(unsigned retval);	
    

    可以显示的调用这两个函数来结束线程。系统从线程启动函数返回时,也会自动调用相应的结束 线程函数,收回分配给线程的资源。

2.2.4 _beginthread()和_beginthreadex()的区别

两组函数都是用来创建和结束线程的。这两对函数的不同点如下:

  1. 从形式上开,_beginthreadex()更像CreateThread()。_beginthreadex()比_beginthread()多3个参数:intiflag,security和threadaddr。
  2. 两种创建方式的线程函数不同。_beginthreadex()的线程函数必须调用_stdcall调用方式,而且必须返回一个unsigned int型的退出码。
  3. _beginthreadex()在创建线程失败时返回0,而_beginthread()在创建线程失败时返回-1。这一点是在检查返回结果是必须注意的。
  4. 如果是调用_beginthread()创建线程,并相应地调用_endthread()结束线程时,系统自动关闭线程句柄;而调用_beginthreadx()创建线程,并相应地调用_endthreadx()结束线程时,系统不能自动关闭线程句柄。因此调用_beginthreadx()创建线程还需程序员自己关闭线程句柄,以清除线程的地址空间。

原文:https://blog.csdn.net/xuanyin235/article/details/77693275

2.2.5 实例

实例一:

#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;

unsigned int WINAPI ThreadProFunc(void *pParam);
int main(int argc, char **argv)
{
	HANDLE hThread;
	unsigned int threadId;
	hThread = (HANDLE)_beginthreadex(NULL, NULL, ThreadProFunc, NULL, 0, &threadId);
	for (int i = 0; i < 100; i++) {
		cout << "nihao" << endl;
	}
	CloseHandle(hThread);	//关闭线程句柄
	system("pause");
	return 0;
}

unsigned int WINAPI ThreadProFunc(void *pParam)
{
	for (int i = 0; i < 100; i++) {
		cout<<"hello\n";
	}
	return 0;
}

枪战模式
在这里插入图片描述
实例二:

#include <Windows.h>
#include <process.h>
#include <iostream>
using namespace std;

typedef struct  _STRUCT_DATA_
{
	int id; //用于标识出票id
	int tickets;
}_DATA, *_pDATA;

//CRITICAL_SECTION g_cs;
unsigned __stdcall Fun1(LPVOID lpParam);
unsigned __stdcall Fun2(LPVOID lpParam);

void main()
{
	HANDLE hThread[2] = { NULL,NULL };
	unsigned threadid[2] = { 0 };
	_DATA stru_data;

	stru_data.id = 0;
	stru_data.tickets = 200;

	hThread[0] = (HANDLE)_beginthreadex(NULL, 0, Fun1, &stru_data, 0, &threadid[0]);
	hThread[1] = (HANDLE)_beginthreadex(NULL, 0, Fun2, &stru_data, 0, &threadid[1]);

	//InitializeCriticalSection(&g_cs);
	Sleep(4000);
	//LeaveCriticalSection(&g_cs);

}

unsigned __stdcall Fun1(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		//EnterCriticalSection(&g_cs);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun1: " << data->id++ ;
			cout << " *** thread 1 :sell ticket: " << data->tickets-- << endl;
			//LeaveCriticalSection(&g_cs);
		}
		else
		{
			//LeaveCriticalSection(&g_cs);
			break;
		}
	}
	return 0;
}

unsigned __stdcall Fun2(LPVOID lpParam)
{
	_pDATA data = (_pDATA)lpParam;
	while (TRUE)
	{
		//EnterCriticalSection(&g_cs);
		if (data->tickets > 0)
		{
			Sleep(1);
			cout << "fun2: " << data->id++ ;
			cout << " === thread 2:sell ticket: " << data->tickets-- << endl;
		//	LeaveCriticalSection(&g_cs);
		}
		else
		{
			//LeaveCriticalSection(&g_cs);
			break;
		}
	}
	return 0;
}

在这里插入图片描述

总结:
https://blog.csdn.net/youshijian99/article/details/79679783
https://blog.csdn.net/xuanyin235/article/details/77693275






2.3 创建线程方式3——AfxBeginThread

MFC是微软的VC开发集成环境中提供给程序员的基础函数库,它用类库的方式将Win32 API进行封装,以类的方式提供给开发者。在VC++附带的MFC类库中,提供了对多线程编程的支持,基本原理与基于Win32 API的设计一致,但由于MFC对同步对象做了封装,因此实现起来更加方便,避免了对象句柄管理上的烦琐工作。

MFC提供了两个重载版的AfxBeginThread()函数,一个用于用户界面线程,另一个用于工作者线程。

2.3.1 用户界面线程原型

对于用户界面线程,其原型为:

CWinThread* AFXAPI AfxBeginThread(  
   CRuntimeClass* pThreadClass,             //从CWinThread派生的RUNTIME_CLASS类  
   int nPriority,                           //线程优先级,如果为0,则与创建该线程的线程相同   
   UINT nStackSize,                         //线程的堆栈大小,如果为0,则与创建该线程的线程相同   
   DWORD dwCreateFlags,	                    //创建标识,如果是CREATE_SUSPENDED,表示悬挂状态,否则线程在创建后开始线程的执行   
   LPSECURITY_ATTRIBUTES lpSecurityAttrs);  //线程的安全属性

2.3.2 工作者线程原型

对于工作线程,其原型为:

CWinThread* AfxBeginThread(   
  AFX_THREADPROC pfnThreadProc,                //线程的入口函数,声明:UNIT MyThreadFunction(LPVOID pParam)  
  LPVOID lParam,                               //传入线程的参数,注意它的类型为LPVOID,所以我们可以传递一个结构体入线程   
  int nPriority = THREAD_PRIORITY_NORMAL,      //线程优先级,一般设置为0,让它和主线程具有共同的优先级    
  UINT nStackSize = 0,	                       //指定新创建的线程的栈的大小.如果为 0,新创建的线程具有和主线程一样的大小的栈   
  DWORD dwCreateFlags = 0,                     //创建标识,如果是CREATE_SUSPENDED,表示悬挂状态,否则线程在创建后开始线程的执行   
  LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //线程的安全属性   
  );
 //用于创建工作者线程 
 //返回值:成功时返回一个指向新线程的线程对象的指针,否则为NULL

MFC应用程序中,可以利用如下代码实现:

CWinThread *thread1 = AfxBeginThread((AFX_THREADPROC)Fun1, &stru_data);

此外还是用到:
SuspendThread()函数用于暂停线程;
ResumeThread()函数用于恢复线程。

2.3.3 结束线程

结束线程的两种方式:

  1. 这是最简单的方式,也就是让线程函数执行完成,此时线程正常结束.它会返回一个值,一般0是成功结束,当然你可以定义自己的认为合适的值来代表线程成功执行.在线程内调用AfxEndThread将会直接结束线程,此时线程的一切资源都会被回收.注意在线程中使用了CString类,则不能用AfxEndThread来进行结束线程,会有内存泄漏,只有当程序结束时,会在输出窗口有提示多少byte泄漏了。因为Cstring的回收有其自己的机制。建议在AfxEndThread之前先进行return。
  2. 如果你想让另一个线程B来结束线程A,那么,你就需要在这两个线程中传递信息.不管是工作者线程还是界面线程,如果你想在线程结束后得到它的结果,那么你可以调用:::GetExitCodeThread函数。

2.3.4 举例

CWinThread* mythread;
//死循环函数
UINT EndlessLoop (LPVOID lpParam)
{
	int i = 1;
	for (;;)
		i += i;
	return 0;
}
void CSingleThreadDlg::OnBnClickedStartthread()
{
	//启动线程函数
	mythread = AfxBeginThread(
		EndlessLoop,//即上面定义的函数
		NULL
		);
}
void CSingleThreadDlg::OnBnClickedPausethread()
{
	// TODO:  在此添加控件通知处理程序代码
	mythread->SuspendThread();//挂起线程
}
void CSingleThreadDlg::OnBnClickedresumethread()
{
	// TODO:  在此添加控件通知处理程序代码
	mythread->ResumeThread();//恢复线程
}







2.4 三者区别

CreateThreadCreateThreadWindows API函数,提供操作系统级别操作,不用于MFC及RTL函数中。一般不建议调用此函数。CreateThread是Windows的API函数,提供操作系统级别的创建线程的操作。_beginthread(及_beginthreadex)与AfxBeginThread的底层实现都调用了CreateThread函数。
_beginthread/beginthreadex_beginthread/beginthreadex函数在实现过程中都调用了CreateThread函数,但都在调用前做了很多初始化工作,在调用后又做了很多检查工作,这使得线程的创建更完整。结束线程的_endthread()函数和_endthreadex()函数在实现的过程中调用了Exithread()函数,但他们都做了更多的善后工作,其中的_endthread()函数甚至还包揽了句柄的删除工作。
AfxBeginThreadAfxBeginThread是MFC创建线程函数,首先创建了相应的CWinThread对象,然后调用CWinThread::CreateThread,CWinThread::CreateThread中,完成了对线程对象的初始化工作。

CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。

实际应用建议

  1. 不要在一个MFC程序中使用_beginthreadex()或CreateThread()。这句话的意思是由于AfxBeginThread()是MFC封装的启动线程的函数,里面包含了很多和MFC相关的启动信息,而且封装了一些常用的操作,使用起来也比较简便。而用另外两个函数就需要程序员对类型,安全性检查进行更多的思考!

  2. MFC中用_beginthreadex()函数应该是最佳选择,因为_beginthreadex()函数是CRun-timeLibrary中的函数,函数的参数和数据类型都是CRun-timeLibrary中的类型,这样在启动线程时就不需要进行Windows数据类型和CRun-timeLibrary中的数据类型之间的转化。减低了线程启动时的资源消耗和时间的消耗!

请牢记:MFC中,决不应该调用CreateThread。相反,应该使用Visual C++运行库函数_beginthreadex

[c++11]多线程编程(一)——初识
[c++11]多线程编程(二)——理解线程类的构造函数

学习总结:
xuanyin235
蒋鹿丸

Window Form 应用程序是基于 Windows 操作系统 GUI 界面的应用程序,它使用了多线程技术。在 Window Form 应用程序中,主线程通常用于处理用户界面(UI)操作,而其他线程则用于执行后台任务,例如进行网络通信、文件处理等。这方式可以避免长时间的操作阻塞 UI 线程,让应用程序更加流畅。 在 Window Form 应用程序中,创建线程方式与标准的 C# 程序一样,可以使用 Thread 类或 ThreadPool 类来创建线程。但需要注意的是,在 UI 线程中访问 UI 控件是不安全的,必须使用 Invoke 或 BeginInvoke 方法来让 UI 线程更新 UI 控件。 例如,以下代码演示了如何在 Window Form 应用程序中创建一个后台线程来执行耗时的任务: ``` private void btnStart_Click(object sender, EventArgs e) { // 创建后台线程 Thread thread = new Thread(new ThreadStart(DoWork)); thread.Start(); } private void DoWork() { // 执行耗时的任务 // ... // 更新 UI 控件(需要使用 Invoke 或 BeginInvoke 方法) this.Invoke(new Action(() => { // 更新 UI 控件 // ... })); } ``` 在上述代码中,btnStart_Click 方法是 UI 线程中的事件处理程序,当用户点击按钮时会创建一个后台线程来执行任务。DoWork 方法是后台线程的入口点,它执行耗时的任务,并使用 Invoke 方法来更新 UI 控件。这样可以保证 UI 线程不会被阻塞,同时也可以让用户获得更好的体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值