亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  C++ coroutine generator 實現筆記

C++ coroutine generator 實現筆記

時間:2024-02-15

本文授權自知乎用戶IceBear,點擊文末”閱讀原文“查看原文章。

C++20 給我們帶來了非?,F代化的協程特性,但是主要的增加都集中于語核部分。由于庫特性尚未準備充分,所以 C++20 標準庫中尚沒有多少現成的、組裝好的協程設施供我們使用。但是!僅僅通過使用 std::coroutine_handle (這是一個編譯器為之開洞的類)并在你的類中定制好幾個規定的接口,我們就可以組合出五花八門的功能。你可以理解為,雖然我們沒有現成的飛機、火車,但是我們有沙子和鐵礦石!完全可以從沙子和鐵礦石出發,造出飛機、火車。我知道很多人詬病 C++ 的這個特點,沒有現成的這個、現成的那個,自己造很麻煩。但是這也是我比較喜歡 C++ 的一點——上限非常高,你可以為自己的飛機、火車做定制,加上你想要的功能或去掉你不想要的功能;除此以外,你甚至還可以造出之前還沒有問世的東西,比如星艦!在其他語言中,語言和標準庫給你提供了什么就是什么了,你幾乎沒有超越的能力。

在 C++23 周期,LEWG (庫特性工作組) 在新冠肆虐的艱難背景下完成了大量的工作,使得 C++23 增添了不少很有益的設施(可參考 C++23特性總結 - 上 - Mick235711的文章 - 知乎 https://zhuanlan.zhihu.com/p/562383157)。但是,對于協程方面的內容還是舉棋不定。std::generator 和 std::lazy 在合并的計劃里進進出出,而最終只有 std::generator 達成目標。對于更花花綠綠的協程庫,像 task 等,則可憐的連提案都沒有。

另外,在可預見的將來,哪怕標準庫收錄了一些基本的協程類,為了探索更加花花綠綠的協程高級功能,我們還是需要從最基本的協程設施出發,也就是理解 std::coroutine_handle 和相應的接口。

本文將向讀者展示如何實現一個簡單的生成器 (generator) 和一個能支持協程內外雙向信息傳遞的生成器。網上關于什么是協程的講解很多,哪怕是講 C++ 協程的中文材料也不少。且因筆者時間精力有限,本文不會詳細地介紹協程概念,而是更側重于展示怎么實現出一個 generator,以填補相關參考資料的不足。

1

感性的了解一下協程
















協程可以簡單地理解為:一個執行時可以主動打斷,程序執行流程可以在調用方和被調用方之間進進出出若干次的函數。

Python 是最早的一批支持協程的語言,我們不妨用 Python 來演示一下協程的神奇。(其實早在 19 年,那時 C++ 編譯器還沒支持協程的時候,筆者就是利用 Python 來理解協程的)

從這個例子我們可以看出以下幾點怪異的現象:

1) myrange 函數中并沒有一句 return 語句,我們卻調用 myrange 函數得到了一個 gen 對象(第 22 行)

2) 22 行調用 myrange 函數后,這個函數似乎并沒有立即開始執行(沒有執行第 3 行的 print 語句,倒是執行第 23 行的 print 語句了)

3) 調用 gen 對象的 __next__ 方法后,myrange 函數開始執行。執行到第 7 行時,myrange 函數 "yield" 了一個值,然后程序的執行流程又切換到主函數的第 24 行。__next__ 方法得到了剛剛 "yield" 的結果。

4) 26 行再次調用 __next__ 時,執行流程回到了 myrange 中。而且并不是從 myrange 的開頭重新開始執行,而是從上一次 "yield" 的地方,也就是第 7 行繼續執行。i 的值似乎也沒受到影響。

如果你熟悉了協程的特點,這無非可以概括為,協程執行時可以主動打斷(更學術一點叫掛起)自己,將控制權交還給調用方。協程掛起期間,協程的棧上信息都可以得到保留。協程恢復后,從上一次的掛起點繼續執行。


