首页 > 程序开发 > 软件开发 > Vb > 正文
VB.net学习笔记(二十七)线程同步上
2016-06-12       个评论    来源:白话魔法师  
收藏    我要投稿



X夫妇二人试图同时从同一账户(总额1000)中支取1000。由于余额有1000,夫妇各自都满足条件,于是银行共支付2000。结果是银行亏了1000元。这种两个或更多线程试图在同一时刻访问同一资源来修改其状态,并产生不良后果的情况被称做竞争条件。
\
为避免竞争条件,需要使Withdraw()方法具有线程安全性,即在任一时刻只有一个线程可以访问到该方法。

一、线程同步
多个线程读或写同一资源,就会造成错漏状况,这时就需要线程同步。同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。线程A与B , A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。“同”字是指协同、协助、互相配合。
这种在任一时刻只允许一个线程访问某一资源的现象称做同步。保持同步主要是避免竞争条件,确保线程的安全。至少有3种方式能够使一个对象具有线程安全性:在代码内同步临界区、使对象不可改变、使用线程安全包装器。
1、同步临界区
临界区是指在用一时刻只允许一个线程执行的代码段。
使对象或实例变量具有线程安全性的最简单方式是标识和同步其临界区。上图中:使Withdraw()方法成为临界区并需要具有线程安全性:让方法Withdraw( )同步化。
在任一时刻只有一个线程(X先生或X夫人)能够访问资源,执行中不能被中断的事务处理叫做原子操作。同步化的Withdraw( )方法具有原子性。
2、对象不可改变
不可变的对象是指一旦创建了对象其状态就不能被修改。在这种方法中,不需要锁定临界区,因为没有一种不可变的对象的方法(仅构造函数)实际写入对象的实例变暈,所以,不可变对象符合了线程安全的定义。
3、线程安全包装器
使对象具有线程安全性的另一个方法是编写个基于对象的包装器类,包装器类将会是线程安令的而不是使对象本身线程安全的。对象将保持小变,而且新的包装器类将包括线程安全代码的同步区。
例: AccountWrapper .类充当了Account类的线程安全包装器,


Class AccountWrapper
    Private _a As Account
    Public Sub New(ByVal a As Account)
        _a = a
    End Sub
    Public Function Withdraw(ByVal amount As Double) As Boolean
        SyncLock Me
            '....
            Return _a.Withdraw(amount)
        End SyncLock
    End Function
End Class
在这种方法中,Account对象不具有任何线程安全的特性,因为所有的线程安全都是由AccountWrapper类提供的。

 

采用线程安全包装器的方法,将开发线程安全的AccountWrapper类作为Account类的伸缩。
 

二、.NET对同步的支持
.NET Framework 在 System.Threading命名空间中提供了一些类,可以开发线程安全代码。
Monitor
提供同步访问对象的机制。Monitor对象用来锁定代码临界区,以便在任一时刻有且仅有一个线程访问临界区。Monitor对象帮助确保代码临界区的原子性。
Mutex
还可用于进程间同步的同步基元。除了它们通过对一个线程的处理来授权对共享资源的独占访问外,Mutex对象类似于Monitor对象。重载了构造函数的Mutex可以用于指定Mutex的所属关系和名字。
AutoResetEvent

通知正在等待的线程已发生事件。 此类不能被继承。

ManualResetEvent

通知一个或多个正在等待的线程已发生事件。 此类不能被继承。
AutoResetEvent 和 ManualResetEvent 用来通知一个或多个已经触发事件的等待线程。这些类都县Notlnheritable
InterLocked
为多个线程共享的变量提供原子操作。Interlocked 类有如下方法:CompareExchange()、Decrement()、Exchange(), and Increment(),这些方法为同步访问被多个线程共享的变量提供了一种简单的机制。
SynchronizationAttribute 类
设置组件的同步值。无法继承此类。SynchronizationAttribute确保了问一时刻只有一个线程可以访问对象。这种同步进程是自动的且不需要任何临界区的显式封锁。

三、.NET同步策略
通用语言基础结构提供了3种策略来同步访问实例、Shared方法和实例域,也就是:
A 同步上下文
B 同步代码区
C 手控同步


