青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

大龍的博客

常用鏈接

統計

最新評論

volatile編寫多線程程序的好幫手

來源:?作者:lphpc?2006-05-16?出處:巧巧讀書?
?新聞??效果??數據結構??設計??操作系統??

?????? 并不是我故意想弄糟你的心情,但是在這期專欄里,我們將討論多線程編程這一話題。正如上一期Generic里所說的,編寫異常安全(exception-safe)的程序是非常困難的,但是和編寫多線程程序比起來,那簡直就是兒戲。

?????? 多線程的程序是出了名的難編寫、難驗證、難調試、難維護,這通常是件苦差事。不正確的多線程程序可能可以運行很多年也不出一點錯,直到滿足某些臨界的條件時,才出現意想不到的奇怪錯誤。

?????? 不用說,編寫多線程程序的程序員需要使用可能得到的所有幫助。這期專欄將專注于討論競爭條件(race conditions)——這通常是多線程程序中各種麻煩的根源——深入了解它并提供一些工具來防止競爭。令人驚異的是,我們將讓編譯器盡其所能來幫助你做這些事。


?????? 僅僅一個不起眼的關鍵字。
????? 盡管C和C++標準對于線程都明顯的“保持沉默”,但它們以volatile關鍵字的形式,確實為多線程保留了一點特權。

?????? 就象大家更熟悉的const一樣,volatile是一個類型修飾符(type modifier)。它是被設計用來修飾被不同線程訪問和修改的變量。如果沒有volatile,基本上會導致這樣的結果:要么無法編寫多線程程序,要么編譯器失去大量優化的機會。下面我們來一個個說明。

考慮下面的代碼:

代碼:

class Gadget
{
public:
void Wait()
{
while (!flag_)
{
Sleep(1000); // sleeps for 1000 milliseconds
}
}
void Wakeup()
{
flag_ = true;
}
...
private:
bool flag_;
};





上面代碼中Gadget::Wait的目的是每過一秒鐘去檢查一下flag_成員變量,當flag_被另一個線程設為true時,該函數才會返回。至少這是程序作者的意圖,然而,這個Wait函數是錯誤的。
假設編譯器發現Sleep(1000)是調用一個外部的庫函數,它不會改變成員變量flag_,那么編譯器就可以斷定它可以把flag_緩存在寄存器中,以后可以訪問該寄存器來代替訪問較慢的主板上的內存。這對于單線程代碼來說是一個很好的優化,但是在現在這種情況下,它破壞了程序的正確性:當你調用了某個Gadget的Wait函數后,即使另一個線程調用了Wakeup,Wait還是會一直循環下去。這是因為flag_的改變沒有反映到緩存它的寄存器中去。編譯器的優化未免有點太……樂觀了。

在大多數情況下,把變量緩存在寄存器中是一個非常有價值的優化方法,如果不用的話很可惜。C和C++給你提供了顯式禁用這種緩存優化的機會。如果你聲明變量是使用了volatile修飾符,編譯器就不會把這個變量緩存在寄存器里——每次訪問都將去存取變量在內存中的實際位置。這樣你要對Gadget的Wait/Wakeup做的修改就是給flag_加上正確的修飾:

class Gadget
{
public:
... as above ...
private:
volatile bool flag_;
};

大多數關于volatile的原理和用法的解釋就到此為止,并且建議你用volatile修飾在多個線程中使用的原生類型變量。然而,你可以用volatile做更多的事,因為它是神奇的C++類型系統的一部分。


把volatile用于自定義類型
volatile修飾不僅可以用于原生類型,也可以用于自定義類型。這時候,volatile修飾方式類似于const(你也可以對一個類型同時使用const和volatile)。

與const不同,volatile的作用對于原生類型和自定義類型是有區別的。就是說,原生類型有volatile修飾時,仍然支持它們的各種操作(加、乘、賦值等等),然而對于class來說,就不是這樣。舉例來說,你可以把一個非volatile的int的值賦給一個volatile的int,但是你不能把一個非volatile的對象賦給一個volatile對象。

讓我們舉個例子來說明自定義類型的volatile是怎么工作的。
代碼:

class Gadget
{
public:
void Foo() volatile;
void Bar();
...
private:
String name_;
int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;




如果你認為volatile對于對象來說沒有什么作用的話,那你可要大吃一驚了。
volatileGadget.Foo(); // ok, volatile fun called for
// volatile object
regularGadget.Foo(); // ok, volatile fun called for
// non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
// volatile object!

從沒有volatile修飾的類型到相應的volatile類型的轉換是很平常的。但是,就象const一樣,你不能反過來把volatile類型轉換為非volatile類型。你必須用類型轉換運算符:
Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

一個有volatile修飾的類只允許訪問其接口的一個子集,這個子集由類的實現者來控制。用戶只有用const_cast才可以訪問這個類型的全部接口。而且,象const一樣,類的volatile屬性會傳遞給它的成員(例如,volatileGadget.name_和volatileGadget.state_也是volatile變量)。

volatile,臨界區和競爭條件
多線程程序中最簡單也是最常用的同步機制要算是mutex(互斥對象)了。一個mutex只提供兩個基本操作:Acquire和Release。一旦某個線程調用了Acquire,其他線程再調用Acquire時就會被阻塞。當這個線程調用Release后,剛才阻塞在Acquire里的線程中,會有一個且僅有一個被喚醒。換句話說,對于一個給定的mutex,只有一個線程可以在Acquire和Release調用之間獲取處理器時間。在Acquire和Release調用之間執行的代碼叫做臨界區(critical section)。(Windows的用語可能會引起一點混亂,因為Windows把mutex本身叫做臨界區,而Windows的mutex實際上指進程間的mutex。如果把它們分別叫作線程mutex和進程mutex可能會好些。)

Mutex是用來避免數據出現競爭條件。根據定義,所謂競爭條件就是這樣一種情況:多個線程對數據產生的作用要依賴于線程的調度順序的。當兩個線程競相訪問同一數據時,就會發生競爭條件。因為一個線程可以在任意一個時刻打斷其他線程,數據可能會被破壞或者被錯誤地解釋。因此,對數據的修改操作,以及有些情況下的訪問操作,必須用臨界區保護起來。在面向對象的編程中,這通常意味著你在一個類的成員變量中保存一個mutex,然后在你訪問這個類的狀態時使用這個mutex。

多線程編程高手看了上面兩個段落,可能已經在打哈欠了,但是它們的目的只是提供一個準備練習,我們現在要和volatile聯系起來了。我們將把C++的類型和線程的語義作一個對比。


在一個臨界區以外,任意線程會在任何時間打斷別的線程;這是不受控制的,所以被多個線程訪問的變量容易被改得面目全非。這和volatile的原意[1]是一致的——所以需要用volatile來防止編譯器無意地緩存這樣的變量。

在由一個mutex限定的臨界區里,只有一個線程可以進入。因此,在臨界區中執行的代碼有和單線程程序有相同的語義。被控制的變量不會再被意外改變——你可以去掉volatile修飾。

簡而言之,線程間共享的數據在臨界區之外是volatile的,而在臨界區之內則不是。

你通過對一個mutex加鎖來進入一個臨界區,然后你用const_cast去掉某個類型的volatile修飾,如果我們能成功地把這兩個操作放到一起,那么我們就在C++類型系統和應用程序的線程語義建立起聯系。這樣我們可以讓編譯器來幫我們檢測競爭條件。


LockingPtr
我們需要有一個工具來做mutex的獲取和const_cast兩個操作。讓我們來設計一個LockingPtr類,你需要用一個volatile的對象obj和一個mutex對象mtx來初始化它。在LockingPtr對象的生命期中,它會保證mtx處于被獲取狀態,而且也提供對去掉volatile修飾的obj的訪問。對obj的訪問類似于smart pointer,是通過operator->和operator*來進行的。const_cast是在LockingPtr內部進行。這個轉化在語義上是正確的,因為LockingPtr在其生存期中始終擁有mutex。

首先,我們來定義和LockingPtr一起工作的Mutex類的框架:

代碼:

class Mutex
{
public:
void Acquire();
void Release();
...
};




為了使用LockingPtr,你需要用操作系統提供的數據結構和底層函數來實現Mutex。
LockingPtr是一個模板,用被控制變量的類型作為模板參數。例如,如果你希望控制一個Widget,你就要這樣寫LockingPtr <Widget>。

LockingPtr的定義很簡單,它只是實現了一個單純的smart pointer。它關注的焦點只是在于把const_cast和臨界區操作放在一起。

代碼:

template <typename T>
class LockingPtr {
public:
// Constructors/destructors
LockingPtr(volatile T& obj, Mutex& mtx)
: pObj_(const_cast<T*>(&obj)),
pMtx_(&mtx)
{ mtx.Lock(); }
~LockingPtr()
{ pMtx_->Unlock(); }
// Pointer behavior
T& operator*()
{ return *pObj_; }
T* operator->()
{ return pObj_; }
private:
T* pObj_;
Mutex* pMtx_;
LockingPtr(const LockingPtr&);
LockingPtr& operator=(const LockingPtr&);
};




盡管很簡單,LockingPtr對于編寫正確的多線程代碼非常有用。你應該把線程間共享的對象聲明為volatile,但是永遠不要對它們使用const_cast——你應該始終是用LockingPtr的自動對象(automatic objects)。讓我們舉例來說明。

比如說你有兩個線程需要共享一個vector<char>對象:

代碼:


class SyncBuf {
public:
void Thread1();
void Thread2();
private:
typedef vector<char> BufT;
volatile BufT buffer_;
Mutex mtx_; // controls access to buffer_
};




在一個線程的函數里,你只需要簡單地使用一個LockingPtr<BufT>對象來獲取對buffer_成員變量的受控訪問:

代碼:

void SyncBuf::Thread1() {
LockingPtr<BufT> lpBuf(buffer_, mtx_);
BufT::iterator i = lpBuf->begin();
for (; i != lpBuf->end(); ++i) {
... use *i ...
}
}



這樣的代碼很容易編寫,也很容易理解——每當你需要使用buffer_時,你必須創建一個LockingPtr<BufT>來指向它。當你這樣做了以后,你就可以訪問vector的全部接口。

這個方法的好處是,如果你犯了錯誤,編譯器會指出它:
代碼:

void SyncBuf::Thread2() {
// Error! Cannot access 'begin' for a volatile object
BufT::iterator i = buffer_.begin();
// Error! Cannot access 'end' for a volatile object
for (; i != lpBuf->end(); ++i) {
... use *i ...
}
}




你不能訪問buffer_的任何函數,除非你進行了const_cast或者用LockingPtr。這兩者的區別是LockingPtr提供了一個有規則的方法來對一個volatile變量進行const_cast。
LockingPtr有非常好的表達力。如果你只需要調用一個函數,你可以創建一個無名的臨時LockingPtr對象,然后直接使用它:

代碼:

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}