經過封裝后的 C++ 協程庫,也可以向用戶展示出和 Python 中幾乎完全一致的用法。如下就是我們將要實現的 generator 所展示的應用效果。

當然,C++ 畢竟是一個靜態類型的語言,除了 range 函數要寫 “返回值” generator<int>(當然實際上 range 是一個協程,代碼 81 行只是借用了原來的函數和返回值語法了,嚴格來說不能認為這里聲明了一個返回 generator<int> 類型的函數) ,以及 90 行需要聲明 gen 的類型以外, 可以說和 Python 沒什么兩樣了。

2

std::coroutine_handle
















std::coroutine_handle 類模板是為我們實現協程的各種“魔法”提供支持的最底層的設施,其主要負責存儲協程的句柄。它分為模板 std::coroutine_handle<Promise> 和模板特化 std::coroutine_handle<void> ,std::coroutine_handle<void> 可以簡單理解為就是做了類型擦除的 std::coroutine_handle<Promise>

打開 <coroutine> 頭文件我們不難看出,std::coroutine_handle<Promise> 中的不少方法是依靠 __builtin 內建函數,也就是編譯器開洞實現的。

std::coroutine_handle 中保存了協程的上下文。我們協程執行到哪兒切出去了(協程切換回來后從哪兒開始繼續執行)?我們協程的棧上變量在協程切出去期間怎么能得到保留?這些問題的解決都是歸功于 std::coroutine_handle 保存了協程的上下文。

std::coroutine_handle 中的方法不多,但是各個都至關重要。由于有些概念還沒有鋪墊,我們先只羅列三個比較容易理解的方法:

done 方法,可以查詢一個協程是否已經結束;

resume 方法可以恢復一個協程的執行;

destroy 方法可以銷毀一個協程。

std::coroutine_handle 只是一個很底層很底層的設施,沒有 RAII 包裹。它就像裸指針一樣(其實它內部也就是一個裸指針),需要靠我們手動創建、手動銷毀。我們剛剛談到,std::coroutine_handle 保存了協程的上下文,其中就有棧上變量的信息。如果一個 handle 被創建出來,用完以后我們忘了對它調用 destroy 了,那么其中存儲的上下文信息當然也就沒有被銷毀——也就是內存泄漏了。如果不小心做了兩次 destroy,那么就可能會引發 double free 錯誤了。所以,我們得寫一個 RAII 類將其包裝起來,以解決忘記銷毀或者其他比如淺拷貝等問題。

這里,鄭重向大家推薦由清華大學出版社出版的《C++20 實踐入門》和《C++20 高級編程》。這兩本書是目前最新的一批介紹 C++20 的教程。該書緊跟潮流,就 C++20 的幾大熱點內容,如 modulesconcepts 、ranges 等作了詳細介紹。全卷內容質量上乘,精雕細琢,非那些在歷史舊版本的基礎上草草加兩章節新內容圈錢的書可比也!

非常感謝清華大學出版社對這篇文章的贊助!本著對我的讀者負責的態度,我堅持要求審讀完書的大部分內容后才能做推薦,清華大學出版社的編輯對此給予了高度支持。且,從 8 月份聯系我開始,到本文落筆,編輯非常寬容地給了我 4 個月的時間 —— 一段非常充足的時間閱讀了這兩本書,之后才有了這里的精心推薦。再次表示致敬和謝意!

3

generator
















我們的 generator 類終于出場了。

首先,C++ 的協程要求 generator 中必須有 promise_type 這個類型。你可以通過 typedef/using 的方式 alias 一個別名,也可以干脆就定義成 generator 的內部類 —— 本文選擇后者。

template <typename T>
struct generator
{
struct promise_type
{
}
};

promise_type 中有這么幾個可定制的、會影響協程行為的重要接口,先介紹兩個:

