Silverlight之各种线程的操作

Silverlight中经常会用到多线程来处理一些复杂的业务,或者是过程较长的业务,下面为大家总结了Silverlight常用的处理多线程的方法。

主要使用的对象如下:

1.System.ComponentModel.BackgroundWorker

2.System.Threading.Interlocked

3.lock关键字

4.Thread

5.ThreadPool

6.EventWaitHandle -通知其他线程是否可入的类

7.Timer

8.Monitor提供同步访问对象的机制

9.ThreadStaticAttribute - 所指定的静态变量对每个线程都是唯一的

下面一一来进行介绍和使用:

一、System.ComponentModel.BackgroundWorker,BackgroundWorker 类允许您在单独的专用线程上运行操作。耗时的操作(如下载和数据库事务)在长时间运行时可能会导致用户界面 (UI) 似乎处于停止响应状态。如果您需要能进行响应的用户界面,而且面临与这类操作相关的长时间延迟,则可以使用 BackgroundWorker 类方便地解决问题。

看下BackgroundWorker的主要属性和方法:

* WorkerSupportsCancellation - 是否支持在其他线程中取消该线程的操作
* WorkerReportsProgress - 是否可以报告操作进度
* ProgressChanged - 报告操作进度时触发的事件
* DoWork - BackgroundWorker 调用 RunWorkerAsync() 方法时触发的事件。在此执行具体操作(在此方法中不要访问UI的对象,在Progress和Completed中操作)
* RunWorkerCompleted - 操作完成/取消/出错时触发的事件

 

下面看一个完整的例子:

前台XAML代码(很简单的几个操作):

<UserControl x:Class="SilverlightApplication.Threads.BackgroundWorker"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
Width
="400" Height="300">
<StackPanel Margin="5">
<StackPanel Orientation="Horizontal" Margin="5">
<Button x:Name="btnStart" Content="开始" Margin="5" Click="btnStart_Click"></Button>
<Button x:Name="btnCancel" Content="取消" Margin="5" Click="btnCancel_Click"></Button>
</StackPanel>

<StackPanel Margin="5">
<ProgressBar Margin="5" x:Name="progress" Height="25" Maximum="100"/>
<TextBlock x:Name="tbcMsg" Margin="5"></TextBlock>
</StackPanel>
</StackPanel>
</UserControl>

页面上就是两个按钮,分别为开始和取消,一个进度条显示当前处理的业务的进度,一个TextBlock显示传递的参数值。

看下后台代码:

View Code
 public partial class BackgroundWorker : UserControl
{
/**/
/*
* 演示用 BackgroundWorker 在后台线程上执行耗时的操作
* 按“开始”键,开始在后台线程执行耗时操作,并向UI线程汇报执行进度
* 按“取消”键,终止后台线程
* BackgroundWorker 调用 UI 线程时会自动做线程同步
*/
// BackgroundWorker - 用于在单独的线程上运行操作。例如可以在非UI线程上运行耗时操作,以避免UI停止响应
System.ComponentModel.BackgroundWorker _backgroundWorker;

public BackgroundWorker()
{
InitializeComponent();
BackgroundWorkerDemo();
}

void BackgroundWorkerDemo()
{
/**/
/*
* WorkerSupportsCancellation - 是否支持在其他线程中取消该线程的操作
* WorkerReportsProgress - 是否可以报告操作进度
* ProgressChanged - 报告操作进度时触发的事件
* DoWork - BackgroundWorker 调用 RunWorkerAsync() 方法时触发的事件。在此执行具体操作
* RunWorkerCompleted - 操作完成/取消/出错时触发的事件
*/
_backgroundWorker = new System.ComponentModel.BackgroundWorker();

_backgroundWorker.WorkerSupportsCancellation = false;
_backgroundWorker.WorkerReportsProgress = true;

_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(_backgroundWorker_ProgressChanged);
_backgroundWorker.DoWork += new DoWorkEventHandler(_backgroundWorker_DoWork);
_backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_backgroundWorker_RunWorkerCompleted);
}