回到原生類型
我們已經看到了volatile對于保護對象免于不受控的訪問是多么出色,并且看到了LockingPtr是怎么提供了一個簡單有效的辦法來編寫線程安全的代碼。現在讓我們回到原生類型,volatile對它們的作用方式是不同的。

讓我們來考慮一個多個線程共享一個int變量的例子。

代碼:

class Counter
{
public:
...
void Increment() { ++ctr_; }
void Decrement() { --ctr_; }
private:
int ctr_;
};




如果Increment和Decrement是在不同的線程里被調用的,上面的代碼片斷里就有bug。首先,ctr_必須是volatile的。其次,即使是一個看上去是原子的操作,比如++ctr_,實際上也分為三個階段。內存本身是沒有運算功能的,當對一個變量進行增量操作時,處理器會:
把變量讀入寄存器
對寄存器里的值加1
把結果寫回內存
這個三步操作稱為RMW(Read-Modify-Write)。在一個RMW操作的Modify階段,大多數處理器都會釋放內存總線,以使其他處理器能夠訪問內存。
如果在這個時候另一個處理器對同一個變量也進行RMW操作,我們就遇到了一個競爭條件:第二次寫入會覆蓋掉第一次的值。

為了防止這樣的事發生,你又要用到LockingPtr:

代碼:

class Counter
{
public:
...
void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
void Decrement() { --*LockingPtr<int>(ctr_, mtx_); }
private:
volatile int ctr_;
Mutex mtx_;
};