(一)同步上下文
简而言之就是允许一个线程和另外一个线程进行通讯(http://blog.csdn.net/iloli/article/details/16859605)
上下文是一组属性或使用规则,这组属性或使用规则对涉及到运行时执行的对象集合是通用的。能够添加的上下文属性包括有关同步的、线程亲缘性和事务处理的策略。在这种策略中,使用SynchronizationAttribute类使ContextBoundObject对象变得简单、自动同步。
驻留在上下文中的以及被绑定到上下文规则的对象称为受上下文约束的对象。
.NET把同步锁和对象自动关联起来,在每种方法调用前锁定对象,方法返回后释放对象(将被别的线程使用)。
由于线程同步和并发处理管理属于最普通的幵发陷阱之一,因此这种方法极大地提高了效率。这种属性比纯粹的同步多,其中包括与别的对象共亨锁的策略。
SynchronizationAttribute类对缺少手工处理同步经验的程序员来说是有益的,因为它覆盖了实例变量、实例方法、应用这种属性的类的实例域。但是,它不处理Shared域和方法的同步。如果我们必须同步特殊代码块,它也不起作用。同步整个对象是我们对轻松使用必须付出的代价。
例:通过使用 SynchronizationAttribute 來保证Account类的线程安全。


[SynchronizationAttribute] Public Class
	Account Inherits ContextBoundObject
	Sub ApprovedOrNotWithdraw (Amount)
		1.Check the Account Balance
		2.Update the Account with the new balance
		3.Send approval to the ATM
	End Sub
End Class

(二)同步代码区
第二种同步策略是有关特殊代码区的同步。如Monitor和 ReaderWriterLock 类、SyncLock 语句。

1.Monitor 类
锁即访问权限,获得锁即获得访问代码块(临界区)的权限。
通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还 可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。

使用Monitor.Enter()方法获得一个锁,然后,使用Monitor.Exit()方法释放该锁。一个线程获得锁,其他线程就要等到该锁被释放后才能使用。访问同步对象的线程可以采取的操作:
Enter, TryEnter
获取对象锁。此操作同样会标记临界区的开头。其他任何线程都不能进入临界区,除非它使用其他锁定对象执行临界区中的指令。
Wait
释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。
Pulse(信号),PulseAll
向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。
Exit
释放对象上的锁。此操作还标记受锁定对象保护的临界区的结尾。
\

----------------------------------------------------------------------------
Monitor.Pulse()的意义?如果不调用它会造成怎样的后果?(http://zhidao.baidu.com/link?url=dbtqrVwQvGxcEKbPrCfhjzS__VA3wPcGr2nmAtMQMyngoAI-gm-AU7br0d4a-miDskzINbs5h2ERnOYpXQpEoK)
一、意义?
首先,同步的某一个对象包含若干引用,这些引用被处于三个状态的线程队列所占据,这三个队列分别是:拥有锁的线程(处于执行中)、就绪队列(马上就可以获得锁的)、等待队列(wait中即阻塞的,需要进入就绪队列才拿到锁)。

 

执行流向:等待队列---> 就绪队列---> 拥有锁的线程

当拥有锁的线程执行完毕让出了锁,就绪队列的线程才有机会一窝蜂上去抢,锁只有一个,抢不到的继续在就绪队列里等待下一次机会(当然也需要考虑优先级设置情况的,没有则是这样),如此,直到 就绪队列 里的线程全部执行完。
问题来了:等待队列 的线程如何进入 就绪队列 ,以便得到执行机会呢?
基本途径就是:Monitor.Pulse() 或 超时自动进入。
所以Monitor.Pulse()的意义在于:将等待队列中的下一个线程放入就绪队列。(PulseAll()则是所有)。当然,如果等待队列里是空的,则不处理Pulse。

二、如果不调用它会造成怎样的后果?
不调用Pulse()造成的后果, 需要看等待队列中wait的超时设置,即”等待的最长时间“。
等待的最长时间有几个设置:无限期(Infinite)、某一时长、0。造成的后果由这个决定:

1、 Infinite:无限期等待状态下,如果当前获得锁的线程不执行Pulse(),那么本线程一直处于阻塞状态,在等待队列中,得不到执行机会;

2、某一时长:则两个情况:
a)在该时间内,还没有超时,如果当前执行线程有Pulse(),那么本线程有机会进入就绪队列。如果当前执行线程不调用Pulse(),则本线程依然呆在等待队列;
b)超过时长,这个线程会自动进入 就绪队列,无需当前获得锁的线程执行Pulse();