1) initial_suspend —— 它回答的是協程一出生時是否就要馬上掛起的問題;

2) final_suspend —— 它回答的是協程最后一次是否要掛起的問題;

對于一個 generator 而言,這兩個問題的回答是:

初始時始終都要掛起,最后一次始終都不掛起。

std::suspend_always std::suspend_never 是標準庫里面已經定義好的類型,可以方便地回答是否要掛起的問題。

    struct promise_type
{
...

std::suspend_always initial_suspend() const
{
return {};
}

std::suspend_never final_suspend() const noexcept
// 這里注意一下,由于 final_suspend 在收尾階段工作,所以必須是 noexcept 的
{
return {};
}
}

在新協程創建完畢好后,C++ 會執行 co_await promise.initial_suspend(),同樣的, 在協程結束前也會 co_await promise.final_suspend()。當然了,從名字中我們也能看出,co_await 一個 std::suspend_always 時,執行流程永遠都會無條件切出去,而對于 std::suspend_never 則是永遠也不會切出。

還記得我們剛剛觀察的 Python 代碼中的現象嗎?主函數中調用 myrange 的時候,是不是立馬得到一個 gen 對象的?是不是 myrange 里面沒有立即得到執行的?在第一次調用 __next__ 的時候才會去執行的吧?這其實就是因為 myrange 協程一創建好就掛起自己將程序流程切回到調用方了。

如果 initial_suspend 這里回答的是 suspend_never,那么協程就會立刻開始執行。

建議等 generator 實現完成后讀者自己動手實踐下,將 initial_suspend 和 final_suspend 的回答換換,看看結果會有什么改變。

3) promise_type 中第三個定制接口是 unhandled_exception,它回答的是協程被其里頭的沒有捕獲的異常終止時做何處理的問題。

我們這里只是簡單處理一下,調用 exit 提前終止程序。當然這樣的做法太簡化了,實際應用時可以考慮使用 std::exception_ptr 等設施做更嚴謹的處理。

    struct promise_type
{
...

void unhandled_exception()
{
std::exit(EXIT_FAILURE);
}
}

4) promise_type 中第四個定制接口,也是最核心的一個是 get_return_object。這個方法也涉及到了如何創建一個 coroutine 的問題 —— 答案就是使用 std::coroutine_handle<promise_type>::from_promise(*this),即從自己這個 promise ( 也就是*this) 創建一個 coroutine (from_promise 得到的就是一個 coroutine_handle)。generator 中也需要配合,提供一個接受 coroutine_handle 類型的構造函數,將剛剛構造出的 coroutine_handle 保存。

現在,通過get_return_object 得到了 return_object,就會開始詢問是否要做 initial_suspend 了 (剛剛介紹的 initial_suspend 還記得嗎?)

template <typename T>
struct generator
{
struct promise_type;

std::coroutine_handle<promise_type> handle;

struct promise_type
{
...
generator get_return_object()
{
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
};

generator(std::coroutine_handle<promise_type> handle) :
handle(handle)
{
}

...
};

我們之前也提到,coroutine_handle 是無 RAII 的,因此 generator 中得根據三/五法則,做好 RAII。該析構的析構,禁止拷貝,寫好移動構造/移動賦值。

template <typename T>
struct generator
{
struct promise_type;

std::coroutine_handle<promise_type> handle;

...

public:
generator() = default;
generator(const generator &) = delete;

private:
generator(std::coroutine_handle<promise_type> handle) :
handle(handle)
{
}

public:

generator(generator && src) :
handle(src.handle)
{
src.handle = nullptr;
}

generator& operator=(const generator &) = delete;

generator& operator=(generator && src)
{
if (!handle) {
handle.destroy();
}
handle = src.handle;
src.handle = nullptr;
}

~generator()
{
if (!handle) {
handle.destroy();
}
}

...
};

5) 定制 yield_value 接口

接下來的定制則對于 generator 來說至關重要,我們馬上就可以讓我們的 generator 支持 yield 了!