現在這段代碼正確了,但是和SyncBuf相比,這段代碼的質量要差一些。為什么?因為對于Counter,編譯器不會在你錯誤地直接訪問ctr_(沒有對它加鎖)時產生警告。雖然ctr_是volatile的,但是編譯器還是可以編譯++ctr_,盡管產生的代碼絕對是不正確的。編譯器不再是你的盟友了,你只有自己留意競爭條件。
那么你該怎么做呢?很簡單,你可以用一個高層的結構來包裝原生類型的數據,然后對那個結構使用volatile。這有點自相矛盾,直接用volatile修飾原生類型是一個不好的用法,盡管這是volatile最初期望的用法!


volatile成員函數
到現在為止,我們討論了具有volatile數據成員的類;現在讓我們來考慮設計這樣的類,它會作為更大的對象的一部分并且在線程間共享。這里,volatile的成員函數可以幫很大的忙。

在設計類的時候,你只對那些線程安全的成員函數加volatile修飾。你必須假定外面的代碼會在任何地方任何時間調用volatile成員函數。不要忘記:volatile相當于自由的多線程代碼,并且沒有臨界區;非volatile相當于單線程的環境或者在臨界區內。

比如說,你定義了一個Widget類,它用兩個方法實現了同一個操作——一個線程安全的方法和一個快速的不受保護的方法。

代碼:

class Widget
{
public:
void Operation() volatile;
void Operation();
...
private:
Mutex mtx_;
};



注意這里的重載(overloading)用法。現在Widget的用戶可以用一致的語法調用Operation,對于volatile對象可以得到線程安全性,對于普通對象可以得到速度。用戶必須注意把共享的Widget對象定義為volatile。
在實現volatile成員函數時,第一個操作通常是用LockingPtr對this進行加鎖,然后其余工作可以交給非volatile的同名函數做:

代碼:

void Widget::Operation() volatile
{
LockingPtr<Widget> lpThis(*this, mtx_);
lpThis->Operation(); // invokes the non-volatile function
}





小結
在編寫對線程程序的時候,使用volatile將對你十分有益。你必須堅持下面的規則:

把所有共享對象聲明為volatile
不要對原生類型直接使用volatile
定義共享類時,用volatile成員函數來表示它的線程安全性。
如果你這么做了,而且用了簡單的通用組件LockingPtr,你就可以寫出線程安全的代碼,并且大大減少對競爭條件的擔心,因為編譯器會替你操心,并且勤勤懇懇地為你指出哪里錯了。

在我參與的幾個項目中,使用volatile和LockingPtr產生了很大效果。代碼十分整潔,也容易理解。我記得遇到過一些死鎖的情況,但是相對于競爭條件,我寧愿對付死鎖的情況,因為它們調試起來容易多了。那些項目實際上根本沒有碰到過有關競爭條件的問題。

致謝
非常感謝James Kanze和Sorin Jianu提供了很有洞察力的意見。


致謝
非常感謝James Kanze和Sorin Jianu提供了很有洞察力的意見。

附:濫用volatile的本質?[2]
在上一期的專欄《Generic<Programming>: volatile — Multithreaded Programmer's Best Friend》發表以后,我收到很多反饋意見。就像是注定的一樣,大部分稱贊都是私人信件,而抱怨都發到USENET新聞組comp.lang.c++.moderated和 comp.programming.threads里去了。隨后引起了很長很激烈的討論,如果你對這個主題有興趣,你可以去看看這個討論,它的標題是“volatile, was: memory visibility between threads.”。

我知道我從這個討論中學到了很多東西。比如說,文章開頭的Widget的例子不太切題。長話短說,在很多系統(比如POSIX兼容的系統)中,volatile修飾是不需要的,而在另一些系統中,即使加了volatile也沒有用,程序還是不正確。

關于volatile correctness,最重要的一個問題是它依賴于類似POSIX的mutex,如果在多處理器系統上,光靠mutex就不夠了——你必須用memory barriers。

