精华内容
下载资源
问答
  • clang和MSVC的最新实现已经提供了试验性的协程实现,想要试用的话,clang需要开启-fcoroutins-ts -stdlib=libc++两个开关,而MSVC需要开启/await开关。 我们从一个简单的generator来看编译器做了什么: #include <...

    clang和MSVC的最新实现已经提供了试验性的协程实现,想要试用的话,clang需要开启-fcoroutins-ts -stdlib=libc++两个开关,而MSVC需要开启/await开关。

    我们从一个简单的generator来看编译器做了什么:

    #include <experimental/generator>
    #include <cstdio>
    
    namespace stdexp = std::experimental;
    
    stdexp::generator<int> gen(int count) {
        for (int i = 0; i < count; i++) {
            co_yield i;
        }
    }
    
    int main() {
        for (auto v : gen(10)) {
            printf("%d\n", v);
        }
    }

    显然的,这段代码会输出0-9,如果你觉得main里面的range-based-for有点玄学的话,这么写main也行。

    int main() {
        auto g = gen(10);
        auto it = g.begin();
        printf("%d\n", *it); //0
        it++;
        printf("%d\n", *it); //1
        it++;
        printf("%d\n", *it); //2
    }

    第一次调用gen,生成了一个generator g,然后我们获取了g的迭代器,每次对迭代器取值都能获得一个从gen函数里yield出来的值,而对迭代器的++,不难猜出,它负责恢复gen函数的执行。

    如果你随心所欲的删除或者增加代码里的it++,就能发现输出的结果会响应的跳过一些值,不难猜出,每次通过it++恢复gen函数执行,gen函数yield之后,都会把yield的值存在某个地方,然后你才能通过*it的操作把它取出来。

    promise类型

    这个某个地方,其实就是promise类型和返回值的共同作用结果,对于每个协程,在正式进入函数体时,编译器都会构造一个promise_type的对象,这个promise_type按照C++的尿性,必然是你自己提供的,通过一个traits,这个我们后面会讲。然后编译器会调用promise的get_return_object方法,这个方法的返回值在本例中会被用来构造generator对象,也就是说coroutine的返回值是通过promise提供的,为什么要这样设计,因为coroutine经常需要在每次suspend/resume之前/后修改或设置返回值,比如本例的存一个int进去。所以需要让promise和返回值之间建立一个联系,这样最好的方式就让promise来提供返回值,这样promise_type和return_type的内部实现之间就可以搞一些py交易,比如互相保存一下对方的指针什么的,来实现互相操作对方。

    这么多话,总结一下,就是coroutine通过操作promise来修改返回值,promise是coroutine向外返回的结果的入口。

    同时promise_type也是整个协程的抽象,最早我们提到过编译器实现的无栈协程会把整个协程分配在某个地方,而这里的promise,就成为了编译器向你暴露的协程的一个接口。

    函数到底是怎么执行的

    最开始,编译器会构造gen函数的promise_type对象,通过coroutine_traitspromise_type类型。coroutine_traits接受函数返回类型和参数类型作为参数,你可以自己特化它。

    stdexp::coroutine_traits<stdexp::generator<int>, int>::promise_type __p;

    我们这个例子里返回类型是generator<int>,有一个参数int

    然后编译器调用__p.get_return_object(),用他的返回值(多半是这个promise本身)来构造main里的generator g

    g的构造函数会通过(编译器)标准库提供的coroutine_handle<promise_type>::from_promise(__p),从promise获取对应的coroutine_handle,这是代表协程的句柄,用它来控制协程的恢复和销毁。

    然后编译器调用__p.initial_suspend(),它的返回值用来决定要不要在函数体执行前暂停,在我们这个例子里,应该暂停,因为函数体应当在第一次获取迭代器(即调用begin时开始执行)。

    然后函数被暂停,调用回到main,第一行执行完毕。

    当你调用g,即那个generator<int>begin后,g__handle.resume()恢复协程执行,gen函数进入循环体。

    gen函数第一次co_yield时,编译器调用__p.yield_value(i),将局部变量i的值传给promisepromise就可以把这个值存起来,等待main里面generator的迭代器来取,然后yield_value的返回值用来决定要不要暂停协程执行,我们这个例子里,同样应该暂停,回到调用者那里处理yield出来的值。

    此时调用回到main,第二行执行完毕,g的迭代器里已经装了一个gen函数yield出来的值。

    printf里面*itit通过generator里面存的coroutine_handle把值取出来,coroutine_handle可以通过promise成员函数来获取到对应的promise

    it++同样也会恢复协程执行,和上面begin的描述一样。

    当你第11次调用it++时,gen函数实际上是第12次恢复执行(因为begin恢复执行了一次),gen函数从循环最后一次yield的位置恢复,退出循环,执行到函数体的结尾,此时编译器会调用__p.final_suspend(),询问你是不是要在最后再暂停一次gen函数,我们这里是需要的。因为在控制流从gen的循环退出,执行到函数最后时,如果协程直接结束,coroutine_handle直接被销毁,it无从得知它的__handle已经变为野指针,当他用__handle.done()来更新自己是否到end的状态时就会爆炸,所以最后应该再额外暂停一次,就像迭代器允许指向一个尾后位置来表示end一样,允许一个协程在函数体执行完毕,销毁之前再暂停一下,表示一个end状态。

    main最后generator g销毁时会顺带执行__handle.destroy(),销毁gen协程。

    用伪代码说一下

    generator<int> gen(int a) {
        coroutine_traits<generator<int>, int>::promise_type __p;
        generator<int> __r = __p.get_return_object(); //这里实际上是构造了main里的返回值,但是代码里没法描述,就写在gen里了
        if (__p.initial_suspend()) { //true
            //第1次暂停
        }
        for (int i = 0; i < count; i++) {
            if (__p.yield_value(i)) { //true
                //第i+2次暂停
            }
        }
        if (__p.final_suspend()) { //true
            //第count+2次暂停
        }
        //__p销毁
    }

    转载于:https://www.cnblogs.com/pointer-smq/p/8877635.html

    展开全文
  • 有了generator,你可以生成一个惰性求值的列表,对他进行变换,而这些所有的操作都是惰性执行的,这就是C#里面的LINQ,Python和Javascirpt里的生成器,Java8的Stream API,C++的range-ts也可以接入coroutine,而避免...

    上一篇说了coroutine的本质是什么,就是resumable function,那么一个函数有了suspend和resume功能之后,会打开什么样的新世界大门呢?随便举几个例子。

    • 函数每次被唤醒,就丢出一个值,然后暂停——这是generator
    • 函数启动一个IO操作,注册IO操作完成时唤醒自己,然后暂停——这是async-await
    • 函数开启一个管道,暂停,另一个函数往管道里写一点东西,然后唤醒它——这是channel
    • 函数检查某个值是不是期望的,如果不是,就暂停——这是exception

    其实,2和3的机制是很相似的,只不过2里面唤醒coroutine的是操作系统的callback,3是你自己的另一个线程。4里面说的异常,其实就是这样的,因为异常和coroutine再往下追溯,他们的理论基础都是CPS(Continuation Passing Style),也就是把当前操作的后续操作作为闭包传给当前操作,而当前操作可以选择执行哪一个(或是否执行)后续操作。虽然coroutine和exception都用到了CPS,但大部分的coroutine都额外支持了exception,耦合在了一起,因为他们没有直接提供操作continuation的东西。

    扯远了,可以看到了,coroutine解锁了很多写代码的新姿势。

    有了generator,你可以生成一个惰性求值的列表,对他进行变换,而这些所有的操作都是惰性执行的,这就是C#里面的LINQ,Python和Javascirpt里的生成器,Java8的Stream API,C++的range-ts也可以接入coroutine,而避免使用复杂的迭代器封装状态。

    有了async-await,你可以把异步代码写成像同步代码那样,而代码在await的边界处是自动暂停和继续的,这无疑降低了程序员手动写状态机维护状态的难度,也避免了一连串.then造成回调地狱的问题。channel和异步操作的async-await很相似,两条线程如今可以主动的暂停自己和唤醒对方,通过一个普通的原子变量来传递信息,而不需要用厚重的管道或者多线程同步机制来等待和唤醒对方。

    关于coroutine模拟exception,这倒是没有太大的必要,不过,考虑到coroutine的本质是CPS,那么就可以用coroutine来模拟rust的自动向上传播错误码,haskell的maybe monad等等,这些东西是对应语言的错误处理机制,就像C++的异常一样。

    脑洞,反过来,异常可以模拟coroutine吗?不行,异常有点像暂停之后再也不会唤醒的coroutine。

    转载于:https://www.cnblogs.com/pointer-smq/p/8809203.html

    展开全文
  • 大家先不要在脑子里思考这个中断和恢复执行具体要怎么做,而是先建立一个概念模型,一个函数除了调用结束后返回caller,还可以在调用中途返回caller,并可以由caller在中断的地方继续执行,这就是coroutine了。...

    什么是coroutine

    什么是coroutine?接触过的脑子里肯定会蹦出来很多词:async-await,generator,channel,yield,高并发,甚至goroutine。其实,这些都是coroutine的外部表象,coroutine的本质是什么?上古时期的计算机科学家们早就给出了概念,coroutine就是可以中断并恢复执行的subroutine,什么是subroutine?就是大家熟知的函数。

    大家先不要在脑子里思考这个中断和恢复执行具体要怎么做,而是先建立一个概念模型,一个函数除了调用结束后返回caller,还可以在调用中途返回caller,并可以由caller在中断的地方继续执行,这就是coroutine了。所以coroutine也被叫做resumable function,可继续函数,并且从定义上看,coroutine是subroutine的超集,也就是所有的function也都是coroutine,只不过他们没有执行中断和继续这两个操作。

    所以看到这里,先不要想太多,先明确了coroutine是可以中断并恢复的函数就好了,我们然后来明确几个概念,为了后面提到的时候不会混乱。

    coroutine的暂停/中断,也叫suspend,是coroutine暂停执行,并将控制流返回给调用者caller的过程。

    coroutine的恢复/继续,也叫resume,是caller恢复coroutine执行的过程。

    当然coroutine还包含了普通函数也有的调用(invoke)和返回(return)两个操作。

    中断和恢复的实现

    函数要中断并从中断处恢复,那么从常识上考虑,就像线程切换cpu要保存寄存器状态一样,函数中断前也要保存当前的状态到一个持久的位置,然后中断后这部分栈空间才能放心的交给caller去继续用,不然恢复的时候现场都被破坏了就不对了。

    保存当前状态有两种常见的实现,一种是说,既然函数中断之后栈空间不能被别人改写,寄存器的值要保存下来,那不如让这个函数使用独立的栈空间好了,这种实现就是有栈协程(stackful-coroutine)。函数调用前,保存调用者的所有寄存器,然后malloc一块单独的空间并把栈指针指过去,然后正常调用函数,被调用的函数自然就会在这块独立的栈空间上操作。中断前,把所有寄存器都存到栈上,然后把栈指针指回调用者的栈空间,并且恢复调用者的寄存器,这就实现了有栈协程的暂停操作。

    根据我的描述也能看出,有栈协程的实现需要底层操作,比如修改栈指针,保存寄存器等等,而且有栈协程需要大块的栈空间分配,不管什么样的函数,每次调用都要malloc出来几kb的空间,并且保存寄存器和恢复也需要一定的性能损失,现代处理器需要保存的寄存器往往有上百字节,还是比较大的。

    当然也不是说有栈协程不好,和后面要说的无栈协程相比,有栈协程不需要太多编译器支持,还是很棒棒的。著名的有栈协程实现包括Windows的Fiber,ucontext,fcontext,boost fiber等。

    用Windows的Fiber举例,调用起来是这样的

    void* main_func;
    void coro() {
        int i = 0;
        i++;
        SwitchToFiber(main_func); //suspend回main
        i++;
    }
    int main() {
        main_func = ConvertThreadToFiber(xxx);
        void* coro = CreateFiber(coro, xxx);
        SwitchToFiber(coro); //调用coro
        //coro suspend
        SwitchToFiber(coro); //让他resume
        DeleteFiber(coro);
    }

    除了有栈协程,另一个常见的实现就是无栈协程(stackless coroutine),这是怎么实现的呢?无栈协程需要编译器的转换工作,对于一个简单的协程(伪代码)

    void fun()
    {
        int i = 0;
        i++;
        SUSPEND();
        i++;
        return;
    }

    编译器会将他转写成一个对象(或类似物),以suspend的地方为分界,将函数拆成几部分,每个部分为一个单独的函数,然后将局部变量都做成类成员,这样每个被拆出来的子过程都可以访问这个成为了成员变量的局部变量,最后,保存一个状态变量,生成一个MoveNext函数(名字只是为了表述方便),每次调用MoveNext,根据状态变量的值,来执行前面被拆出来的不同的函数,比如上面的伪代码,会被编译器转写成以下的样子(命名只是为了表述方便)

    struct fun_coroutine {
        int i;
        int __state = 0;
        void MoveNext() {
            switch(__state) {
            case 0:
                return __part0();
            case 1:
                return __part1();
            }
        }
        void __part0() {
            i = 0;
            i++;
            __state = 1;
        }
        void __part1() {
            i++;
        }
    };

    调用者对fun的调用也会被转写成构造fun_coroutine,然后调用MoveNext成员函数,此时执行的是__part0,__part0的返回就是函数第一次suspend,调用者可以选择第二次调用MoveNext,这时被执行的就是__part1函数了。

    由此看来,无栈协程的调用消耗的空间就是局部变量占用的全部空间,相比有栈协程每次分配几KB小很多,而且,无栈协程对象直接构造在调用者的栈上,意味着其中的成员(局部变量)也都在调用者的栈上,相比有栈协程把局部变量放在新开的空间上,CPU cache局部性更好,同时无栈协程的中断和函数返回几乎没有区别,而有栈携程的中断需要保存上百字节的寄存器,并且,无栈携程需要编译器参与,那么编译器完全可以进行类似函数内联,常量折叠之类的操作,将协程的调用尽可能优化到没有。综合性能会比有栈协程更好。

    吹了这么多,有栈协程好是好,可是需要编译器支持,而对于C++这种巨复杂的语言,你加点什么东西一要提防着不要影响其它feature和已有代码,二要地方这些东西能不能和已有feature结合,不能冲突,三还要不能限定实现(比如C#的yield只能返回IEnumerator<T>),所以牙膏挤到了C++20甚至23,还没有正式确立加入语言。

    后话

    我在说无栈协程suspend的性能和函数返回没区别的地方,肯定有人会反驳我说fun_coroutine会被new出来啊,异常之类的东西会增大overhead啊,实际上这些东西普通函数也有,你new了一个对象然后调用成员函数,和调用全局函数,区别大吗,我感觉是不大。此外有人会怀疑转写成对象+状态会让编译器没法优化,实际上,llvm是直接支持coroutine的,如果你的语言编译到llvm,你的前端可以不把他转换成对象给llvm看,而是直接用llvm的协程原语,剩下的丢给llvm去优化。

    转载于:https://www.cnblogs.com/pointer-smq/p/8780895.html

    展开全文
  • 1.1 ts 如何声明一个boolean, number, string类型的值 // 在js中,定义 isFlag 为true, 但是后面还可以重新给它赋值为字符串,而ts中就不行,同理,声明number, string 也一样 let bool: boolean = true; // 会报错...

    1.1 ts 如何声明一个boolean, number, string类型的值

    // 在js中,定义 isFlag 为true, 但是后面还可以重新给它赋值为字符串,而ts中就不行,同理,声明number, string 也一样
    let bool: boolean = true;
    // 会报错:不能将类型“"str"”分配给类型“boolean”。
    // isFlag = "str"
    let str: string = "a";
    // 会报错:不能将类型“1”分配给类型“string”。
    // str = 1
    let num: number = 1;
    // 会报错:不能将类型“"a"”分配给类型“number”。
    // num = "a"
    

    1.2 ts 如何声明一个 undefined, null 类型的值

    let u: undefined = undefined;
    let n: null = null;
    

    1.3 ts 如何声明一个数组

    // 方式一:声明一个每一项只能是string类型的数组。在元素类型后面接上[],表示由此类型元素组成一个数组
    let arr1: string[] = ["a", "b"];
    // 方式二:使用数组泛型,Array<元素类型>
    let arr2: Array<string> = ["a", "b"];
    // 方式三:利用接口: ts的核心原则是对值所具有的结构进行类型检查,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约
    interface NumberArray {
      // 这个接口表示:只要索引的类型是number,那么值的类型必须是number
      [index: number]: number;
    }
    let arr3: NumberArray = [1, 2, 3];
    // 数组里想放对象?
    let arr4: Array<object> = [{ a: "1" }, { b: "2" }];
    // 数组里想放多种数据类型? 用 | 隔开就好
    let arr5: Array<string | number> = ["a", 1];
    // 数组里想放任意类型?
    let arr6: Array<any> = ["a", 1, true, { a: 1 }, [1, 2]];
    

    1.4 ts 如何声明一个对象

    // 利用接口
    interface OA {
     name: string; // 表示对象要有name属性, 值是string类型
     age?: number; // ? 表示age属性可以有也可以没有, 值是number类型
     readonly id: number; // readonly 表示 id 属性只可读,不可修改
    }
    let obj1: OA = { name: "obj1", id: 1 };
    let obj2: OA = { name: "obj2", age: 18, id: 2 };
    // 这种情况下name 和 id 属性必须要,age属性可要可不要,但是除了这三种属性外,其它的属性都不准出现
    // let obj3:OA = {a: 1} // 会报错
    // 有时候还是希望一个对象允许有任意的属性怎么办?
    interface OB {
     name: string;
     age?: number;
     [propName: string]: any;
    }
    let obj4: OB = { name: "a" };
    let obj5: OB = { name: "a", age: 1 };
    let obj6: OB = { name: "a", age: 1, other1: 2, other2: "2", other3: { a: 1 } };
    

    1.5 ts 如何声明一个函数

    // 一: :void 表示函数没有返回值
    function fn2(x: number): void {
      console.log(x);
    }
    // 二: 这里表示sum 函数接收两个参数(多了少了都不行), 参数类型是 number, 返回值也是number
    function fn1(x: number, y: number): number {
      return x + y;
    }
    // 三: 同样也可以用?来表示可选参数,但是可选参数只能放到最后面
    function fn3(x: number, y?: number): number {
      if (y) {
        return x + y;
      }
      return x;
    }
    // 四:ts 怎么表示默认参数
    function fn41(x: number, y: number = 1): number {
      return x + y;
    }
    // 传值的时候,默认参数可以不传
    fn41(1);
    fn41(1, 2);
    // 默认参数不像可选参数必须放后面,它也可以放前面, 但是还是推荐放后面去
    function fn42(x: number = 1, y: number): number {
      return x + y;
    }
    // 传值的时候,默认参数可以不传,但是如果默认参数不放后面,这样传值感觉很怪
    fn42(undefined, 2);
    // 五:ts表示剩余参数。利用扩展运算符
    function fn52(array: any[], ...items: any[]) {
      items.forEach(item => {
        array.push(item)
      })
    }
    // 六:箭头函数
    // es6 箭头函数:没有参数的时候
    // let fn61 = () => {}
    
    // es6 箭头函数:一个参数的时候,可以不用圆括号
    // let fn62 = arg => {
    //   console.log(arg)
    // }
    
    // es6 箭头函数:多个参数的时候,如果致谢一行的话,不用大括号和return
    // let fn63 = (arg1, arg2) => arg1 + arg2;
    
    // es6 箭头函数:多个参数并且换行的时候,需要写大括号和return
    // let fn64 = (arg1, arg2) => {
    //   return arg1 + arg2
    // }
    
    // es6 箭头函数:返回一个对象,必须在对象外面加上圆括号
    // let fn65 = (arg1, arg2) => ({a: arg1, b:arg2})
    
    // es6 上例中一般会不改名字,同名可以缩写成这样
    // let fn66 = (arg1, arg2) => ({
    //   arg1,
    //   arg2
    // })
    
    展开全文
  • 为什么我的代码不解密ts段?在from Crypto.Cipher import AESimport requests as reqimport osimport m3u8with req.session() as req:m3u = m3u8.load('...
  • vue3项目也是了typescript,并且现在ts也很火,就正好一起实践一下。准备工作确保安装了node开始1、项目初始化npm init vite-app my-vue3此时项目就已经初始化好,并且可以正常运行了。cd m...
  • 比如如果我想做个视频站,也用ts这种方式,该怎么做? ===================== 都是答非所问。 我想问的是这种播放方式的实现原理。 假定我搭了个服务器。里面有一个视频 。 我在浏览器打开这个网页播放...
  • 这里我我的thinkpad E430c给大家做个演示,首先和往常一样按开机,在这个界面记得要按Fn+F12,联想键盘的新功能键太难了,干嘛都得先按FN,当然这个也是可以关闭的,回头我再也个经验给大家讲。2. 这样就进入了...
  • 我知道的有和directshow和ffmpeg,但是不知道怎么用。求大神给予指导!万分感谢!  采集卡有个开发包,里面有个接收TS流的函数: int btert_receive(void* pBuffer,int nLength,int DeviceId,int ChannelId); ...
  • 公共模块,通常会被多个项目、不同的开发人员使用,所以开发公共模块时,你自己会还不够,要让所有人都能很快的知道怎么去使用,这一点很关键。通常会从3个方面做到这点:精心分割代码逻辑,遵循开闭原则;变量名...
  • 已实验很多视频 都是这样的 的是这个代码 ffmpeg -f concat -i filelist.txt -c copy output.mp4 求大佬告知 </p>
  • 各位大侠,我写了个程序,其中有部分涉及到了把系统当前时间录入到数据库中,我使用了PreparedStatement对象的setTimestamp(int Index, Timestamp ts, Calendar c)方法,但是执行了sql语句后发现时间没录进去值显示...
  • 使用方法:电脑potplay,电视盒子kodi.2月12号还能,以后就难说了。中文HD,http://liveali.ifeng.com/live/FHZW.m3u8中文HD,http://111.13.42.9/PLTV/88888888/224/3221225948/1.m3u8中文HD,...
  • VSCode格式化插件格式化.ts文件后,会将单引号转换成双引号,但是这时tslint就会报错: " should be 'tslint(quotemark) 想让tslint去掉这条规则怎么办呢? 按快捷键Ctrl+Shift+P,输入tslint,选择TSLint: ...
  • 通过 lupdate 命令产生的 Qt 翻译文件(.ts)。 Qt Linguist 打开时,会有“”项, 若是编辑软件打开.ts文件则是“<source></source>”项目。 请问这是怎么产生的? 谢谢!
  • TS 开发时怎么,确定一些VUE自己的类型,或者说怎么查看一些VUE自己的类型。还有DOM类型也是一样。 因为是刚使用TS开发,并不熟悉VUE自己的一些类型,还有一些DOM类型,网上查找也...
  • <div><p>我在shims-vue.d.ts中声明了 iview的$Message 在vue中使用正常,但在store的permission模块中过滤权限路由时 我需要提示以及路由跳转 ,如何才能使用$Message呢 this也只是指向模块或者window...
  • 公共模块,通常会被多个项目、不同的开发人员使用,所以开发公共模块时,你自己会还不够,要让所有人都能很快的知道怎么去使用,这一点很关键。通常会从3个方面做到这点:精心分割代码逻辑,遵循开闭原则;变量名...
  • 目录 项目介绍 项目前的准备及项目构思 模块化 封装通用组件 @语法导入相关文件 ...custom.d.ts怎么用 Vue单文件组件的三种写法 Layout组件&插槽<slot> 使用VueRouter 在TS组件里使用mixin
  • 大多数的小伙伴们平时办公的时候都喜欢PDF,但是由于PDF文件过多可能会造成阅读和传输的不便。我们想要解决这个问题,其实很简单。就是学会把多个PDF合并成单一PDF。具体如何实现多个PDF合并成一个PDF,下面就来看...
  • 泛型是指定一个表示类型的变量,它来代替某个实际的类型用于编程,而后通过实际调用时传入或推导的类型来对其进行替换,以达到一段使用泛型程序可以实际适应不同类型的目的。为了实现泛型所要达到的目的,我们也可...
  • input标签的方式不可用,不能采用类似加标签属性“ oninput="value=value.replace(/[^\d]/g,'')"的方法”只能if和正则来判断,且不破坏原有逻辑,请问这里应该怎么写呢?没学过ts的语法格式,想到的是“if ...
  • python爬取数据时,单进程里开启多线程抓取,毕竟IO密集我就没搞多进程了。代码如下def get_downloads_url_list(self,pageNum):FilePath='C:/RMDZY/h'+str(pageNum)os.chdir(FilePath)with open(FilePath+'/m3u8...
  • 泛型从没怎么用上过;同时想熟悉下node,没有完整做过node的应用;并且想加深下自己对不同状态管理框架的理解。谢谢方方🙏</p><p>该提问来源于开源项目:FrankFang/fack</p></div>

空空如也

空空如也

1 2 3 4 5 ... 15
收藏数 286
精华内容 114
关键字:

ts怎么用