還是以此舉例,co_yield 關鍵字實際上只是一個語法糖,這一行會被編譯器替換為 co_await promise.yield_value(i),在有了 initial_suspend 和 final_suspend 的經驗后,我們這次也就能很容易地猜測出,我們要在 promise_type 中實現一個 yield_value 方法,而返回值負責回答要不要切出的問題。顯然,每次 yield 時總是要掛起協程,所以,yield_value 方法的返回值類型應當是 suspend_always。你猜對了嗎?

    struct promise_type
{
....

std::optional<T> opt;

template <typename Arg>
std::suspend_always yield_value(Arg && arg)
{
opt.emplace(std::forward<Arg>(arg));
return {};
}
};

在 promise 中,我們還增加了一個 optional,用以存放 yield 的結果。注意,很多例子,甚至包括 cppreference 在內,promise 內部都是用的 T 類型的裸值來存放 yield 的結果的。在模板編程中這樣做兼容性不太好,我們需要考慮能照顧到不可默認構造的類型。除此以外,我們使用萬能引用和完美轉發以提升構造值時的性能。

而這個 opt,當然是在等 generator 來取它的。

template <typename T>
struct generator
{
...

T & next()
{
handle.resume();
if (handle.done()) {
throw generator_done("generator done");
}
return *(handle.promise().opt);
}
};

generator<int> range(int n)
{
for(int i = 0; i < n; ++i) {
co_yield i;
}
}

int main()
{
generator<int> gen = range(4);

for (int i = 0; i < 4; ++i) {
std::cout << gen.next() << std::endl;
}
}

這里需要結合前文介紹過的內容梳理下。由于 initial_suspend 的返回值是 suspend_always,所以協程剛創建好后就切出,執行流程到了 gen = range(4) 。

再下面,每次對 gen 調用 next 方法時,會執行 handle.resume() 恢復協程。

協程首次恢復運行,當然是從 range 函數的開頭開始執行 (如果不是首次恢復運行,當然就是從上一次 yield 出去的地方恢復運行),直到碰上了 co_yield。這時, 調用 promise.yield_value(i),根據 co_yield 后面值 (也就是 i) 構造好了值保存在 opt 中。隨后,由于 promise.yield_value(i) 的結果是 suspend_always,所以協程切出, 執行流程回到了 handle.resume() 之后。正常情況下 (協程沒有執行完畢),next 方法就會從 promise 里的那個 optional 中取出 yield 的結果,返回給主函數中以供輸出。如果檢測到已經是最后一次 yield 后再調用 next 的 (即 resume 后檢測到 done 的話),則拋出 generator_done 異常。

完整的 generator 代碼如下:

#include <coroutine>
#include <optional>
#include <iostream>
#include <stdexcept>
#include <cstdlib>

struct generator_done :
std::logic_error
{
private:
typedef std::logic_error super;

public:
using super::super;
};

template <typename T>
struct generator
{
struct promise_type;

std::coroutine_handle<promise_type> handle;

struct promise_type
{
std::optional<T> opt;

std::suspend_always initial_suspend() const
{
return {};
}

std::suspend_never final_suspend() const noexcept
{
return {};
}

void unhandled_exception()
{
std::exit(EXIT_FAILURE);
}

generator get_return_object()
{
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}

template <typename Arg>
std::suspend_always yield_value(Arg && arg)
{
opt.emplace(std::forward<Arg>(arg));
return {};
}
};

public:
generator() = default;
generator(const generator &) = delete;

private:
generator(std::coroutine_handle<promise_type> handle) :
handle(handle)
{
}

public:
generator(generator && src) :
handle(src.handle)
{
src.handle = nullptr;
}

generator& operator=(const generator &) = delete;

generator& operator=(generator && src)
{
if (!handle) {
handle.destroy();
}
handle = src.handle;
src.handle = nullptr;
}


~generator()
{
if (!handle) {
handle.destroy();
}
}

T & next()
{
handle.resume();
if (handle.done()) {
throw generator_done("generator done");
}
return *(handle.promise().opt);
}

};