/// <summary>
/// 操作完成时候触发的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void _backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{

/**/
/*
* RunWorkerCompletedEventArgs.Error - DoWork 时产生的错误
* RunWorkerCompletedEventArgs.Cancelled - 后台操作是否已被取消
* RunWorkerCompletedEventArgs.Result - DoWork 的结果
*/
if (e.Error != null)
{
tbcMsg.Text += e.Error.ToString() + "\r\n";
}
else if (e.Cancelled == true)
{
tbcMsg.Text += "用户取消了操作" + "\r\n";
}
else
tbcMsg.Text += e.Result.ToString();
}

/// <summary>
/// 调用DoWork触发的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
/**/
/*
* DoWorkEventArgs.Argument - RunWorkerAsync(object argument)传递过来的参数
* DoWorkEventArgs.Cancel - 取消操作
* DoWorkEventArgs.Result - 操作的结果。将传递到 RunWorkerCompleted 所指定的方法
* BackgroundWorker.ReportProgress(int percentProgress, object userState) - 向 ProgressChanged 汇报操作的完成进度
* int percentProgress - 操作完成的百分比 1% - 100%
* object userState - 传递到 ProgressChanged 的参数
*/
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
_backgroundWorker.ReportProgress((i + 1) * 10, i);
}
e.Result = "操作完成";
}


/// <summary>
/// 进度改变事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// ProgressChangedEventArgs.ProgressPercentage - ReportProgress 传递过来的操作完成的百分比
// ProgressChangedEventArgs.UserState - ReportProgress 传递过来的参数
tbcMsg.Text = string.Format("当前进度:{0},传递的参数:{1}",e.ProgressPercentage,e.UserState);
progress.Value = e.ProgressPercentage;
}


private void btnStart_Click(object sender, RoutedEventArgs e)
{
// IsBusy - 指定的 BackgroundWorker 是否正在后台操作
// RunWorkerAsync(object argument) - 开始在后台线程执行指定的操作
// object argument - 需要传递到 DoWork 的参数
if (!_backgroundWorker.IsBusy)
_backgroundWorker.RunWorkerAsync("开始执行这个方法.....");
}

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
// CancelAsync() - 取消 BackgroundWorker 正在执行的后台操作
_backgroundWorker.CancelAsync();
}
}


代码其实很简单的,定义一个全局的BackgroundWorker对象,然后绑定ProgressChanged事件、DoWork事件以及RunWorkerCompleted事件(事件的具体作用在上边

已经提到了)。当点击开始按钮开始调用RunWorkerAsync方法,开始DoWork的操作,并为其传递参数,用于在TextBlock显示。在DoWork中也是很简单的,一个for循

环用于调用ReportProgress方法更改当前的进度,用于在ProgressChanged事件中时时更新进度条的value,同时在TextBlock中显示传递的值(在当前事件中使用ProgressChangedEventArgs.ProgressPercentage得到当前的进度,当然这个值我们是可以手动指定的,就是调用了ReportProgress方法传递的参数,使用ProgressPercentage.UserState得到传递的用户参数);当点击取消按钮则调用CancelAsync方法用于取消当前的操作。

就这样BackgroundWorker的简单使用方法就完毕了。

 

二、System.Threading.Interlocked类,为多个线程共享的变量提供原子操作。

此类的方法可以防止可能在下列情况发生的错误:计划程序在某个线程正在更新可由其他线程访问的变量时切换上下文;或者当两个线程在不同的处理器上并发执行时。 此类的成员不引发异常。

Increment 和 Decrement 方法递增或递减变量并将结果值存储在单个操作中。 在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:

  1. 将实例变量中的值加载到寄存器中。

  2. 增加或减少该值。

  3. 在实例变量中存储该值。

如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。 然后由另一个线程执行所有三个步骤。 当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。

Exchange 方法自动交换指定变量的值。 CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。 比较和交换操作按原子操作执行。