3、0:等待时长为零,则调用wait之后的线程直接进入就绪队列而不是 等待队列。
-----------------------------------------------------------------------------------
复习一下线程的状态图:

\

白话:多人(多线程)到医院同一窗口挂号(获得CPU),当前医院大厅正在办理挂号的人Tom(线程)就是Enter状态(获得锁),办理完毕后Tom就闪人(Exit释放锁)。正在办理的中途Tom想抽烟(或其它急事),但大厅不能抽只能在大厅外,于是Tom说等下(Wait)出去抽烟(或办事)5分钟,尽管挂号(锁定的代码块)未执行完,Tom就跑到外面去抽去了(释放锁,Wait优先Exit)。医院人员不会等Tom,他会办理后面人(另一线程)的挂号。在外面抽烟(办事)的人(WaitQuene等待队列)悠闲地抽,不必担心,因为医院人员在5分钟后(或后面线程提供的资源满足时)会主动发出信号(单人pulse,全部pulseall):快点来排队,于是大厅外的人(等待队列)收到信号就会回大厅内继续排队(就绪队列)。轮到原抽烟(Wait)的人再次到达窗口办理挂号时,就会继续从原来停下的地方(代码)向下办理。
例:Enter与Exit


Imports System.Threading
Namespace MonitorEnterExit
    Public Class EnterExit
        Private result As Integer = 0
        Public Sub NonCriticalSection() '无临界区,乱序竞争
            Console.WriteLine(("Entered Thread " & Thread.CurrentThread.GetHashCode().ToString))
            For i As Integer = 1 To 5
                Console.WriteLine(("Result = " & result & " ThreadID ” + Thread.CurrentThread.GetHashCode().ToString))
                result += 1
                Thread.Sleep(1000)
            Next i
            Console.WriteLine((”Exiting Thread " & Thread.CurrentThread.GetHashCode()))
        End Sub
        Public Sub CriticalSection()   '有临界区,有Enter与Exit以便上锁独享
            Monitor.Enter(Me)
            Console.WriteLine(("Entered Thread " & Thread.CurrentThread.GetHashCode.ToString))
            For i As Integer = 1 To 5
                Console.WriteLine(("Result = " & result & " ThreadID " + Thread.CurrentThread.GetHashCode.ToString))
                result += 1
                Thread.Sleep(1000)
            Next i
            Console.WriteLine(("Exiting Thread " & Thread.CurrentThread.GetHashCode()))
            Monitor.Exit(Me)
        End Sub
        Public Overloads Shared Sub Main(ByVal args() As [String])
            Dim e As New EnterExit()
            If args.Length > 0 Then '带参时,无临界区,线程乱序竞争
                Dim ntl As New Thread(New ThreadStart(AddressOf e.NonCriticalSection))
                ntl.Start()
                Dim nt2 As New Thread(New ThreadStart(AddressOf e.NonCriticalSection))
                nt2.Start()
            Else                    '不带参,有临界区,线程上锁独享
                Dim ctl As New Thread(New ThreadStart(AddressOf e.CriticalSection))
                ctl.Start()
                Dim ct2 As New Thread(New ThreadStart(AddressOf e.CriticalSection))
                ct2.Start()
            End If
            Console.ReadLine()
        End Sub
    End Class
End Namespace
可以看到有临界区与无临界时结果有显著的区别:
\

(1)Wait(等待)与Pulse(唤醒)
注意:只有在Enter()和Exit()代码块才可以调用Wait()和Pulse()方法.
Imports System.Threading
Namespace WaitAndPulse
    Public Class WaitPulsel  '唤醒等待类1
        Private result As Integer = 0
        Private _IM As LockMe
        Public Sub New(ByVal lock As LockMe)
            _IM = lock
        End Sub
        Public Sub CriticalSection()
            Monitor.Enter(_IM)
            Console.WriteLine(("WaitPulsel: Entered Thread " & Thread.CurrentThread.GetHashCode.ToString))
            For i As Integer = 1 To 5
                Monitor.Wait(_IM)
                Console.WriteLine("WaitPulsel: WokeUp")
                Console.WriteLine(("WaitPulse 1: Result = ” + result.ToString + ” ThreadID " +
                Thread.CurrentThread.GetHashCode().ToString))
                result += 1
                Monitor.Pulse(_IM)
            Next i
            Console.WriteLine(("WaitPulsel: Exiting Thread " & Thread.CurrentThread.GetHashCode.ToString))
            Monitor.Exit(_IM)
        End Sub
    End Class
    Public Class WaitPulse2  '唤醒等待类2
        Private result As Integer = 0
        Friend _IM As LockMe
        Public Sub New(ByVal lock As LockMe)                                  '3、接收lock到_IM
            _IM = lock
        End Sub
        Public Sub CriticalSection()
            Monitor.Enter(_IM)
            Console.WriteLine(("WaitPulse2: Entered Thread " & Thread.CurrentThread.GetHashCode().ToString))
            For i As Integer = 1 To 5
                Monitor.Pulse(_IM)
                Console.WriteLine("WaitPulse2: Result = " & result.ToString + " ThreadID ” & Thread.CurrentThread.GetHashCode.ToString)
                result += 1
                Monitor.Wait(_IM)
                Console.WriteLine("WaitPulse2: WokeUp")
            Next i
            Console.WriteLine("WaitPulse2: Exiting Thread " & Thread.CurrentThread.GetHashCode.ToString)
            Monitor.Exit(_IM)
        End Sub
    End Class
    Public Class ClassForMain '主程序类
        Public Shared Sub Main()
            Dim lock As New LockMe()                                         '1、lockme实例化
            Dim e1 As New WaitPulsel(lock)                                   '2、传送lock
            Dim e2 As New WaitPulse2(lock)
            Dim tl As New Thread(New ThreadStart(AddressOf e1.CriticalSection))
            tl.Start()
            Dim t2 As New Thread(New ThreadStart(AddressOf e2.CriticalSection))
            t2.Start()
            Console.ReadLine()
        End Sub
    End Class
    Public Class LockMe '被操作的类,此类的控制方式由WaitPulse1与WatiPulse2两个来决定
        '空             
    End Class
End Namespace
结果如下:
\
说明:1处实例化被操作的类,2处再创建两个可以控制lock的方式方法,并将lock传递到_IM对象中。两条线程虽然各自按循环向前走,但是停还是走则由e1和e2中的wait与pulse来控制。具体的执行如下图的红线,最后线程1中在退出前,唤醒了线程2,所以e2临界区最后退出。
\
注意:Monitor 锁定对象(即引用类型),而不是值类型。 虽然可以将值类型传递到 Enter 和 Exit,但对每个调用它都分别进行了装箱。 由于每次调用都将创建一个单独的对象,所以绝不会阻止 Enter 并且它应该保护的代码不会真正同步。 此外,传递到 Exit 的对象不同于传递到 Enter 的对象,因此 Monitor 将引发 SynchronizationLockException 异常并显示消息“从代码的非同步块调用了对象同步方法。”
\
装箱与拆箱(左图):
值类型在栈上;引用类型(如对象)在堆上,它的地址是放在栈。装箱就将值类型(i)打包放在堆上从而使其由值类型变成引用类型(对象A),它的引用地址放在栈上obj_i。拆箱就是把引用类型(对象A)再次变回值类型i。
为什么Monitor 只能锁定对象(即引用类型)?
Moniter锁定的是堆上的引用对象(右图,obj),如果变成了值类型,每次进入,就会装值类型装箱打包成引用类型,而每次装箱尽管里面内容一样,但形成地址不一样(第一次打包是A,第二次打包是B),循环进入的次数越多,就还会形成C、D、E……那么到底是上锁保护A还是上锁保护B呢?于是编译器就纳闷了,会抛出异常。

(2)TryEnter()方法
试图在一个对象上获得独占锁。如果当前线程获取该锁,则为 true;否则为 false。
Imports System.Threading
Namespace MonitorTryEnter
    Public Class ATryEnter
        Public Sub New()
        End Sub
        Public Sub CriticalSection()
            Dim b As Boolean = Monitor.TryEnter(Me, 1000)
            Console.WriteLine("Thread " & Thread.CurrentThread.GetHashCode & " TryEnter Value: " & b)
            If b Then '1、if    
                For i As Integer = 1 To 3
                    Thread.Sleep(3000)
                    Console.WriteLine(i & " " & Thread.CurrentThread.GetHashCode.ToString)
                Next i
                Monitor.Exit(Me)                     '2、释放锁
            End If    '1、endif  
        End Sub
        Public Shared Sub Main()
            Dim a As New ATryEnter()
            Dim tl As New Thread(New ThreadStart(AddressOf a.CriticalSection))
            Dim t2 As New Thread(New ThreadStart(AddressOf a.CriticalSection))
            tl.Start()
            t2.Start()
            Console.ReadLine()
        End Sub
    End Class
End Namespace
说明:在1处无if时,结果是左图;无if时结果是右图。左图因为无if,最后异常,因为不上锁是也执行,最后释放锁时前面根本就没有上锁,还释放什么锁呢?于是抛出异常了。正常的做法是右图,用TryEnter去尝试,返回为真,才执行进而临界区的代码。

 

注意:正常的做法是1、为了保证线程进入临界区达到上锁目的,应对TryEnter进行判断;2、如果发生异常,应在Try…Catch…中的Finally块中放置Exit,以确保线程不因异常一直保持锁。
\

2、SyncLock 语句
SyncLock关键字可以用作Monitor方法的一个替换用法。下面的两部分代码是等价的:


            Monitor.Enter(x)
            '…………
            Monitor.Exit(x)


            SyncLock Me
                '………….
            End SyncLock
SyncLock 块通过调用 System.Threading 命名空间中的 Monitor 类的 Enter 方法和 Exit 方法获取和释放独占锁。
SyncLock 语句(在执行一个语句块前获取此块的独占锁。)
SyncLock lockobject
    [ block ]
End SyncLock
lockobject 必需。 计算结果等于对象引用的表达式。lockobject 的值不能为 Nothing。 必须先创建锁定对象
block 可选。 获取独占锁时要执行的语句块。
End SyncLock 终止 SyncLock 块。

 

SyncLock 语句可确保多个线程不在同一时间执行语句块。最常见作用是保护数据不被多个线程同时更新。如果操作数据的语句必须在没有中断的情况下完成,请将它们放入 SyncLock 块。 有时将受独占锁保护的语句块称为“临界区”。
SyncLock 块的操作就像 Try...Finally 结构,其中 Try 块获取 lockobject 上的独占锁,而 Finally 块则释放此锁。 因此,SyncLock 块确保锁的释放,不管您如何退出块。 即使发生未经处理的异常,也是如此。


Class simpleMessageList
    Public messagesList() As String = New String(50) {}
    Public messagesLast As Integer = -1
    Private messagesLock As New Object 
    Public Sub addAnotherMessage(ByVal newMessage As String)
        SyncLock messagesLock
            messagesLast += 1
            If messagesLast < messagesList.Length Then
                messagesList(messagesLast) = newMessage
            End If 
        End SyncLock 
    End Sub 
End Class
例:SyncLock  Me造成死锁的情况。
Imports System.Threading
Namespace Locking
    Public Class Locking
        Private result As Integer = 0
        Public Sub CriticalSection()
            SyncLock Me
                Console.WriteLine("Entered Thread " & Thread.CurrentThread.GetHashCode.ToString)
                For i As Integer = 1 To 5
                    Console.WriteLine("Result = " & result & " ThreadID " & Thread.CurrentThread.GetHashCode.ToString)
                    result += 1
                    Thread.Sleep(1000)
                Next i
                Console.WriteLine("Exiting Thread " & Thread.CurrentThread.GetHashCode.ToString)
            End SyncLock
        End Sub
        Public Overloads Shared Sub Main(ByVal args() As String)
            Dim e As New Locking()
            Dim t1 As New Thread(New ThreadStart(AddressOf e.CriticalSection))
            t1.Start()
            Dim t2 As New Thread(New ThreadStart(AddressOf e.CriticalSection))
            t2.Start()
            Console.ReadLine()
        End Sub
    End Class
End Namespace
\
说明:MSND建议尽量不要用SyncLock Me。原因是Me的滥用,使得代码变得复杂,甚至互锁、死锁。
例:SyncLock Me与类外锁定对象实例时死锁情况。
Imports System.Threading
Namespace TestUseMe
    Class C1
        Private deadlocked As Boolean = True
        Public Sub LockMe(ByVal o As Object)
            SyncLock Me
                While deadlocked        '一直为真,故死锁
                    deadlocked = CType(o, Boolean)
                    Console.WriteLine("Foo: I am locked :(")
                    Thread.Sleep(500)
                End While
            End SyncLock
        End Sub
        Public Sub DoNotLockMe()
            Console.WriteLine("I am not locked :)")
        End Sub
    End Class
    Class Program
        Shared Sub main()
            Dim c1 As New C1
            Dim t1 As New Thread(AddressOf c1.LockMe)
            '在t1线程中调用LockMe,并将deadlock设为true(将出现死锁)
            t1.Start()
            Thread.Sleep(100)
            SyncLock c1           '在主线程中上锁 c1
                c1.DoNotLockMe()  '此法未在t1线程上锁,可调用
                c1.LockMe(False)  '此法已在t1线程上锁(死锁,需改deadlock为假解锁)无法访问
            End SyncLock
            Console.ReadLine()
        End Sub
    End Class
End Namespace
说明:结果(如下),t1线程锁定其中LockMe(ByVal o As Object)方法后死锁,这里主线程锁定c1对象,DoNotLockMe()方法可访问,但LockMe(ByVal o As Object)方法被死锁,所以无法访问,因此只有一个未被锁定的方法输出内容。
\
另外一个就是t1线程中的SyncLock Me,这个Me应该是c1,但奇怪的是并没有锁全,导致主线程中仍可访问DoNotLockMe(),这是为什么呢?
原因是:尽量用Private(原文:lockobject 表达式应始终计算仅属于您的类的对象。您应该声明一个 Private 对象变量,以保护属于当前实例的数据,或声明一个 Private Shared 对象变量,以保护所有实例共有的数据。)。
例:微软上的例子
Imports System.Threading
Module Module1
    Class Account
        Dim thisLock As New Object '随便创建的一个引用型对象实例
        Dim balance As Integer  '这才是保护的数据
        Dim r As New Random()
        Public Sub New(ByVal initial As Integer)
            balance = initial
        End Sub
        Public Function Withdraw(ByVal amount As Integer) As Integer
            SyncLock thisLock
                If balance >= amount Then
                    Console.Write("ThreadID:" & Thread.CurrentThread.ManagedThreadId.ToString)
                    Console.Write(", Balance: " & balance & ", Withdraw:-" & amount)
                    balance = balance - amount
                    Console.WriteLine(" Result: " & balance)
                    Return amount
                Else
                    Return 0
                End If
            End SyncLock
        End Function
        Public Sub DoTransactions()
            Withdraw(r.Next(1, 500))   '取钱
        End Sub
    End Class
    Sub Main()
        Dim threads(10) As Thread
        Dim acc As New Account(1000) '银行总额1000
        For i As Integer = 0 To 9    '产生10个线程
            Dim t As New Thread(New ThreadStart(AddressOf acc.DoTransactions))
            threads(i) = t
        Next
        For i As Integer = 0 To 9    '10个线程开始执行
            threads(i).Start()
        Next
        Console.ReadLine()
    End Sub
End Module
\

 

 
点击复制链接 与好友分享!回本站首页
相关TAG标签 线程 笔记
上一篇:VB.net学习笔记(二十六)线程的坎坷人生
下一篇:VB.net学习笔记(二十八)线程同步下
相关文章
图文推荐
文章
推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做实用的IT技术学习网站