做網(wǎng)站設(shè)像素國內(nèi)廣告投放平臺
多線程操作
- 進程與線程
- 線程的創(chuàng)建 create_pthread
- 創(chuàng)建線程池
- 給線程傳入對象的指針
- 線程等待 pthread_join
- 退出線程 pthread_exit
- 線程等待參數(shù) retval 與 線程退出參數(shù) retval
- 線程中斷 pthread_cancel
- 獲取線程編號 pthread_self
- 線程分離 pthread_detach
進程與線程
- 進程是資源分配的基本單位
- 線程是調(diào)度的基本單位,共享進程的數(shù)據(jù),擁有自己的一部分數(shù)據(jù)
線程私有的屬性:線程的ID、一組寄存器(上下文數(shù)據(jù))、棧(獨立的棧結(jié)構(gòu))、調(diào)度優(yōu)先級
進程的多個線程共享同一塊地址空間,對堆區(qū)、棧區(qū)都是共享的
線程共享進程的資源有:文件描述符表、每種信號的處理方式(默認動作、忽略動作、自定義動作)、當(dāng)前工作目錄
線程的創(chuàng)建 create_pthread
Linux下沒有真正意義的線程,而是用進程模擬的線程(LWP)。對此,Linux不會提供直接創(chuàng)建線程的系統(tǒng)調(diào)用,只會提供創(chuàng)建輕量級進程的接口。
在用戶看來會很變扭,進程是進程,線程是線程就要區(qū)分開來。
所以出現(xiàn)了用戶級線程庫 pthread
:對Linux接口進行封裝,給用戶提供進行線程控制的接口
(pthread
線程庫在任何版本的Linux操作系統(tǒng)都會存在, pthread
也被稱為原生線程庫)
可以通過 man
的3號手冊來查看線程庫的使用,這里不作演示
接下來介紹一些線程庫的接口使用:
使用原生線程庫需要包含頭文件:#include <pthread>
- 創(chuàng)建線程
int pthread_create(pthread_t *thread, const pthread_attr_t* attr,void* (*start_routine)(void*), void* arg);
pthread_create 函數(shù)參數(shù)介紹
thread:線程 id 地址,pthread_t 為無符號整數(shù)
attr:線程屬性(線程優(yōu)先級)
start_routine:函數(shù)指針,執(zhí)行對應(yīng)的函數(shù)功能(可以對函數(shù)進行傳參),也被稱為回調(diào)函數(shù)
arg:是指向任意數(shù)據(jù)的指針,將參數(shù)傳遞給 start_routine 函數(shù)
返回值:線程創(chuàng)建成功返回0,失敗錯誤碼被設(shè)置
示例:
#include <iostream>
#include <pthread>
#include <unistd.h>void* thread_run(void* arg)
{while(true){std::cout << "new thread running" << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, thread_run, nullptr);//創(chuàng)建線程,t是輸出型參數(shù)//主進程while(true){std::cout << "main thread running, new thread id:" << t << std::endl;sleep(1);}return 0;
}
上面代碼直接編譯的話會出現(xiàn)鏈接報錯,這是因為這個多線程是一個庫,直接編譯 g++ 會找不到這個庫,需要指定編譯器去找線程庫。
對此,在編譯時,使用 g++ 進行編譯要加上 -lpthread
選項
g++ -o threadTest threadTest .c -std=c++11 -lpthread
可以通過 ldd
對編譯好的可執(zhí)行文件來查看線程庫的位置:
ldd threadTest
執(zhí)行程序可以看到,主線程與子線程同時運行:
此時輸出的線程id會很大,很奇怪。其實這些線程的id是地址,創(chuàng)建的線程會被線程庫管理起來,形成數(shù)組,每個對應(yīng)的線程id 其實就是數(shù)組的下標。
創(chuàng)建的線程是不能確定先后順序的. Linux下的線程是輕量級的進程,進程創(chuàng)建執(zhí)行的先后順序是由調(diào)度器決定的,對此線程誰先誰后的問題也要看調(diào)度器來決定的
創(chuàng)建線程池
下面來創(chuàng)建一個線程池,讓每一個線程都執(zhí)行 thread_run 這個函數(shù),打印對應(yīng)的創(chuàng)建編號
#include <iostream>
#include <pthread>
#include <unistd.h>#define NUM 10void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char thname[64];snprintf(thname, sizeof(thname), "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//創(chuàng)建線程池,將thname傳參}//主進程while(true){std::cout << "main thread running" << std::endl;sleep(1);}return 0;
}
編譯運行:
結(jié)果很不對,輸出的結(jié)果都是一樣的。
在給線程回調(diào)函數(shù)進行傳參時,傳入的是 thname 地址。thname 字符數(shù)組是屬于主線程的,屬于臨時變量。前面提到線程會共享進程中的數(shù)據(jù)。對此,每個線程都會對這個變量進行讀寫,導(dǎo)致最終顯示的結(jié)果都是一樣的。
解決方式如下:
對 thname 變量在堆上申請空間,待到回調(diào)函數(shù)使用完后對這個資源進行釋放:
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(1);}delete name; //釋放空間return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char* thname = new char[64]; //堆上開辟空間snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//創(chuàng)建線程池,將thname傳參}//主進程while(true){std::cout << "main thread running" << std::endl;sleep(1);}return 0;
}
編譯運行:
創(chuàng)建線程前,每次對 thname 進行資源申請,回調(diào)函數(shù)之后對資源進行釋放,可以很好的避免資源共享情況發(fā)生。從結(jié)果也可以看出不同線程的執(zhí)行先后順序也是不確定的。
給線程傳入對象的指針
創(chuàng)建線程時,不僅僅只可以傳入內(nèi)置類型變量的指針,還可以傳入自定義類型變量的指針
示例:構(gòu)建 ThreadDate 類,其內(nèi)部包含線程的基本信息。在類中實現(xiàn)輸入型參數(shù)和輸出型參數(shù),方便我們獲取線程處理后的數(shù)據(jù)結(jié)果
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <ctime>#define NUM 3enum { OK=0, ERROR };struct ThreadDate
{//構(gòu)造ThreadDate(const string& name, pthread_t tid, time_t createTime, size_t top = 0):_name(name), _tid(tid), _createTime((uint64_t)createTime),_status(OK),_top(top),_result(0){}~ThreadDate(){}//成員變量//輸入型變量string _name;pthread_t _tid;uint64_t _createTime; //創(chuàng)建時間//輸出型變量int _status; //線程退出狀態(tài)size_t _top;//累加到最大值int _result;
};
下面通過實例化這個類,來演示線程中傳入對象:
int main()
{pthread_t tids[NUM];// 創(chuàng)建線程池for (int i = 0; i < NUM; i++){char *thname = new char[64];snprintf(thname, 64, "thread-%d", i + 1);//定義ThreadDate類,傳入到線程中ThreadDate* tdate = new ThreadDate(std::string(thname), i+1, time(nullptr), (100+ i * 5));pthread_create(tids + i, nullptr, thread_run, tdate); //將tdate對象進行傳參}void *ret = nullptr; // 用于保存子線程退出的信息for (size_t i = 0; i < NUM; i++){int n = pthread_join(tids[i], &ret); //傳入ret指針的地址if(n != 0) std::cerr << "pthread_join error" << std::endl;ThreadDate* td = static_cast<ThreadDate*>(ret); //指針類型轉(zhuǎn)換if(td->_status == OK) //輸出對象內(nèi)容std::cout << td->_name << " 計算的結(jié)果是: " << td->_result << " (它要計算的是[1, " << td->_top << "])" << std::endl;//釋放資源delete td;}return 0;
}
線程等待 pthread_join
上面實現(xiàn)的代碼中,我們將主線程用死循環(huán)的方式,一直維持進程的運行。
如果去掉死循環(huán),線程還能繼續(xù)執(zhí)行下去嗎?
對上面的代碼進行修改:在線程被創(chuàng)建后,維持 3 秒后主進程退出
int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char* thname = new char[64]; //堆上開辟空間snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//創(chuàng)建線程池,將thname傳參}//主進程sleep(3);return 0;
}
進程是資源的申請的主體,進程退出了,不管子進程還在進行什么操作都會終止運行。
這樣會造成什么后果?
會造成資源泄漏,如果此時的線程在堆區(qū)申請了資源還沒來得及釋放,會導(dǎo)致內(nèi)存泄漏。
線程與子進程一樣,線程退出后需要被回收處理。就拿子進程來說,當(dāng)子進程退出后會處于僵尸狀態(tài),父進程如果沒有等待子進程,對子進程的僵尸狀態(tài)進行回收的話會造成資源的泄漏。
有僵尸進程,但是有沒有僵尸線程一說。與進程相似,線程退出后也會處于一種被回收的狀態(tài),沒有及時回收線程的話,也會造成內(nèi)存泄漏!
對此,線程退出是需要進行等待的
下面來介紹一個函數(shù)接口:pthread_join
等待線程
int pthread_join(pthread_t thread, void **retval);
參數(shù)介紹:
thread:等待的線程 id 號
retval:是一個指向指針的指針,用于存儲被等待線程的返回值
返回值:等待成功返回0,失敗錯誤碼被返回
對上面的代碼進行修改,寫一個等待進程的版本:
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(1);}delete name; //釋放空間return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){// char thname[64];char* thname = new char[64];// snprintf(thname, sizeof(thname), "thread-%d", i + 1);snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//創(chuàng)建線程池,將thname傳參}for(size_t i = 0; i < NUM; i++){pthread_join(tids[i], nullptr);//等待線程}return 0;
}
有了線程等待,可以很好的避免內(nèi)存泄漏。主進程會等待所有的子線程,只有當(dāng)所有的線程都退出后才會結(jié)束整個程序的運行。
退出線程 pthread_exit
如何控制線程的退出呢?
這里還是拿進程來說,也比較好舉例(前面也說過線程是輕量級的進程)。進程退出的方式可以在main函數(shù)中使用 return 語句、在任意行代碼處調(diào)用 exit 函數(shù)。
那么線程可以使用類似的方法嗎?
先來看看 return 語句的作用,還是拿剛剛編寫的代碼來舉例。這里我們直接往死循環(huán)內(nèi)部編寫 3 秒的停頓,之后直接執(zhí)行break 語句,后續(xù)執(zhí)行 return 語句。為了方便展示,下面只展示修改的代碼:
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(3);break; //跳出循環(huán)}delete name; return nullptr;
}
編譯運行,來看看執(zhí)行結(jié)果:
所有的線程都會打印一次,然后停頓卡住,到執(zhí)行 return 語句后所有的線程都會退出。執(zhí)行的效果也是符合我們的預(yù)期的。
下面來使用 exit 函數(shù)來測試線程退出情況,還是上面的代碼,將 break 語句換成 exit 函數(shù)
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;exit(10); //調(diào)用exit函數(shù)}delete name; //釋放空間return nullptr;
}
下面來看看現(xiàn)象:
線程池只創(chuàng)建了一部分,然后直接終止了運行。在右邊監(jiān)視 threadTest 進程也沒有任何顯示。
exit 函數(shù)退出作用是整個 threadTest 進程,當(dāng)某一子線程調(diào)用了 exit 函數(shù)的時候,就會導(dǎo)致整個進程都退出。這也是為什么會只創(chuàng)建了一些子線程,然后導(dǎo)致整個進程都結(jié)束運行了。
對此,在線程執(zhí)行流中,非必要情況下,不要輕易的調(diào)用 exit 函數(shù)。
不能使用 exit 函數(shù),但是線程庫中提供了一個API,用于退出某一線程:pthread_exit
void pthread_exit(void *retval);
參數(shù)介紹:
retval:指向線程退出狀態(tài)的指針
當(dāng)線程調(diào)用 pthread_exit 時,它會立即停止執(zhí)行,并釋放其棧空間。但是,線程的資源(如線程ID和線程屬性)直到其他線程調(diào)用 pthread_join 來回收它時才會被完全釋放。
示例:
#define NUM 3void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(4);break;}delete name; //釋放空間pthread_exit(nullptr); //退出調(diào)用的線程
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){// char thname[64];char* thname = new char[64];// snprintf(thname, sizeof(thname), "thread-%d", i + 1);snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//創(chuàng)建線程池,將thname傳參}//等待線程for(size_t i = 0; i < NUM; i++){ pthread_join(tids[i], nullptr); }return 0;
}
這里只創(chuàng)建了三個子線程的線程池,來看看運行的效果:
線程等待參數(shù) retval 與 線程退出參數(shù) retval
先來看看這兩個API的接口聲明:
int pthread_join(pthread_t thread, void **retval); //線程等待接口
void pthread_exit(void *retval); //線程退出接口
兩個函數(shù)之間的 retval 參數(shù)有關(guān)聯(lián)嗎?答案是有的。
一般創(chuàng)建進程都是為了幫助我們?nèi)ネ瓿赡承┤蝿?wù),線程也是如此,創(chuàng)建線程也是為了幫助進程完成一部分任務(wù)。進程在完成任務(wù)后正常退出,返回對應(yīng)的退出碼。當(dāng)然,進程完成到一定的任務(wù)時也會直接退出。
下面是進程退出的幾個情況:
- 在 main 函數(shù)中調(diào)用 return 語句,返回對應(yīng)的退出碼;
- 在進程中任意代碼處調(diào)用 exit 函數(shù)。當(dāng)然調(diào)用 exit 函數(shù)需要傳參,進程退出的退出碼也就是傳入exit 函數(shù)參數(shù)的值;
- 收到OS的終止信號
進程的退出碼、退出信號的返回,是方便我們?nèi)ゲ榭串?dāng)前進程是不是完成了指定的任務(wù)。線程也是如此,線程退出是否正常我們也要知道。對此,上面提到的 兩個 API 接口的參數(shù)作用就是用于獲取線程退出的退出信息!
線程退出接口 pthread_exit 一般是用在回調(diào)函數(shù)內(nèi)部,也就是子線程中。我們可以先將 pthread_exit 功能想象成 exit 函數(shù)那般,在子線程退出后我們將子線程退出碼帶出來。
但是問題來了,為什么 pthread_exit 傳入的參數(shù)是 void* retval
一級指針?
這個要結(jié)合 pthread_join 來看:
int pthread_join(pthread_t thread, void **retval);
pthread_join 是等待線程的一個接口,會回收退出的子線程(線程的ID、線程的屬性等)。pthread_join 的 retval 是一個輸出型參數(shù)。
這里的 retval 如同在進程中調(diào)用的 wait 函數(shù)時,傳入 status 參數(shù),這個 status 也是輸出型參數(shù),會將 子進程的退出碼、退出信號帶出來。
retval 參數(shù)的作用就是將子線程的退出數(shù)據(jù)帶出來,不同的是這里是二級指針。在使用前需要定義一個指針,然后將這個指針的地址傳入 pthread_join 的 retval 參數(shù)中。在子線程調(diào)用 pthread_exit 函數(shù)時,傳出對應(yīng)的數(shù)據(jù)即可。
光說不做,假把戲。下面來看看測試案例:
void *thread_run(void *arg)
{char *name = (char *)arg;while (true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(3);break;}delete name; // 釋放空間pthread_exit((void*)1); //子線程退出,退出信息設(shè)置為1
}int main()
{pthread_t tids[NUM];for (int i = 0; i < NUM; i++){char *thname = new char[64];snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname); // 創(chuàng)建線程池,將thname傳參}void *ret = nullptr; // 用于保存子線程退出的信息for (size_t i = 0; i < NUM; i++){int n = pthread_join(tids[i], &ret); //傳入ret指針的地址if(n != 0) std::cerr << "pthread_join error" << std::endl;std::cout << "子線程:thread->" << i+1 << ",退出碼為:" << (uint64_t)ret << std::endl;}return 0;
}
這里需要注意就是傳指針的問題:
定義 ret 一級指針,傳參到 pthread_join 內(nèi)部時,傳入的是 ret 指針的地址。pthread_exit 傳參需要傳入指針類型,對此上面代碼需要對 1 進行 void* 類型的強轉(zhuǎn)。在輸出子線程退出信息時,ret 是指針,經(jīng)過子線程的等待,ret內(nèi)部值已經(jīng)被設(shè)置為了除了低位的第一位為1其他全為 0 的二進制序列,在通過 uint64_t 類型強轉(zhuǎn)即可將數(shù)據(jù)打印輸出!
還要提一點就是:在獲取線程的退出碼時,是不需要考慮異常的。如果一個線程中出現(xiàn)了異常,那么就會帶動的整個主進程退出。主進程都退出了還需要考慮等待進程的異常嗎?是不需要的。對此,在多線程中是不需要考慮異常的!異常問題通常是由進程來考慮。
線程中斷 pthread_cancel
在實際開發(fā)需求中,如果想要將創(chuàng)建的線程中斷運行需要用到 API:pthread_cancel
int pthread_cancel(pthread_t thread);
參數(shù)介紹:
thread:傳入的線程編號
示例:我們先來創(chuàng)建一個正常線程,再執(zhí)行一段任務(wù)后線程會自動退出:
void* thread_run(void* args)
{//靜態(tài)類型轉(zhuǎn)換const char* str = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << str << "is runing :" << cnt-- << endl;sleep(1);}//退出線程pthread_exit((void*)1);
} int main()
{//創(chuàng)建線程pthread_t tid;pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");//等待線程void* ret = nullptr;pthread_join(tid, &ret);return 0;
}
修改上述代碼,在線程執(zhí)行兩秒任務(wù)后,直接調(diào)用 pthread_cancel 接口,查看現(xiàn)象:
void* thread_run(void* args)
{//靜態(tài)類型轉(zhuǎn)換const char* str = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << str << "is runing :" << cnt-- << endl;sleep(1);}//退出線程pthread_exit((void*)1);
} int main()
{//創(chuàng)建線程pthread_t tid;pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");//2秒后,中斷線程sleep(2);pthread_cancel(tid);//等待線程void* ret = nullptr;pthread_join(tid, &ret);return 0;
}
可以看到當(dāng)線程執(zhí)行兩秒后直接中斷
獲取線程編號 pthread_self
pthread_t pthread_self(void);
誰調(diào)用這個接口就獲取誰的線程 id 編號,示例:
void* thread_run(void* args)
{//靜態(tài)類型轉(zhuǎn)換const char* str = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << str << "is runing :" << cnt-- << "obtain self id ->" << pthread_self() << endl; //獲取線程idsleep(1);}//退出線程pthread_exit((void*)1);
} int main()
{//創(chuàng)建線程pthread_t tid;pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");//等待線程void* ret = nullptr;pthread_join(tid, &ret);cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;return 0;
}
線程分離 pthread_detach
新線程被創(chuàng)建,默認情況下是 joinable 的,線程退出,主進程需要對這個線程進行 pthread_join 操作。不對線程進行等待的操作就會造成內(nèi)存泄漏,無法釋放資源。
如果不關(guān)心線程的返回值,那么等待就會變成一種負擔(dān)。
就是主線程自己為了等待子線程,難道不用去做自己的事情了嗎?這個時候,我們可以告訴OS,當(dāng)線程退出的時候,自己去釋放資源。如何操作呢?需要用到下面這個 API :
int pthread_detach(pthread_t thread);
pthread_detach 功能是將一個線程分離出來,但是要記住一個點:被分離的線程在后續(xù)操作是不能被等待的!!如果對被分離的線程進行 pthread_join 操作,主進程是會報錯的。報錯出現(xiàn)后,就不會再對子線程進行等待操作,直接向后運行屬于主進程的代碼。
線程分離好比現(xiàn)實生活中的:已婚與未婚,是屬于一種屬性。
線程分離,并不是字面上的意思將線程與進程分離開那種。分離是一種屬性,沒有被分離的線程,是 joinable 的。該線程需要被等待回收資源;已經(jīng)被分離的線程,其內(nèi)部屬性會發(fā)生變化,表示這個線程不需要再被等待回收資源。
示例:創(chuàng)建一個子線程,在等待子線程之前對該子線程進行分離操作
#include <pthread.h>
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <string>using namespace std;void* threadRoution(void* arg)
{const char* tname = static_cast<const char*>(arg);int cnt = 5;while(cnt){cout << tname << ":" << cnt-- << endl;sleep(1);}return nullptr;
}int main()
{//創(chuàng)建線程pthread_t tid;pthread_create(&tid, nullptr, threadRoution, (void*)"thread 1");//對子線程進行分離操作pthread_detach(tid);//等待線程void* ret = nullptr;int n = pthread_join(tid, &ret);if(n != 0) cerr << "error:" << errno << strerror(n) << endl;return 0;
}
編譯查看效果:
主進程在等待子線程時,發(fā)現(xiàn)該線程已經(jīng)被分離。對此,不會再阻塞等待子線程,程序直接向后運行走,子線程也沒有機會繼續(xù)執(zhí)行對應(yīng)的功能,整個進程就退出了。
因此,線程分離的主要功能就是將子線程分離出來,讓主進程有更多的時間去處理屬于自己事情,也不需要對子線程的資源釋放與否而擔(dān)心。
不過在使用線程分離的時候,要注意執(zhí)行流先后問題,不然會出現(xiàn)奇奇怪怪的現(xiàn)象。
下面來舉個例子:在子線程內(nèi)部去調(diào)用本線程的分離
void* threadRoution(void* arg)
{//將調(diào)用的線程分離開來pthread_detach(pthread_self());const char* tname = static_cast<const char*>(arg);int cnt = 5;while(cnt){cout << tname << ":" << cnt-- << endl;sleep(1);}return nullptr;
}int main()
{//創(chuàng)建線程pthread_t tid;pthread_create(&tid, nullptr, threadRoution, (void*)"thread 1");int n = pthread_join(tid, nullptr);if(n != 0) cerr << "error:" << errno << strerror(n) << endl;return 0;
}
此時會發(fā)現(xiàn),線程正常的跑,主進程也等待成功。
子線程調(diào)用分離沒有用嗎?其實不然,這是由于執(zhí)行流先后問題:
子線程被創(chuàng)建出來之前,主進程就執(zhí)行到了 pthread_join 代碼處,子線程還沒有來得及分離,分離屬性沒有被修改,造成主進程阻塞等待子線程。對此,就算子線程將自己分離開來,主進程早就處于進行了等待狀態(tài),也就造成了子線程繼續(xù)往后執(zhí)行的現(xiàn)象。
提示:使用線程分離的接口,盡量在創(chuàng)建線程之后進行調(diào)用,防止奇奇怪怪的執(zhí)行流的問題產(chǎn)生
線程操作就講到這里,感謝大家的支持!!