另一個更哲理性的問題是:嚴格來說通過類型轉換把變量的volatile屬性去掉是不合法的,即使volatile屬性是你自己為了volatile correctness而加上去的。正如Anthony Williams指出的,可以想象一個系統可能把volatile數據放在一個不同于非volatile數據的存儲區中,在這種情況下,進行地址變換會有不確定的行為。

另一個批評是volatile correctness雖然可以在一個較低層次上解決競爭條件,但是不能正確的檢測出高層的、邏輯的競爭條件。例如,你有一個mt_vector模版類,用來模擬std::vector,成員函數經過正確的線程同步修正。考慮這段代碼:

volatile mt_vector<int> vec;

if (!vec.empty()) {
vec.pop_back();
}

這段代碼的目的是刪除vector里的最后一個元素,如果它存在的話。在單線程環境里,他工作地很好。然而如果你把它用在多線程程序里,這段代碼還是有可能拋出異常,盡管empty和pop_back都有正確的線程同步行為。雖然底層的數據(vec)的一致性有保證,但是高層操作的結果還是不確定的。
無論如何,經過辯論之后,我還是保持我的建議,在有類POSIX的mutex的系統上,volatile correctness還是檢測競爭條件的一個有價值的工具。但是如果你在一個支持內存訪問重新排序的多處理器系統上,你首先需要仔細閱讀你的編譯器的文檔。你必須知己知彼。

最后,Kenneth Chiu提到了一篇非常有趣的文章http://theory.stanford.edu/~freunds/race.ps,猜猜題目是什么?“Type-Based Race Detection for Java”。這篇文章講了怎么對Java的類型系統作一點小小的補充,從而讓編譯器和程序員一起在編譯時檢測競爭條件。

posted on 2006-12-05 12:06 大龍 閱讀(178) 評論(0)  編輯 收藏 引用


