C++实现经典同步问题(生产者消费者、读者写者、哲学家进餐、吸烟者问题)

一、生产者 - 消费者问题

  • 环境:windows

问题描述:
一组生产者进程和一组消费者进程共享一个初始为空、大小为〃的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或一个消费者从中取出消息。


代码:

#include <Windows.h>

#include <iostream>
#include <stdlib.h>

typedef HANDLE Semaphore;
#define P(S) WaitForSingleObject(S, INFINITE)
#define V(S) ReleaseSemaphore(S, 1, NULL)

int produceId = 1000;	//生产的的产品ID
int consumeId;			//消费的产品ID
int buf[10];			//用于存放缓存中的数据
int BUFSIZE = 10;	//缓存大小
int in = 0;				//用于表示生产者放入缓存的下表索引
int out = 0;			//用于表示消费者从缓存中那组的数据的下标索引

//初始化empty互斥量,初值为10,表示空的缓存数
Semaphore empty = CreateSemaphore(
	NULL,
	10,
	10,
	NULL
);

//初始化互斥量,用户互斥访问临界资源
Semaphore mutex = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

//初始化full互斥量,初值为0,用于表示缓存中放了多少份数据
Semaphore full = CreateSemaphore(
	NULL,
	0,
	10,
	NULL
);

DWORD WINAPI Producer(LPVOID);	//生产者线程函数
DWORD WINAPI Consumer(LPVOID);	//消费者线程函数

VOID Produce();			//生产者生产数据 的函数
VOID Consume();			//消费者消费数据的函数 

VOID printbuf();		//用于打印缓存中的数据的函数


int main()
{
    
	HANDLE produce;
	HANDLE consumer1, consumer2, consumer3;

	produce = CreateThread(
		NULL,
		0,
		Producer,
		NULL,
		0,
		NULL
	);
	consumer1 = CreateThread(
		NULL,
		0,
		Consumer,
		NULL,
		0,
		NULL
	);
	consumer2 = CreateThread(
		NULL,
		0,
		Consumer,
		NULL,
		0,
		NULL
	);
	consumer3 = CreateThread(
		NULL,
		0,
		Consumer,
		NULL,
		0,
		NULL
	);
	while (1)
	{
		WaitForSingleObject(produce, INFINITE);
		WaitForSingleObject(consumer1, INFINITE);
		WaitForSingleObject(consumer2, INFINITE);
		WaitForSingleObject(consumer3, INFINITE);
	}

	return 0;
}

VOID printbuf() {
	std::cout << "缓存中的数据:";
	bool hasData = false;
	for (int i = 0; i < BUFSIZE; i++) {
		if (buf[i] != 0) {
			std::cout << buf[i] << "----";
			hasData = true;
		}
	}
	if (hasData)
		std::cout << std::endl;
	else
		std::cout << "无" << std::endl;
}

VOID Produce() {
	produceId++;
	buf[in++] = produceId;
	std::cout << "我是生产者,我生产了:" << produceId << std::endl;
	printbuf();
	in %= BUFSIZE;
}

VOID Consume() {
	consumeId = buf[out];
	std::cout << "我是消费者,我消费了:" << consumeId << std::endl;
	buf[out++] = 0;
	printbuf();
	out %= BUFSIZE;
}

DWORD WINAPI Producer(LPVOID) {
	while (1) {
		P(empty);		//有无空缓存
		P(mutex);		//互斥夹紧临界资源
		Produce();		//生产数据
		Sleep(1000);
		V(mutex);		//互斥夹紧临界资源
		V(full);		//full值+1
	}
	return 0;
}

DWORD WINAPI Consumer(LPVOID) {
	while (1) {
		P(full);		//缓存中有无数据
		P(mutex);		//互斥夹紧临界资源
		Consume();		//消费数据
		Sleep(1000);
		V(mutex);		//互斥夹紧临界资源
		V(empty);		//空缓存数加1
	}
	return 0;
}

运行效果:
在这里插入图片描述

再来看一个复杂一点的生产者 - 消费者问题:
桌子上有一个盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时, 爸爸或妈妈才可向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取岀。


代码:

#include <Windows.h>

#include <iostream>
#include <stdlib.h>

typedef HANDLE Semaphore;
#define P(S) WaitForSingleObject(S, INFINITE)
#define V(S) ReleaseSemaphore(S, 1, NULL)

#define _NULL 0
#define _APPLE 1
#define _ORANGE 2
#define _FATHER 1
#define _MOTHER 2
#define _SON 3
#define _DAUGHTER 4