generator<int> range(int n)
{
for(int i = 0; i < n; ++i) {
co_yield i;
}
}

int main ()
{
generator<int> gen = range(5);

for (int i = 0; i < 5; ++i) {
std::cout << gen.next() << std::endl;
}

}


4

能雙向傳遞信息的

bigenerator
















我們目前完成的 generator 只能做到協程內部向外部 yield 一個值,傳遞出來信息。能不能做到外部也向協程內部回復一個值,將信息由外部傳遞到協程內部呢?C++ 的協程機制也是允許的。

其實,co_yield 表達式,當然,我們上面也知道了, 其實就是 co_await promise.yield_value() 這個表達式,其實也是有計算結果的,只不過,我們之前 generator 中的例子,計算結果為 void 類型 —— 沒有返回值罷了。

要想整個表達式有返回值,當然我們得從 promise.yield_value() 的返回值入手。我們以前用的是 std::suspend_always,現在得自己配置了。

先上效果:

再上源碼:

#include <coroutine>
#include <variant>
#include <iostream>
#include <stdexcept>
#include <cstdlib>

struct generator_done :
std::logic_error
{
private:
typedef std::logic_error super;

public:
using super::super;
};

template <typename T, typename U>
struct bigenerator
{
struct promise_type;

std::coroutine_handle<promise_type> handle;


struct awaitable : public std::suspend_always
{
std::variant<T, U> * ref;

U & await_resume() const
{
return std::get<U>(*ref);
}
};


struct promise_type
{
std::variant<T, U> var;

std::suspend_always initial_suspend() const
{
return {};
}

std::suspend_never final_suspend() const noexcept
{
return {};
}

void unhandled_exception()
{
std::exit(EXIT_FAILURE);
}

bigenerator get_return_object()
{
return bigenerator{std::coroutine_handle<promise_type>::from_promise(*this)};
}

template <typename Arg>
awaitable yield_value(Arg && arg)
{
var.template emplace<T>(std::forward<Arg>(arg));
return awaitable{.ref = &var};
}
};

public:
bigenerator() = default;
bigenerator(const bigenerator &) = delete;

private:
bigenerator(std::coroutine_handle<promise_type> handle) :
handle(handle)
{
}

public:
bigenerator(bigenerator && src) :
handle(src.handle)
{
src.handle = nullptr;
}

bigenerator& operator=(const bigenerator &) = delete;

bigenerator& operator=(bigenerator && src)
{
if (!handle) {
handle.destroy();
}
handle = src.handle;
src.handle = nullptr;
}


~bigenerator()
{
if (!handle) {
handle.destroy();
}
}

template <typename ... Args>
T & next(Args&& ... args)
{
handle.promise().var.template emplace<U>(std::forward<Args>(args)...);
handle.resume();
if (handle.done()) {
throw generator_done("generator done");
}
return std::get<T>(handle.promise().var);
}

};

bigenerator<int, std::string> range(int n)
{
for(int i = 0; i < n; ++i) {
std::string sunk = co_yield i;
std::cout << sunk << std::endl;
}
}

int main ()
{
bigenerator gen = range(10);

for (int i = 0; i < 5; ++i) {
std::cout << gen.next(i + 1, 'a') << std::endl;
}
}

然后講解:

主要變動就是一個新的內部類:awaitable ,在 bigenerator 中,yield_value 接口返回的就是這個類型。它繼承自 std::suspend_always,表明我們確實還是需要每次 yield 時都要掛起,但是,這里我們重寫了 await_resume 方法,使得協程在恢復時調用這個方法,從 promise 中取出外界傳遞進去的結果。

    struct awaitable : public std::suspend_always
{
std::variant<T, U> * ref;

U & await_resume() const
{
return std::get<U>(*ref);
}
};