通过上边的介绍可以看出Interlocked的目的是防止线程之间对值造成的丢失。

Interlocked的主要方法:

Add(Int32, Int32)、Add(Int64, Int64),以原子操作的形式,添加两个 32或者64位位整数并用两者的和替换第一个的值。

CompareExchange(Int32, Int32, Int32),此方法有多个重载详见MSDN,比较两个双精度浮点数(多个类型)是否相等,如果相等,则替换其中一个值。

CompareExchange(Of T)(T, T, T),使用第一个参数和第三个参数进行比较,如果两个的对象是同一个实例,则使用第二个参数替换第一个参数。

Decrement(Int64),Decrement(Int32),以原子操作的形式递减指定变量的值并存储结果。

Exchange(Double, Double),此方法多个重载详见MSDN,以原子操作的形式,将 32 位有符号整数设置为指定的值并返回原始值。

Increment(Int32),Increment(Int64),以原子操作的形式递增指定变量的值并存储结果。

下面通过一个例子来看看使用方法:

public partial class Interlocked : UserControl
{
private static int i = 0;
//如果为0则说明可以执行方法,否则说明正在被占用
private static int usingResource = 0;
public Interlocked()
{
InitializeComponent();

}

private void DoWork()
{
// Interlocked - 为多个线程共享的变量提供原子级的操作(避免并发问题)
//以原子操作的形式,将双精度浮点数设置为指定的值并返回原始值。
if (0 == System.Threading.Interlocked.Exchange(ref usingResource, 1))
{
try
{
//i+1
System.Threading.Interlocked.Increment(ref i);
//i-1
System.Threading.Interlocked.Decrement(ref i);
//i+1 ,返回两者相加后的值
System.Threading.Interlocked.Add(ref i, 1);

// 如果 i 等于 10 ,则将 i 赋值为 11
System.Threading.Interlocked.CompareExchange(ref i, 11, 10);
this.Dispatcher.BeginInvoke(delegate()
{
txtMsg.Text = i.ToString();
});
System.Threading.Interlocked.Exchange(ref usingResource, 0);
}
catch (Exception)
{
throw;
}
}
else
{
this.Dispatcher.BeginInvoke(delegate()
{
txtMsg.Text = "资源正在被使用";
Thread.Sleep(2000);
});
}
}

private void btnStart_Click(object sender, RoutedEventArgs e)
{
Random rnd = new Random();
for (int index = 0; index < 10; index++)
{
Thread thread = new Thread(new ThreadStart(DoWork));
Thread.Sleep(rnd.Next(0, 1000));
thread.Start();
}
}
}

该例子也很好理解,通过创建多个Thread来调用同一个函数,定义全局变量usingResource用来标识当前的方法是否正在被处理,如果在处理则显示资源正在被使用,否则

则执行一些函数的调用,从而会避免线程之间对同一个变量的值的覆盖。


三、Lock关键字,Lock可以保证代码段不会被其他线程中断(其参数必须为一个引用类型的对象)。

View Code
public partial class ucLock : UserControl
{

// 需要被 lock 的静态变量
private static readonly object objLock = new object();
private static int i;
public ucLock()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(ucLock_Loaded);
}

void ucLock_Loaded(object sender, RoutedEventArgs e)
{
i = 0;
for (int index = 0; index < 100; index++)
{
// 开 100 个线程去操作静态变量 i
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
}
Thread.Sleep(3000);
txtMsg.Text = i.ToString();
}

private void DoWork()
{
try
{
// lock() - 确保代码块完成运行,而不会被其他线程中断。其参数必须为一个引用类型的对象
lock (objLock)
{
int j = i + 1;
Thread.Sleep(10);
i = j;
}
}
catch (Exception)
{
throw;
}
}
}

四、Thread,线程类,这个应该是真正用来处理多线程的类

直接看例子,这个对象再熟悉不过了。