int what = 0;			//用于表示是哪种水果
int who = 0;			//用于表示是哪位放了或者吃了水果

//初始化apple互斥量,初值为0,用于表示盘中是否放了苹果
Semaphore apple = CreateSemaphore(
	NULL,
	0,
	1,
	NULL
);

//初始化orange互斥量,初值为0,用于表示盘中是否放了橘子
Semaphore orange = CreateSemaphore(
	NULL,
	0,
	1,
	NULL
);

//初始化plate互斥量,用于表示盘中是否有水果
Semaphore plate = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

DWORD WINAPI Father(LPVOID);	//父亲线程函数
DWORD WINAPI Mother(LPVOID);	//母亲线程函数
DWORD WINAPI Son(LPVOID);		//儿子线程函数
DWORD WINAPI Daughter(LPVOID);	//女儿线程函数

VOID Put(int);			//父母放水果的函数
VOID Get();				//儿女吃水果的函数 

VOID PrintPlate();		//用于打印的函数


int main()
{

	HANDLE father, mother, son, daughter;;

	father = CreateThread(
		NULL,
		0,
		Father,
		NULL,
		0,
		NULL
	);
	mother = CreateThread(
		NULL,
		0,
		Mother,
		NULL,
		0,
		NULL
	);
	son = CreateThread(
		NULL,
		0,
		Son,
		NULL,
		0,
		NULL
	);
	daughter = CreateThread(
		NULL,
		0,
		Daughter,
		NULL,
		0,
		NULL
	);
	while (1)
	{
		WaitForSingleObject(father, INFINITE);
		WaitForSingleObject(mother, INFINITE);
		WaitForSingleObject(son, INFINITE);
		WaitForSingleObject(daughter, INFINITE);
	}

	return 0;
}

VOID PrintPlate() {
	std::string _who[4] = {
		"爸爸",
		"妈妈",
		"儿子",
		"女儿"
	};
	std::string _what[2] = {
		"苹果",
		"橘子"
	};
	if (who == _FATHER || who == _MOTHER) {
		std::cout << "我是:" << _who[who - 1].c_str()
			<< ";  我放了【" << _what[what - 1].c_str() << "】在盘子里...."
			<< std::endl;
	}
	else {
		std::cout << "我是:" << _who[who - 1].c_str()
			<< ";  我从盘子里把【" << _what[what - 1].c_str() << "】拿走了...."
			<< std::endl;
	}
}

VOID Put(int id) {
	what = (id == _APPLE) ? _APPLE : _ORANGE;
	who = (id == _FATHER) ? _FATHER : _MOTHER;
	PrintPlate();
}

VOID Get() {
	who = (what == _APPLE) ? _DAUGHTER : _SON;
	PrintPlate();
	what = _NULL;
}

DWORD WINAPI Father(LPVOID) {
	while (1) {
		P(plate);				//看盘子是否为空
		Put(_APPLE);			//父亲放入苹果
		Sleep(1000);
		V(apple);				//唤醒女儿线程
	}
	return 0;
}

DWORD WINAPI Mother(LPVOID) {
	while (1) {
		P(plate);				//看盘子是否为空
		Put(_ORANGE);			//母亲放入橘子
		Sleep(1000);
		V(orange);				//唤醒儿子线程
	}
	return 0;
}

DWORD WINAPI Son(LPVOID) {
	while (1) {
		P(orange);				//看盘子里是否有橘子
		Get();					//拿走橘子
		Sleep(1000);
		V(plate);				//盘子变空,唤醒父母
	}
	return 0;
}

DWORD WINAPI Daughter(LPVOID) {
	while (1) {
		P(apple);				//看盘子里是否有苹果
		Get();					//拿走苹果
		Sleep(1000);
		V(plate);				//盘子变空,唤醒父母
	}
	return 0;
}

运行效果:
在这里插入图片描述

二、读者 - 写者问题

问题描述:
有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。


读优先(读优先可能会导致写者线程饿死)的代码:

//读者优先,读者优先可能导致写进程饿死
#include <Windows.h>

#include <iostream>
#include <stdlib.h>

typedef HANDLE Semaphore;
#define P(S) WaitForSingleObject(S, INFINITE)
#define V(S) ReleaseSemaphore(S, 1, NULL)

int count = 0;			//用于记录读者数量

//初始化mutex互斥量,用于保证互斥访问count
Semaphore mutex = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

//初始化rw互斥量,用于保证读写进程 互斥地访问文件
Semaphore rw = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

