在C++11之前,涉及到多線程問題,都是和平臺相關(guān)的,比如Windows和Linux下各有自己的接口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進(jìn)行了支持,使得C++在并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。
線程庫(thread) 線程對象的構(gòu)造方式一、調(diào)用無參的構(gòu)造函數(shù)
thread提供了無參的構(gòu)造函數(shù),調(diào)用無參的構(gòu)造函數(shù)創(chuàng)建出來的線程對象沒有關(guān)聯(lián)任何線程函數(shù),即沒有啟動任何線程。比如:
thread t1;
由于thread提供了移動賦值函數(shù),因此當(dāng)后續(xù)需要讓該線程對象與線程函數(shù)關(guān)聯(lián)時,可以以帶參的方式創(chuàng)建一個匿名對象,然后調(diào)用移動賦值將該匿名對象關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移給該線程對象。比如:
void func(int n)
{for (int i = 0; i<= n; i++)
{cout<< i<< endl;
}
}
int main()
{thread t1;
//...
t1 = thread(func, 10);
t1.join();
return 0;
}
場景: 實(shí)現(xiàn)線程池的時候就是需要先創(chuàng)建一批線程,但一開始這些線程什么也不做,當(dāng)有任務(wù)到來時再讓這些線程來處理這些任務(wù)。
二、調(diào)用帶參的構(gòu)造函數(shù)
thread的帶參的構(gòu)造函數(shù)的定義如下:
templateexplicit thread (Fn&& fn, Args&&... args);
參數(shù)說明:
fn
:可調(diào)用對象,比如函數(shù)指針、仿函數(shù)、lambda表達(dá)式、被包裝器包裝后的可調(diào)用對象等。args...
:調(diào)用可調(diào)用對象fn時所需要的若干參數(shù)。調(diào)用帶參的構(gòu)造函數(shù)創(chuàng)建線程對象,能夠?qū)⒕€程對象與線程函數(shù)fn進(jìn)行關(guān)聯(lián)。比如:
void func(int n)
{for (int i = 0; i<= n; i++)
{cout<< i<< endl;
}
}
int main()
{thread t2(func, 10);
t2.join();
return 0;
}
三、調(diào)用移動構(gòu)造函數(shù)
thread提供了移動構(gòu)造函數(shù),能夠用一個右值線程對象來構(gòu)造一個線程對象。比如:
void func(int n)
{for (int i = 0; i<= n; i++)
{cout<< i<< endl;
}
}
int main()
{thread t3 = thread(func, 10);
t3.join();
return 0;
}
說明一下:
thread中常用的成員函數(shù)如下:
成員函數(shù) | 功能 |
---|---|
join | 對該線程進(jìn)行等待,在等待的線程返回之前,調(diào)用join函數(shù)的線程將會被阻塞 |
joinable | 判斷該線程是否已經(jīng)執(zhí)行完畢,如果是則返回true,否則返回false |
detach | 將該線程與創(chuàng)建線程進(jìn)行分離,被分離后的線程不再需要創(chuàng)建線程調(diào)用join函數(shù)對其進(jìn)行等待 |
get_id | 獲取該線程的id |
swap | 將兩個線程對象關(guān)聯(lián)線程的狀態(tài)進(jìn)行交換 |
此外,joinable
函數(shù)還可以用于判定線程是否是有效的,如果是以下任意情況,則線程無效:
調(diào)用thread的成員函數(shù)get_id
可以獲取線程的id,但該方法必須通過線程對象來調(diào)用get_id
函數(shù),如果要在線程對象關(guān)聯(lián)的線程函數(shù)中獲取線程id,可以調(diào)用this_thread
命名空間下的get_id
函數(shù)。比如:
void func()
{cout<< this_thread::get_id()<< endl; //獲取線程id
}
int main()
{thread t(func);
t.join();
return 0;
}
this_thread
命名空間中還提供了以下三個函數(shù):
函數(shù)名 | 功能 |
---|---|
yield | 當(dāng)前線程“放棄”執(zhí)行,讓操作系統(tǒng)調(diào)度另一線程繼續(xù)執(zhí)行 |
sleep_until | 讓當(dāng)前線程休眠到一個具體時間點(diǎn) |
sleep_for | 讓當(dāng)前線程休眠一個時間段 |
線程函數(shù)的參數(shù)是以值拷貝的方式拷貝到線程??臻g中的,就算線程函數(shù)的參數(shù)為引用類型,在線程函數(shù)中修改后也不會影響到外部實(shí)參,因?yàn)槠鋵?shí)際引用的是線程棧中的拷貝,而不是外部實(shí)參。比如:
void add(int& num)
{num++;
}
int main()
{int num = 0;
thread t(add, num);
t.join();
cout<< num<< endl; //0
return 0;
}
如果要通過線程函數(shù)的形參改變外部的實(shí)參,可以參考以下三種方式:
方式一:借助std::ref函數(shù)
當(dāng)線程函數(shù)的參數(shù)類型為引用類型時,如果要想線程函數(shù)形參引用的是外部傳入的實(shí)參,而不是線程??臻g中的拷貝,那么在傳入實(shí)參時需要借助ref函數(shù)保持對實(shí)參的引用。比如:
void add(int& num)
{num++;
}
int main()
{int num = 0;
thread t(add, ref(num));
t.join();
cout<< num<< endl; //1
return 0;
}
方式二:地址的拷貝
將線程函數(shù)的參數(shù)類型改為指針類型,將實(shí)參的地址傳入線程函數(shù),此時在線程函數(shù)中可以通過修改該地址處的變量,進(jìn)而影響到外部實(shí)參。比如:
void add(int* num)
{(*num)++;
}
int main()
{int num = 0;
thread t(add, &num);
t.join();
cout<< num<< endl; //1
return 0;
}
方式三:借助lambda表達(dá)式
將lambda表達(dá)式作為線程函數(shù),利用lambda函數(shù)的捕捉列表,以引用的方式對外部實(shí)參進(jìn)行捕捉,此時在lambda表達(dá)式中對形參的修改也能影響到外部實(shí)參。比如:
int main()
{int num = 0;
thread t([&num]{num++; });
t.join();
cout<< num<< endl; //1
return 0;
}
join與detach啟動一個線程后,當(dāng)這個線程退出時,需要對該線程所使用的資源進(jìn)行回收,否則可能會導(dǎo)致內(nèi)存泄露等問題。thread庫給我們提供了如下兩種回收線程資源的方式:
join方式
主線程創(chuàng)建新線程后,可以調(diào)用join函數(shù)等待新線程終止,當(dāng)新線程終止時join
函數(shù)就會自動清理線程相關(guān)的資源。
join
函數(shù)清理線程的相關(guān)資源后,thread對象與已銷毀的線程就沒有關(guān)系了,因此一個線程對象一般只會使用一次join
,否則程序會崩潰。比如:
void func(int n)
{for (int i = 0; i<= n; i++)
{cout<< i<< endl;
}
}
int main()
{thread t(func, 20);
t.join();
t.join(); //程序崩潰
return 0;
}
但如果一個線程對象join
后,又調(diào)用移動賦值函數(shù),將一個右值線程對象的關(guān)聯(lián)線程的狀態(tài)轉(zhuǎn)移過來了,那么這個線程對象又可以調(diào)用一次join
。比如:
void func(int n)
{for (int i = 0; i<= n; i++)
{cout<< i<< endl;
}
}
int main()
{thread t(func, 20);
t.join();
t = thread(func, 30);
t.join();
return 0;
}
但采用join
的方式結(jié)束線程,在某些場景下也可能會出現(xiàn)問題。比如在該線程被join
之前,如果中途因?yàn)槟承┰驅(qū)е鲁绦虿辉賵?zhí)行后續(xù)代碼,這時這個線程將不會被join
。
void func(int n)
{for (int i = 0; i<= n; i++)
{cout<< i<< endl;
}
}
bool DoSomething()
{return false;
}
int main()
{thread t(func, 20);
//...
if (!DoSomething())
return -1;
//...
t.join(); //不會被執(zhí)行
return 0;
}
因此采用join
方式結(jié)束線程時,join
的調(diào)用位置非常關(guān)鍵,為了避免上述問題,可以采用RAII的方式對線程對象進(jìn)行封裝,也就是利用對象的生命周期來控制線程資源的釋放。比如:
class myThread
{public:
myThread(thread& t)
:_t(t)
{}
~myThread()
{if (_t.joinable())
_t.join();
}
//防拷貝
myThread(myThread const&) = delete;
myThread& operator=(const myThread&) = delete;
private:
thread& _t;
};
使用方式如下:
joinable
判斷這個線程是否需要被join
,如果需要那么就會調(diào)用join
對其該線程進(jìn)行等待。例如剛才的代碼中,使用myThread類對線程對象進(jìn)行封裝后,就能保證線程一定會被join
。
int main()
{thread t(func, 20);
myThread mt(t); //使用myThread對線程對象進(jìn)行封裝
//...
if (!DoSomething())
return -1;
//...
t.join();
return 0;
}
detach方式
主線程創(chuàng)建新線程后,也可以調(diào)用detach
函數(shù)將新線程與主線程進(jìn)行分離,分離后新線程會在后臺運(yùn)行,其所有權(quán)和控制權(quán)將會交給C++運(yùn)行庫,此時C++運(yùn)行庫會保證當(dāng)線程退出時,其相關(guān)資源能夠被正確回收。
detach
的方式回收線程的資源,一般在線程對象創(chuàng)建好之后就立即調(diào)用detach
函數(shù)。detach
函數(shù)分離線程之前被銷毀掉,這時就會導(dǎo)致程序崩潰。joinable
判斷這個線程是否需要被join
,如果需要那么就會調(diào)用terminate
終止當(dāng)前程序(程序崩潰)。四種互斥量
在C++11中,mutex中總共包了四種互斥量:
1、std::mute
mutex鎖是C++11提供的最基本的互斥量,mutex對象之間不能進(jìn)行拷貝,也不能進(jìn)行移動。
mutex中常用的成員函數(shù)如下:
成員函數(shù) | 功能 |
---|---|
lock | 對互斥量進(jìn)行加鎖 |
try_lock | 嘗試對互斥量進(jìn)行加鎖 |
unlock | 對互斥量進(jìn)行解鎖,釋放互斥量的所有權(quán) |
線程函數(shù)調(diào)用lock
時,可能會發(fā)生以下三種情況:
unlock
之前,該線程一致?lián)碛性撴i。線程調(diào)用try_lock
時,類似也可能會發(fā)生以下三種情況:
unlock
之前,該線程一致?lián)碛性撴i。try_lock
調(diào)用返回false,當(dāng)前的調(diào)用線程不會被阻塞。2、std::recursive_mutex
recursive_mutex叫做遞歸互斥鎖,該鎖專門用于遞歸函數(shù)中的加鎖操作。
unlock
。除此之外,recursive_mutex也提供了lock
、try_lock
和unlock
成員函數(shù),其的特性與mutex大致相同。
3、std::timed_mutex
timed_mutex中提供了以下兩個成員函數(shù):
try_lock_for
:接受一個時間范圍,表示在這一段時間范圍之內(nèi)線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間之內(nèi)還是沒有獲得鎖),則返回false。try_lock_untill
:接受一個時間點(diǎn)作為參數(shù),在指定時間點(diǎn)未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間點(diǎn)到來時還是沒有獲得鎖),則返回false。除此之外,timed_mutex也提供了lock
、try_lock
和unlock
成員函數(shù),其的特性與mutex相同。
4、std::recursive_timed_mutex
recursive_timed_mutex就是recursive_mutex和timed_mutex的結(jié)合,recursive_timed_mutex既支持在遞歸函數(shù)中進(jìn)行加鎖操作,也支持定時嘗試申請鎖。
加鎖示例
在沒有使用互斥鎖保證線程安全的情況下,讓兩個線程各自打印1-100的數(shù)字,就會導(dǎo)致控制臺輸出錯亂。比如:
void func(int n)
{for (int i = 1; i<= n; i++)
{cout<< i<< endl;
}
}
int main()
{thread t1(func, 100);
thread t2(func, 100);
t1.join();
t2.join();
return 0;
}
如果要讓兩個線程的輸出不會相互影響,即不會讓某一次輸出中途被另一個線程打斷,那么就需要用互斥鎖對打印過程進(jìn)行保護(hù)。
這里加鎖的方式有兩種,一種是在for循環(huán)體內(nèi)進(jìn)行加鎖,一種是在for循環(huán)體外進(jìn)行加鎖。比如:
void func(int n, mutex& mtx)
{mtx.lock(); //for循環(huán)體外加鎖
for (int i = 1; i<= n; i++)
{//mtx.lock(); //for循環(huán)體內(nèi)加鎖
cout<< i<< endl;
//mtx.unlock();
}
mtx.unlock();
}
int main()
{mutex mtx;
thread t1(func, 100, ref(mtx));
thread t2(func, 100, ref(mtx));
t1.join();
t2.join();
return 0;
}
說明一下:
經(jīng)驗(yàn)分享:
使用互斥鎖時可能出現(xiàn)的問題
使用互斥鎖時,如果加鎖的范圍太大,那么極有可能在中途返回時忘記了解鎖,此后申請這個互斥鎖的線程就會被阻塞住,也就是造成了死鎖問題。比如:
mutex mtx;
void func()
{mtx.lock();
//...
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
{//...
return; //中途返回(未解鎖)
}
//...
mtx.unlock();
}
int main()
{func();
return 0;
}
因此使用互斥鎖時如果控制不好就會造成死鎖,最常見的就是此處在鎖中間代碼返回,此外還有一個比較常見的情況就是在鎖的范圍內(nèi)拋異常,也很容易導(dǎo)致死鎖問題。
因此C++11采用RAII的方式對鎖進(jìn)行了封裝,于是就出現(xiàn)了lock_guard和unique_lock。
lock_guard
lock_guard是C++11中的一個模板類,其定義如下:
templateclass lock_guard;
lock_guard類模板主要是通過RAII的方式,對其管理的互斥鎖進(jìn)行了封裝。
lock
進(jìn)行加鎖。unlock
自動解鎖。通過這種構(gòu)造對象時加鎖,析構(gòu)對象時自動解鎖的方式就有效的避免了死鎖問題。比如:
mutex mtx;
void func()
{lock_guardlg(mtx); //調(diào)用構(gòu)造函數(shù)加鎖
//...
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
{//...
return; //調(diào)用析構(gòu)函數(shù)解鎖
}
//...
} //調(diào)用析構(gòu)函數(shù)解鎖
int main()
{func();
return 0;
}
從lock_guard對象定義到該對象析構(gòu),這段區(qū)域的代碼都屬于互斥鎖的保護(hù)范圍。
如果只想用lock_guard保護(hù)某一段代碼,可以通過定義匿名的局部域來控制lock_guard對象的生命周期。比如:
mutex mtx;
void func()
{//...
//匿名局部域
{lock_guardlg(mtx); //調(diào)用構(gòu)造函數(shù)加鎖
FILE* fout = fopen("data.txt", "r");
if (fout == nullptr)
{ //...
return; //調(diào)用析構(gòu)函數(shù)解鎖
}
} //調(diào)用析構(gòu)函數(shù)解鎖
//...
}
int main()
{func();
return 0;
}
模擬實(shí)現(xiàn)lock_guard
模擬實(shí)現(xiàn)lock_guard類的步驟如下:
lock
函數(shù)進(jìn)行加鎖。unlock
進(jìn)行解鎖。代碼如下:
namespace cl
{templateclass lock_guard
{public:
lock_guard(Mutex& mtx)
:_mtx(mtx)
{ mtx.lock(); //加鎖
}
~lock_guard()
{ mtx.unlock(); //解鎖
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
Mutex& _mtx;
};
}
unique_lock
但由于lock_guard太單一,用戶沒有辦法對鎖進(jìn)行控制,因此C++11又提供了unique_lock。
unique_lock與lock_guard類似,unique_lock類模板也是采用RAII的方式對鎖進(jìn)行了封裝。在創(chuàng)建unique_lock對象調(diào)用構(gòu)造函數(shù)時也會調(diào)用lock進(jìn)行加鎖,在unique_lock對象銷毀調(diào)用析構(gòu)函數(shù)時也會調(diào)用unlock進(jìn)行解鎖。
但lock_guard不同的是,unique_lock更加的靈活,提供了更多的成員函數(shù):
比如如下場景就適合使用unique_lock:
如下圖:
線程安全問題
多線程最主要的問題是共享數(shù)據(jù)帶來的問題(即線程安全)。如果共享數(shù)據(jù)都是只讀的,那么沒問題,因?yàn)橹蛔x操作不會影響到數(shù)據(jù),更不會涉及對數(shù)據(jù)的修改,所以所有線程都會獲得同樣的數(shù)據(jù)。但是,當(dāng)一個或多個線程要修改共享數(shù)據(jù)時,就會產(chǎn)生很多潛在的麻煩。比如:
void func(int& n, int times)
{for (int i = 0; i< times; i++)
{n++;
}
}
int main()
{int n = 0;
int times = 100000; //每個線程對n++的次數(shù)
thread t1(func, ref(n), times);
thread t2(func, ref(n), times);
t1.join();
t2.join();
cout<< n<< endl; //打印n的值
return 0;
}
上述代碼中分別讓兩個線程對同一個變量n進(jìn)行了100000次++
操作,理論上最終n的值應(yīng)該是200000,但最終打印出n的值卻是小于200000的。
根本原因就是++
操作并不是一個原子操作,該操作分為三步:
load
:將共享變量n從內(nèi)存加載到寄存器中。update
:更新寄存器里面的值,執(zhí)行+1操作。store
:將新值從寄存器寫回共享變量n的內(nèi)存地址。++
操作對應(yīng)的匯編代碼如下:
因此可能當(dāng)線程1剛將n的值加載到寄存器中就被切走了,也就是只完成了++
操作的第一步,而線程2可能順利完成了一次完整的++
操作才被切走,而這時線程1繼續(xù)用之前加載到寄存器中的值完成剩余的兩步操作,最終就會導(dǎo)致兩個線程分別對共享變量n進(jìn)行了一次++
操作,但最終n的值卻只被++
了一次。
加鎖解決線程安全問題
C++98中對于這里出現(xiàn)的線程安全的問題,會選擇對共享修改的數(shù)據(jù)進(jìn)行加鎖保護(hù)。比如:
void func(int& n, int times, mutex& mtx)
{mtx.lock();
for (int i = 0; i< times; i++)
{//mtx.lock();
n++;
//mtx.unlock();
}
mtx.unlock();
}
int main()
{int n = 0;
int times = 100000; //每個線程對n++的次數(shù)
mutex mtx;
thread t1(func, ref(n), times, ref(mtx));
thread t2(func, ref(n), times, ref(mtx));
t1.join();
t2.join();
cout<< n<< endl; //打印n的值
return 0;
}
這里可以選擇在for循環(huán)體里面進(jìn)行加鎖解鎖,也可以選擇在for循環(huán)體外進(jìn)行加鎖解鎖。但效果終究是不盡人意的,在for循環(huán)體里面進(jìn)行加鎖解鎖會導(dǎo)致線程的頻繁進(jìn)行加鎖解鎖操作,在for循環(huán)體外面進(jìn)行加鎖解鎖會導(dǎo)致兩個線程的執(zhí)行邏輯變?yōu)榇?,而且如果鎖控制得不好,還容易造成死鎖。
原子類解決線程安全問題
C++11中引入了原子操作類型,使得線程間數(shù)據(jù)的同步變得非常高效。如下:
原子類型名稱 | 對應(yīng)的內(nèi)置類型名稱 |
---|---|
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
注意: 需要用大括號對原子類型的變量進(jìn)行初始化。
程序員不需要對原子類型進(jìn)行加鎖解鎖操作,線程能夠?qū)υ宇愋妥兞炕コ庠L問。比如剛才的代碼可以改為:
void func(atomic_int& n, int times)
{for (int i = 0; i< times; i++)
{n++;
}
}
int main()
{atomic_int n = {0 };
int times = 100000; //每個線程對n++的次數(shù)
thread t1(func, ref(n), times);
thread t2(func, ref(n), times);
t1.join();
t2.join();
cout<< n<< endl; //打印n的值
return 0;
}
除此之外,也可以使用atomic類模板定義出任意原子類型。比如上述代碼還可以改為:
void func(atomic& n, int times)
{for (int i = 0; i< times; i++)
{n++;
}
}
int main()
{atomicn = 0;
int times = 100000; //每個線程對n++的次數(shù)
thread t1(func, ref(n), times);
thread t2(func, ref(n), times);
t1.join();
t2.join();
cout<< n<< endl; //打印n的值
return 0;
}
說明一下:
operator=
等。operator=
默認(rèn)刪除掉了。++
操作,還支持原子的--
、加一個值、減一個值、與、或、異或操作。condition_variable中提供的成員函數(shù),可分為wait系列和notify系列兩類。
wait系列成員函數(shù)
wait系列成員函數(shù)的作用就是讓調(diào)用線程進(jìn)行阻塞等待,包括wait
、wait_for
和wait_until
。
下面先以wait
為例進(jìn)行介紹,wait函數(shù)提供了兩個不同版本的接口:
//版本一
void wait(unique_lock& lck);
//版本二
templatevoid wait(unique_lock& lck, Predicate pred);
函數(shù)說明:
為什么調(diào)用wait系列函數(shù)時需要傳入一個互斥鎖?
wait_for和wait_until函數(shù)的使用方式與wait函數(shù)類似:
注意: 調(diào)用wait系列函數(shù)時,傳入互斥鎖的類型必須是unique_lock。
notify系列成員函數(shù)
notify系列成員函數(shù)的作用就是喚醒等待的線程,包括notify_one
和notify_all
。
notify_one
:喚醒等待隊(duì)列中的首個線程,如果等待隊(duì)列為空則什么也不做。notify_all
:喚醒等待隊(duì)列中的所有線程,如果等待隊(duì)列為空則什么也不做。注意: 條件變量下可能會有多個線程在進(jìn)行阻塞等待,這些線程會被放到一個等待隊(duì)列中進(jìn)行排隊(duì)。
實(shí)現(xiàn)兩個線程交替打印1-100嘗試用兩個線程交替打印1-100的數(shù)字,要求一個線程打印奇數(shù),另一個線程打印偶數(shù),并且打印數(shù)字從小到大依次遞增。
該題目主要考察的就是線程的同步和互斥。
但如果只有同步和互斥是無法滿足題目要求的。
鑒于此,這里還需要定義一個flag變量,該變量的初始值設(shè)置為true。
flag
的值,而讓線程2調(diào)用wait函數(shù)阻塞等待時,傳入的可調(diào)用對象返回!flag
的值。代碼如下:
int main()
{int n = 100;
mutex mtx;
condition_variable cv;
bool flag = true;
//奇數(shù)
thread t1([&]{int i = 1;
while (i<= 100)
{ unique_lockul(mtx);
cv.wait(ul, [&flag]()->bool{return flag; }); //等待條件變量滿足
cout<< this_thread::get_id()<< ":"<< i<< endl;
i += 2;
flag = false;
cv.notify_one(); //喚醒條件變量下等待的一個線程
}
});
//偶數(shù)
thread t2([&]{int j = 2;
while (j<= 100)
{ unique_lockul(mtx);
cv.wait(ul, [&flag]()->bool{return !flag; }); //等待條件變量滿足
cout<< this_thread::get_id()<< ":"<< j<< endl;
j += 2;
flag = true;
cv.notify_one(); //喚醒條件變量下等待的一個線程
}
});
t1.join();
t2.join();
return 0;
}
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