只有注冊用戶登錄后才能發表評論。
網站導航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            国产精品久久久久一区二区三区| 欧美日韩精品一区视频| 国模精品娜娜一二三区| 欧美一区二视频在线免费观看| 日韩网站在线看片你懂的| 欧美日韩国产123区| 在线视频欧美一区| 一区二区三区欧美激情| 欧美亚洲成人精品| 性色一区二区| 久久久久一区二区三区四区| 在线国产精品一区| 亚洲黄一区二区三区| 欧美日产国产成人免费图片| 亚洲已满18点击进入久久| 亚洲欧美久久久| 含羞草久久爱69一区| 亚洲国产欧美日韩另类综合| 欧美三级在线视频| 久久国产精品久久w女人spa| 久久亚洲精品网站| 亚洲一区二区三区777| 欧美一区日韩一区| 99热在线精品观看| 亚洲一区二区三区四区中文| 黄色一区二区在线| 99亚洲一区二区| 韩日在线一区| 一区二区免费在线观看| 狠狠色狠狠色综合日日五| 亚洲欧洲精品一区二区三区不卡 | 久久综合给合久久狠狠色| 亚洲精选一区| 欧美一级夜夜爽| 亚洲乱码久久| 欧美影院视频| 亚洲一区二区毛片| 麻豆成人在线| 久久亚洲风情| 国产精品久久久久久影院8一贰佰| 久久一区二区精品| 欧美午夜精品久久久久免费视| 久久综合给合久久狠狠色 | 欧美在线视频免费观看| 一区二区三区国产在线观看| 久久国产欧美| 亚洲一区二区网站| 欧美国产日韩一二三区| 久久一区二区三区四区| 欧美日韩在线直播| 欧美激情亚洲视频| 狠狠狠色丁香婷婷综合激情| 日韩视频精品| 一区二区三区欧美在线观看| 免费在线看一区| 免费看精品久久片| 国内精品伊人久久久久av一坑| 日韩亚洲综合在线| 99国产精品国产精品久久| 卡一卡二国产精品| 久久综合一区二区| 一区二区三区在线观看欧美| 午夜在线成人av| 欧美一区二区性| 国产精品五月天| 亚洲一级二级| 亚洲欧美日韩精品久久久久| 欧美亚州一区二区三区| 一区二区三区国产| 亚洲一区二区三区中文字幕在线| 欧美激情久久久久久| 亚洲国产精品电影在线观看| 在线免费观看日本欧美| 久久综合一区二区| 亚洲大胆人体视频| 亚洲黄色在线看| 欧美成人亚洲成人| 99国产精品久久| 亚洲一区视频在线| 国产精品欧美风情| 欧美一区二视频| 巨胸喷奶水www久久久免费动漫| 一区免费视频| 欧美凹凸一区二区三区视频| 亚洲国产精品日韩| 一本色道久久加勒比精品| 欧美日韩一区二区三区| 亚洲欧美日韩国产综合在线| 久久精品亚洲一区二区三区浴池 | 女主播福利一区| 亚洲国产经典视频| 亚洲一区二区三区四区五区黄 | 欧美精品七区| 宅男在线国产精品| 久久九九免费视频| 亚洲国产一区二区视频| 欧美日韩二区三区| 欧美一区二区三区精品电影| 蜜臀99久久精品久久久久久软件| 亚洲国产一区二区精品专区| 欧美视频在线观看 亚洲欧| 一区二区三区四区国产| 久久综合色播五月| 亚洲天堂偷拍| 一区在线影院| 欧美日韩一区二区三区四区在线观看| 亚洲欧美怡红院| 亚洲激情视频在线| 久久精品成人一区二区三区蜜臀| 精品99视频| 国产精品国产福利国产秒拍| 久久国产精品99精品国产| 亚洲福利在线看| 欧美专区中文字幕| 亚洲视频综合在线| 亚洲国产精品v| 国产欧美一区二区精品婷婷| 免费国产自线拍一欧美视频| 亚洲欧美资源在线| 91久久精品国产91性色| 久久综合色一综合色88| 亚洲一区二区三区四区五区黄| 在线日韩av片| 国产三区二区一区久久| 欧美日韩免费观看中文| 久久综合999| 午夜精品久久一牛影视| 99视频一区| 亚洲黄色影院| 欧美电影美腿模特1979在线看| 欧美一区二区免费观在线| 一本色道久久综合亚洲精品高清| 亚洲大片一区二区三区| 国产一区二区三区四区老人| 国产精品毛片| 欧美日韩免费在线视频| 欧美国产三区| 欧美成人精品激情在线观看| 日韩午夜在线| 亚洲福利视频网站| 亚洲欧美日韩精品| 亚洲美女电影在线| 91久久黄色| 在线播放日韩专区| 在线观看欧美黄色| 久久另类ts人妖一区二区| 欧美一区影院| 欧美日韩大陆在线| 黄色另类av| 一区二区日韩欧美| 国产精品乱码| 欧美日韩国产在线播放| 欧美激情综合亚洲一二区| 欧美va天堂| 欧美区日韩区| 欧美视频一区在线| 久久只有精品| 蜜桃久久精品乱码一区二区| 国产精品国产三级国产普通话99| 在线观看av一区| 午夜日韩福利| 亚洲高清三级视频| 午夜在线成人av| 欧美视频中文字幕在线| 亚洲电影成人| 久久精品卡一| 中日韩午夜理伦电影免费| 免费一级欧美片在线观看| 国产视频一区在线观看一区免费| 亚洲经典视频在线观看| 久久漫画官网| 亚洲欧美成人一区二区在线电影 | 久久午夜激情| 国产精品亚洲不卡a| 99国产精品99久久久久久| 蘑菇福利视频一区播放| 欧美在线视频免费| 国产欧美韩日| 亚洲欧洲av一区二区三区久久| 亚洲激情综合| 欧美高清成人| 亚洲精品一区二区三区99| 欧美成年人网| 老司机aⅴ在线精品导航| 黄页网站一区| 久久影院午夜片一区| 欧美一区二区三区免费在线看| 国产精品久久久久久影院8一贰佰| 99视频精品免费观看| 亚洲欧洲在线视频| 亚洲精品乱码久久久久久按摩观| 亚洲精品国产精品乱码不99按摩| 亚洲欧美日本伦理| 欧美激情在线有限公司| 国产亚洲观看| 亚洲调教视频在线观看| 美腿丝袜亚洲色图| 亚洲欧美日韩在线高清直播| 欧美韩国一区| 在线观看中文字幕不卡|