DWORD WINAPI Reader(LPVOID);	//读者线程函数
DWORD WINAPI Writer(LPVOID);	//写者线程函数

VOID Read();				//读函数
VOID Write();				//写函数


int main()
{

	HANDLE reader[5];
	HANDLE writer[5];

	for (int i = 0; i < 5; i++) {
		reader[i] = CreateThread(
			NULL,
			0,
			Reader,
			NULL,
			0,
			NULL
		);
	}

	for (int i = 0; i < 5; i++) {
		writer[i] = CreateThread(
			NULL,
			0,
			Writer,
			NULL,
			0,
			NULL
		);
	}

	while (1)
	{
		for (int i = 0; i < 20; i++)
			WaitForSingleObject(reader[i], INFINITE);
		
		for (int i = 0; i < 5; i++)
			WaitForSingleObject(writer[i], INFINITE);
	}

	return 0;
}

VOID Read() {
	std::cout << "我是读线程,我正在读......." << std::endl;
}

VOID Write() {
	std::cout << "我是写线程,我正在写---------------" << std::endl;
}

DWORD WINAPI Reader(LPVOID) {
	while (1) {
		P(mutex);			//互斥访问count变量
		if (count == 0)		//当第一个读线程读共享文件时
			P(rw);			//阻塞写线程
		count++;			//读线程数加 1
		Sleep(1000);
		V(mutex);			//互斥访问count变量
		Read();				//读取共享文件
		P(mutex);			//互斥访问count变量
		count--;			//读线程数减 1
		if (count == 0)		//当当前没有读线程工作时
			V(rw);			//释放rw信号量,通知写线程可以工作了
		V(mutex);			//互斥访问count变量
	}
	return 0;
}

DWORD WINAPI Writer(LPVOID) {
	while (1) {
		P(rw);				//保证互斥访问共享文件
		Write();			//写共享文件
		Sleep(300);
		V(rw);				//保证互斥访问共享文件
	}
	return 0;
}

运行效果:
在这里插入图片描述
注:可以看到,由于读者线程优先,导致了写者线程饿死

写优先,或者叫读写平衡的代码:

//写者优先,或者叫读写平衡
#include <Windows.h>

#include <iostream>
#include <stdlib.h>

typedef HANDLE Semaphore;
#define P(S) WaitForSingleObject(S, INFINITE)
#define V(S) ReleaseSemaphore(S, 1, NULL)

int count = 0;			//用于记录读者数量

//初始化mutex互斥量,用于保证互斥访问count
Semaphore mutex = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

//初始化rw互斥量,用于保证读写进程 互斥地访问文件
Semaphore rw = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

//初始化w互斥量,用于实现“写优先”
Semaphore w = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

DWORD WINAPI Reader(LPVOID);	//读者线程函数
DWORD WINAPI Writer(LPVOID);	//写者线程函数

VOID Read();				//读函数
VOID Write();				//写函数


int main()
{

	HANDLE reader[5];
	HANDLE writer[5];

	for (int i = 0; i < 5; i++) {
		reader[i] = CreateThread(
			NULL,
			0,
			Reader,
			NULL,
			0,
			NULL
		);
	}

	for (int i = 0; i < 5; i++) {
		writer[i] = CreateThread(
			NULL,
			0,
			Writer,
			NULL,
			0,
			NULL
		);
	}

	while (1)
	{
		for (int i = 0; i < 20; i++)
			WaitForSingleObject(reader[i], INFINITE);

		for (int i = 0; i < 5; i++)
			WaitForSingleObject(writer[i], INFINITE);
	}

	return 0;
}

VOID Read() {
	std::cout << "我是读线程,我正在读......." << std::endl;
}

VOID Write() {
	std::cout << "我是写线程,我正在写---------------" << std::endl;
}

DWORD WINAPI Reader(LPVOID) {
	while (1) {
		P(w);				//保证在无写线程时进入
		P(mutex);			//互斥访问count变量
		if (count == 0)		//当第一个读线程读共享文件时
			P(rw);			//阻塞写线程
		count++;			//读线程数加 1
		Sleep(1000);
		V(mutex);			//互斥访问count变量
		V(w);				//释放w,保证写线程优先
		Read();				//读取共享文件
		P(mutex);			//互斥访问count变量
		count--;			//读线程数减 1
		if (count == 0)		//当当前没有读线程工作时
			V(rw);			//释放rw信号量,通知写线程可以工作了
		V(mutex);			//互斥访问count变量
	}
	return 0;
}