public partial class ucThread : UserControl
{
public ucThread()
{
InitializeComponent();
Deom();
}

private string result = "";
void Deom()
{
/**/
/*
* Thread - 用于线程的创建和控制的类
* Name - 线程名称
* IsBackground - 是否是后台线程(对于Silverlight来说,是否是后台线程没区别)
* Start(object parameter) - 启动后台线程
* object parameter - 为后台线程传递的参数
* IsAlive - 线程是否在执行中
* ManagedThreadId - 当前托管线程的唯一标识符
* ThreadState - 指定线程的状态 [System.Threading.ThreadState枚举]
* Abort() - 终止线程
*/

// DoWork 是后台线程所执行的方法(此处省略掉了委托类型)
// ThreadStart 委托不可以带参数, ParameterizedThreadStart 委托可以带参数
Thread thread = new Thread(DoWork);
thread.Name = "ThreadDeom";
thread.IsBackground = true;
//传递参数,使主线程sleep一秒钟
thread.Start(6000);

//得到线程的信息
result += thread.IsAlive + "\r\n";
result += thread.ManagedThreadId + "\r\n";
result += thread.Name + "\r\n";
result += thread.ThreadState + "\r\n";

// thread.Join(); 阻塞调用线程(本例为主线程),直到指定线程(本例为thread)执行完毕(或者超出了指定的时间)为止
// 阻塞调用线程(本例为主线程)
// 如果指定线程执行完毕则继续(本例为thread执行完毕则继续)
// 如果指定线程运行的时间超过指定时间则继续(本例为thread执行时间如果超过5秒则继续)
// 返回值为在指定时间内指定线程是否执行完毕(本例中thread的执行时间为1秒,所以会返回true)
if (thread.Join(5000))
{
result += "指定线程在五秒内执行完毕\r\n";
}
else
result += "未在指定线程在五秒内执行完毕\r\n";
txtMsg.Text = result;
}

void DoWork(object sleepMillisecond)
{
Thread.Sleep(Convert.ToInt16(sleepMillisecond));
result += "开启了新的线程\r\n";
}
}

在例子中通过Thread调用DoWork方法,同时记录下一些信息,同时使用了Join方法用来进行线程的堵塞。


五、ThreadPool

        <TextBlock x:Name="txtMsgQueueUserWorkItem"  TextWrapping="Wrap" Text="click here" MouseLeftButtonDown="txtMsgQueueUserWorkItem_MouseLeftButtonDown" Margin="30" />
     private void txtMsgQueueUserWorkItem_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// ThreadPool - 线程池的管理类

// QueueUserWorkItem(WaitCallback callBack, Object state) - 将指定方法加入线程池队列
// WaitCallback callBack - 需要在新开线程里执行的方法
// Object state - 传递给指定方法的参数
ThreadPool.QueueUserWorkItem(DoWork,DateTime.Now.ToString());
}

private void DoWork(object state)
{
// 作为线程管理策略的一部分,线程池在创建线程前会有一定的延迟
// 也就是说线程入队列的时间和线程启动的时间之间有一定的间隔

DateTime dtJoin = DateTime.Parse(state.ToString());
DateTime dtStart = DateTime.Now;

Thread.Sleep(3000);
DateTime dtEnd = DateTime.Now;

// Dispatcher.BeginInvoke() - 在与 Dispatcher 相关联的线程上执行指定的操作。自动线程同步
this.Dispatcher.BeginInvoke(delegate
{
txtMsgQueueUserWorkItem.Text += string.Format("\r\n入队列时间{0} 启动时间{1} 完成时间{2}",
dtJoin.ToString(), dtStart.ToString(), dtEnd.ToString());
});
}