下面的代碼片段展示了 yield_value 中怎么構造 awaitable。其實只要告知 promise 中的 variant 的地址就可以了。bigenerator 中改用了 variant,主要是考慮到 yield 出去的值和 resume 時傳遞進來的值不會在同一時刻存在,使用 variant 有助于節省空間。

    struct promise_type
{
...

std::variant<T, U> var;

template <typename Arg>
awaitable yield_value(Arg && arg)
{
var.template emplace<T>(std::forward<Arg>(arg));
return awaitable{.ref = &var};
}
};

還有 bigenerator 中 next 的變化,其實也就是恢復協程前,在 promise 的 variant 中構造好傳進去的對象就好了。

template <typename T, typename U>
struct bigenerator
{
...

template <typename ... Args>
T & next(Args&& ... args)
{
handle.promise().var.template emplace<U>(std::forward<Args>(args)...);
handle.resume();
if (handle.done()) {
throw generator_done("generator done");
}
return std::get<T>(handle.promise().var);
}
};

當然,我們這里沒有考慮到 bigenerator<T, T> 這種傳出來和傳進去的消息類型都一樣的情況,但其實只要做一下偏特化就可以了。由于不是協程部分的技術難點,就不再多介紹了。

全文完。

亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
日韩一区二区精品在线观看| 久久这里只有精品视频首页| 亚洲国产精品成人| 欧美视频在线免费看| 欧美高清在线观看| 奶水喷射视频一区| 一区二区欧美日韩| 国产精品一区二区欧美| 欧美日韩精品久久久| 性欧美xxxx大乳国产app| 91久久精品日日躁夜夜躁欧美| 欧美三级欧美一级| 欧美精品网站| 国产精品福利在线观看网址| 久久久久免费观看| 中文在线资源观看网站视频免费不卡 | 黑人一区二区三区四区五区| 99在线精品视频在线观看| 亚洲欧美激情诱惑| 欧美精品aa| 99精品视频免费| 夜夜嗨av一区二区三区中文字幕 | 欧美视频日韩视频| 欧美在线一二三| 久久尤物电影视频在线观看| 美女黄毛**国产精品啪啪| 欧美日韩亚洲国产一区| 久久蜜桃精品| 久久福利影视| 久久嫩草精品久久久久| 欧美韩国日本综合| 欧美视频在线一区二区三区| 国产日韩一区二区三区在线| 99国产精品| 欧美在线不卡| 欧美午夜宅男影院| 日韩一级精品| 亚洲国产三级在线| 久久精品视频在线播放| 久久一区二区三区四区五区| 亚洲人成网站在线播| 亚洲一区二区精品| 欧美日韩在线视频一区二区| 精品999网站| 午夜精品久久久久久久白皮肤| 欧美日韩在线大尺度| 亚洲国产婷婷香蕉久久久久久| 欧美精品三级日韩久久| 99精品视频免费观看视频| 一区二区在线观看视频| 狠狠色狠色综合曰曰| 亚洲精品日本| 亚洲视频观看| 欧美日韩亚洲不卡| 国产欧美日韩视频一区二区三区 | 欧美成人国产va精品日本一级| 亚洲国产精品一区二区www在线| 国产精品综合网站| 国产精品va在线| 欧美亚一区二区| 欧美视频不卡| 国产精品制服诱惑| 国产精品日韩精品| 亚洲一区二区精品| 欧美专区在线观看| 免费短视频成人日韩| 欧美日韩1区| 久久亚洲欧美国产精品乐播| 亚洲一区精品视频| av不卡免费看| 亚洲伊人观看| 蜜臀a∨国产成人精品| 久久久久久9999| 欧美成人亚洲成人| 欧美精品在线播放| 国产免费成人av| 在线精品视频一区二区三四| 国内一区二区三区在线视频| 激情成人在线视频| 日韩一级裸体免费视频| 午夜在线观看免费一区| 久热精品在线视频| 国产伦理一区| 日韩写真在线| 久久精品国产亚洲aⅴ| 欧美日韩国产va另类| 原创国产精品91| 亚洲影院色无极综合| 欧美寡妇偷汉性猛交| 国产日韩在线播放| 亚洲一区二区三区色| 欧美精品手机在线| 亚洲日本免费电影| 美女免费视频一区| 黄色精品在线看| 久久成人国产精品| 国产精品视频男人的天堂| 99热精品在线| 欧美日韩视频一区二区三区| 亚洲区中文字幕| 欧美成人综合| 在线视频亚洲欧美| 国产精品蜜臀在线观看| 亚洲一区二区三区乱码aⅴ蜜桃女 亚洲一区二区三区乱码aⅴ | 国产精品捆绑调教| 洋洋av久久久久久久一区| 欧美精品粉嫩高潮一区二区| 一区二区在线观看av| 久久精品99国产精品日本 | 欧美四级在线观看| 国产午夜精品久久| 亚洲精品字幕| 欧美一区在线直播| 日韩一区二区电影网| 黄色成人精品网站| 国产综合久久| 国产亚洲精品美女| 亚洲图片在区色| 亚洲精品国产精品国自产观看浪潮 | 欧美日韩国产精品成人| 久久人人97超碰精品888| 久久亚洲午夜电影| 欧美成人激情视频| 欧美日韩在线视频一区二区| 国产精品伦理| 尤妮丝一区二区裸体视频| 激情一区二区三区| 99视频+国产日韩欧美| 亚洲性xxxx| 久久免费黄色| 欧美日韩二区三区| 国产欧美精品久久| 亚洲激情av在线| 亚洲综合视频1区| 欧美成人首页| 国产精品久久久一本精品| 国产综合色产| 亚洲一区二区三区在线| 久久久精品一品道一区| 欧美日韩喷水| 影音先锋亚洲视频| 亚洲欧美日韩国产中文| 暖暖成人免费视频| 国产一区二区三区黄| 99精品欧美一区二区三区综合在线| 午夜免费在线观看精品视频| 欧美成人资源网| 国产色婷婷国产综合在线理论片a| 91久久精品www人人做人人爽| 亚洲在线成人| 欧美日韩国产综合网| 亚洲国产成人tv| 欧美专区亚洲专区| 国产精品婷婷午夜在线观看| 亚洲国产精品尤物yw在线观看| 欧美一级久久久| 国产精品乱看| 亚洲私人影院| 欧美日韩国产综合视频在线观看中文 | 国产亚洲精品7777| 亚洲午夜一区二区| 欧美精品成人在线| 亚洲高清二区| 久久午夜羞羞影院免费观看| 国产精品v亚洲精品v日韩精品| 91久久精品一区| 欧美1区2区| 亚洲福利国产精品| 久久综合中文| 亚洲国产一区二区在线| 久久亚洲精品一区| 一色屋精品视频在线看| 欧美一区综合| 国产一区二区三区免费在线观看| 亚洲欧美精品中文字幕在线| 欧美日韩综合精品| 亚洲一区影音先锋| 国产精品麻豆欧美日韩ww | 国产精品推荐精品| 亚洲嫩草精品久久| 国产精品一二一区| 午夜老司机精品| 国产欧美韩日| 久久人91精品久久久久久不卡| 国产亚洲精品久久久久婷婷瑜伽| 午夜精品一区二区三区在线| 国产伦精品一区二区| 欧美一区二区成人| 国产一区日韩欧美| 免费日韩一区二区| 亚洲看片一区| 欧美日韩亚洲综合一区| 亚洲深夜激情| 国产婷婷一区二区| 久热精品视频在线| 亚洲看片免费| 国产精品一区二区欧美| 久久久亚洲精品一区二区三区| 在线精品国产欧美| 欧美日在线观看|