DWORD WINAPI Writer(LPVOID) {
	while (1) {
		P(w);				//在无写线程时进入
		P(rw);				//保证互斥访问共享文件
		Write();			//写共享文件
		Sleep(300);
		V(rw);				//保证互斥访问共享文件
		V(w);				//写完毕,释放
	}
	return 0;
}

运行效果:
在这里插入图片描述
注:可以看到写者线程被调度了

三、哲学家进餐问题

问题描述:
一张圆桌边上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭,如图2.12所示。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,进餐完毕后,放下筷子继续思考。

代码:

#include <Windows.h>

#include <iostream>
#include <stdlib.h>

typedef HANDLE Semaphore;
#define P(S) WaitForSingleObject(S, INFINITE)
#define V(S) ReleaseSemaphore(S, 1, NULL)

//筷子互斥量,初值为1,表示是否有筷子
Semaphore chopstick[5];

//初始化mutex互斥量,用于保证互斥访问chopstick
Semaphore mutex = CreateSemaphore(
	NULL,
	1,
	1,
	NULL
);

DWORD WINAPI Philosopher(LPVOID);	//哲学家线程函数

VOID InitSemaphore();

VOID Eat(int);							//哲学家进餐函数
VOID Think(int);						//哲学家思考函数

int main()
{
	InitSemaphore();

	HANDLE philosopher[5];
	int id[5];
	for (int i = 0; i < 5; i++)
		id[i] = i;

	for (int i = 0; i < 5; i++) {
		philosopher[i] = CreateThread(
			NULL,
			0,
			Philosopher,
			(LPVOID)&(id[i]),
			0,
			NULL
		);
	}

	while (1)
	{
		for (int i = 0; i < 5; i++)
			WaitForSingleObject(philosopher[i], INFINITE);
	}

	return 0;
}

VOID InitSemaphore() {
	for (int i = 0; i < 5; i++) {
		chopstick[i] = CreateSemaphore(
			NULL,
			1,
			1,
			NULL
		);
	}
}

VOID Eat(int i) {
	std::cout << "哲学家【" << i + 1 << "】正在进餐..." << std::endl;
}

VOID Think(int i) {
	std::cout << "哲学家【" << i + 1 << "】正在思考........" << std::endl;
}

DWORD WINAPI Philosopher(LPVOID i) {
	while (1) {
		P(mutex);								//互斥访问筷子
		P(chopstick[*((int *)i)]);				//拿起左手边的筷子
		P(chopstick[((*((int *)i)) + 1) % 5]);	//拿起右手边的筷子
		V(mutex);								//互斥访问筷子
		Eat(*(int *)i);							//进餐
		Sleep(1000);
		V(chopstick[*((int *)i)]);				//放下左手边的筷子
		V(chopstick[((*((int *)i) + 1)) % 5]);	//放下右手边的筷子
		Think(*(int *)i);						//思考.....在这里可能发生竟态条件
	}
	return 0;
}

运行效果:
在这里插入图片描述
注:以上思考的输出出现混乱的原因:

在一个哲学家线程调用到 Think 函数的时候由于没有信号量的保护,因此可能发生竟态条件,由于线程异步地执行,可能在某个线程运行 Think 的时候发生了调度。

四、吸烟者问题

问题描述:
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种材料放到桌上,如此重复(让三个抽烟者轮流地抽烟)。

代码:

#include <Windows.h>

#include <iostream>
#include <stdlib.h>

typedef HANDLE Semaphore;
#define P(S) WaitForSingleObject(S, INFINITE)
#define V(S) ReleaseSemaphore(S, 1, NULL)

#define YanCaoAndZhi 1
#define YanCaoAndJiaoShui 2
#define ZhiAndJiaoShui 3

int num;			//供应者供应用的 随机数
int what;			//表示桌上有什么材料

//初始化offerYanCao互斥量,用于表示供应烟草和纸
Semaphore offerYanCaoAndZhi = CreateSemaphore(
	NULL,
	0,
	1,
	NULL
);

//初始化offerZhi互斥量,用于表示供应烟草和胶水
Semaphore offerYanCaoAndJiaoShui = CreateSemaphore(
	NULL,
	0,
	1,
	NULL
);

//初始化offerJiaoShui互斥量,用于表示供应纸和胶水
Semaphore offerZhiAndJiaoShui = CreateSemaphore(
	NULL,
	0,
	1,
	NULL
);

//初始化finish互斥量,用于表示吸烟完成
Semaphore finish = CreateSemaphore(
	NULL,
	0,
	1,
	NULL
);