六、EventWaitHandle - 通知其他线程是否可入的类

    public partial class EventWaitHandle : UserControl
{
// AutoResetEvent(bool state) - 通知其他线程是否可入的类,自动 Reset()
// bool state - 是否为终止状态,即是否禁止其他线程入内
private System.Threading.AutoResetEvent autoResetEvent =
new System.Threading.AutoResetEvent(false);

// ManualResetEvent(bool state) - 通知其他线程是否可入的类,手动 Reset()
// bool state - 是否为终止状态,即是否禁止其他线程入内
private System.Threading.ManualResetEvent manualResetEvent =
new System.Threading.ManualResetEvent(false);

private static int i;

public EventWaitHandle()
{
InitializeComponent();

// 演示 AutoResetEvent
AutoResetEventDemo();

// 演示 ManualResetEvent
ManualResetEventDemo();
}

private void AutoResetEventDemo()
{
i = 0;

for (int x = 0; x < 100; x++)
{
// 开 100 个线程去操作静态变量 i
System.Threading.Thread thread =
new System.Threading.Thread(new System.Threading.ThreadStart(AutoResetEventDemoCallback));
thread.Start();

// 阻塞当前线程,直到 AutoResetEvent 发出 Set() 信号
autoResetEvent.WaitOne();
}

System.Threading.Thread.Sleep(1000);
// 1 秒后 100 个线程都应该执行完毕了,取得 i 的结果
txtAutoResetEvent.Text = i.ToString();
}

private void AutoResetEventDemoCallback()
{
try
{
int j = i + 1;

// 模拟多线程并发操作静态变量 i 的情况
System.Threading.Thread.Sleep(5);

i = j;
}
finally
{
// 发出 Set() 信号,以释放 AutoResetEvent 所阻塞的线程
autoResetEvent.Set();
}
}


private void ManualResetEventDemo()
{
i = 0;

for (int x = 0; x < 100; x++)
{
// Reset() - 将 ManualResetEvent 变为非终止状态,即由此线程控制 ManualResetEvent,
// 其他线程排队,直到 ManualResetEvent 发出 Set() 信号(AutoResetEvent 在 Set() 时会自动 Reset())
manualResetEvent.Reset();

// 开 100 个线程去操作静态变量 i
System.Threading.Thread thread =
new System.Threading.Thread(new System.Threading.ThreadStart(ManualResetEventDemoCallback));
thread.Start();

// 阻塞当前线程,直到 ManualResetEvent 发出 Set() 信号
manualResetEvent.WaitOne();
}

System.Threading.Thread.Sleep(1000);
// 1 秒后 100 个线程都应该执行完毕了,取得 i 的结果
txtManualResetEvent.Text = i.ToString();
}

private void ManualResetEventDemoCallback()
{
try
{
int j = i + 1;

// 模拟多线程并发操作静态变量 i 的情况
System.Threading.Thread.Sleep(5);

i = j;
}
finally
{
// 发出 Set() 信号,以释放 ManualResetEvent 所阻塞的线程,同时 ManualResetEvent 变为终止状态)
manualResetEvent.Set();
}
}
}
}

这种方式也是很简单的,防止了多线程之间的并发,采取了先进行线程的阻塞,然后在线程的指定函数的尾部通过调用Set方法使释放阻塞的线程。

七、Timer的使用

在Silverlight中Timer的使用和在C#中其他地方的方法是一样的:

 public partial class Timer : UserControl
{
System.Threading.SynchronizationContext _syncContext;
// Timer - 用于以指定的时间间隔执行指定的方法的类
System.Threading.Timer _timer;
private int _flag = 0;

public Timer()
{
InitializeComponent();

// UI 线程
_syncContext = System.Threading.SynchronizationContext.Current;

Demo();
}

void Demo()
{
// 输出当前时间
txtMsg.Text = DateTime.Now.ToString() + "\r\n";

// 第一个参数:定时器需要调用的方法
// 第二个参数:传给需要调用的方法的参数
// 第三个参数:此时间后启动定时器
// 第四个参数:调用指定方法的间隔时间(System.Threading.Timeout.Infinite 为无穷大)
_timer = new System.Threading.Timer(MyTimerCallback, "webabcd", 3000, 1000);
}

private void MyTimerCallback(object state)
{
string result = string.Format("{0} - {1}\r\n", DateTime.Now.ToString(), (string)state);

// 调用 UI 线程。不会做自动线程同步
_syncContext.Post(delegate { txtMsg.Text += result; }, null);

_flag++;
if (_flag == 5)
_timer.Change(5000, 500); // 执行5次后,计时器重置为5秒后启动,每5毫秒的间隔时间执行一次指定的方法
else if (_flag == 10)
_timer.Dispose(); // 执行10次后,释放计时器所使用的全部资源
}
}


在这里需要提到
System.Threading.SynchronizationContext.Current,通过此得到SynchronizationContext当前的UI线程上下文对象,由于跨线程的关系,在其他线程中是无法操作UI线程对象的,所以可以使用其的Post方法进行同步处理。

 

八、Monitor的用法,其的作用和Lock的作用是一样的。

 public partial class Monitor : UserControl
{
private static readonly object objLock = new object();
private static int i;

public Monitor()
{
InitializeComponent();

i = 0;

for (int x = 0; x < 100; x++)
{
// 开 100 个线程去操作静态变量 i
System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(DoWork));
thread.Start();
}

System.Threading.Thread.Sleep(1000);
// 1 秒后 100 个线程都应该执行完毕了,取得 i 的结果
txtMsg.Text = i.ToString();
}

private void DoWork()
{
try
{
// Monitor - 提供同步访问对象的机制

// Enter() - 在指定对象上获取排他锁
System.Threading.Monitor.Enter(objLock);

int j = i + 1;

// 模拟多线程并发操作静态变量 i 的情况
System.Threading.Thread.Sleep(5);

i = j;

// Exit() - 释放指定对象上的排他锁
System.Threading.Monitor.Exit(objLock);
}
finally
{
// code
}
}
}

在代码的起始部分 System.Threading.Monitor.Enter进行排他锁,在代码的终止部分System.Threading.Monitor.Exit(objLock)进行释放排他锁。

 

九、使用ThreadStatic特性

    <StackPanel HorizontalAlignment="Left" Margin="5">
<TextBlock x:Name="txtMsg" />
<TextBlock x:Name="txtMsg2" />
</StackPanel>

可以看到在value1上添加了ThreadStatic特性标识。

public partial class ucThreadStaticAttribute : UserControl
{
//变量对于每一个线程都是唯一的
[ThreadStatic]
private static int value1;
private static int value2;
public ucThreadStaticAttribute()
{
InitializeComponent();
Demo();
}

void Demo()
{
Thread thread1 = new Thread(Dowork);
thread1.Name = "thread1";
thread1.Start();

Thread.Sleep(1000);

Thread thread2 = new Thread(Dowork2);
thread2.Name = "thread2";
thread2.Start();
}

void Dowork()
{
for (int i = 0; i < 10; i++)
{
value1++;
value2++;
}
string s = value1.ToString(); // value - 本线程独有的静态变量
string s2 = value2.ToString(); // value2 - 所有线程共用的静态变量

this.Dispatcher.BeginInvoke(delegate { txtMsg.Text = s + " - " + s2; });
}

void Dowork2()
{
for (int i = 0; i < 10; i++)
{
value1++;
value2++;
}
string s = value1.ToString(); // value - 本线程独有的静态变量
string s2 = value2.ToString(); // value2 - 所有线程共用的静态变量

this.Dispatcher.BeginInvoke(delegate { txtMsg2.Text = s + " - " + s2; });
}


}

可以看到一共两个Thread,经过绑定函数的处理之后显示在txtMsg和txtMsg2,看下效果:

第一行的为线程一的 value变量和value2变量,其中的value为加了ThreadStatic特性,表示对于每一个线程都是唯一的,而value则是线程共享的,可以看到

value在两次的线程中值都是自己的,而value2则是经过线程一计算后继续在线程二中进行叠加。


到此Silverlight中使用多线程(可以在C#中使用)就结束了,希望大家多多提供建议.



转载于:https://www.cnblogs.com/ListenFly/archive/2011/12/24/2294880.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值