DWORD WINAPI Producer(LPVOID);	//供应者线程函数
DWORD WINAPI Smoker1(LPVOID);	//1号吸烟者,拥有烟草
DWORD WINAPI Smoker2(LPVOID);	//2号吸烟者,拥有纸
DWORD WINAPI Smoker3(LPVOID);	//3号吸烟者,拥有胶水

VOID Put(int);					//供应者放材料
VOID Smoke();				//吸烟者吸烟


int main()
{

	HANDLE producer;
	HANDLE smoker1, smoker2, smoker3;

	
	producer = CreateThread(
		NULL,
		0,
		Producer,
		NULL,
		0,
		NULL
	);

	smoker1 = CreateThread(
		NULL,
		0,
		Smoker1,
		NULL,
		0,
		NULL
	);

	smoker2 = CreateThread(
		NULL,
		0,
		Smoker2,
		NULL,
		0,
		NULL
	);

	smoker3 = CreateThread(
		NULL,
		0,
		Smoker3,
		NULL,
		0,
		NULL
	);

	while (1)
	{
		WaitForSingleObject(producer, INFINITE);
		WaitForSingleObject(smoker1, INFINITE);
		WaitForSingleObject(smoker2, INFINITE);
		WaitForSingleObject(smoker3, INFINITE);
	}

	return 0;
}

VOID Put(int i) {
	if (i == YanCaoAndZhi) {
		what = YanCaoAndZhi;
		std::cout << "我是供应者,我现在要把【烟草】和【纸】放到桌上" << std::endl;
	}
	else if (i == YanCaoAndJiaoShui) {
		what = YanCaoAndJiaoShui;
		std::cout << "我是供应者,我现在要把【烟草】和【胶水】放到桌上" << std::endl;
	}
	else {
		what = ZhiAndJiaoShui;
		std::cout << "我是供应者,我现在要把【纸】和【胶水】放到桌上" << std::endl;
	}
}

VOID Smoke() {
	if (what == YanCaoAndZhi) {
		std::cout << "我是吸烟者【3】号,我拥有【胶水】,我现在从桌上拿走【烟草】和【纸】,我开始吸烟了"
			<< std::endl;
	}	
	else if (what == YanCaoAndJiaoShui) {
		std::cout << "我是吸烟者【2】号,我拥有【纸】,我现在从桌上拿走【烟草】和【胶水】,我开始吸烟了"
			<< std::endl;
	}
	else {
		std::cout << "我是吸烟者【1】号,我拥有【烟草】,我现在从桌上拿走【纸】和【胶水】,我开始吸烟了"
			<< std::endl;
	}
	what = 0;
}

DWORD WINAPI Producer(LPVOID) {
	while (1) {
		num++;							//要放入什么
		num = num % 3;					
		if (num == 0) {
			Put(YanCaoAndZhi);			//放入烟草和纸
			V(offerYanCaoAndZhi);		//唤醒拥有胶水的吸烟者
		}
		else if (num == 1) {
			Put(YanCaoAndJiaoShui);		//放入烟草和胶水
			V(offerYanCaoAndJiaoShui);	//唤醒拥有纸的吸烟者
			
		}
		else {
			Put(ZhiAndJiaoShui);		//放入纸和胶水
			V(offerZhiAndJiaoShui);		//唤醒拥有烟草的吸烟者
		}
		Sleep(1000);
		P(finish);						//是否有吸烟者吸完烟了?
	}
	return 0;
}

DWORD WINAPI Smoker1(LPVOID){			//拥有烟草的吸烟者
	while (1) {
		P(offerZhiAndJiaoShui);			//供应者是否放好了纸和胶水
		Smoke();						//拿走材料开始吸烟
		Sleep(1000);
		V(finish);
	}
	return 0;
}

DWORD WINAPI Smoker2(LPVOID) {			//拥有纸的吸烟者
	while (1) {
		P(offerYanCaoAndJiaoShui);		//供应者是否放好了烟草和胶水
		Smoke();						//拿走材料开始吸烟
		Sleep(1000);
		V(finish);
	}
	return 0;
}

DWORD WINAPI Smoker3(LPVOID) {			//拥有胶水的吸烟者
	while (1) {
		P(offerYanCaoAndZhi);			//供应者是否放好了烟草和纸
		Smoke();						//拿走 材料开始吸烟
		Sleep(1000);
		V(finish);
	}
	return 0;
}

运行效果:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一棵快乐的二叉树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值