|
libus是一個(gè)線程安全的庫(kù),但是多個(gè)線程的libusb相互配合工作需要額外的考慮。
最根本需要解決的問(wèn)題是所有的libusb I/O 處理都是通過(guò)poll()/select() 系統(tǒng)調(diào)用監(jiān)控文件描述符。在asynchronous接口中直接顯露出來(lái)的,但是同樣需要注意synchronous接口是在asynchonrous接口之上實(shí)現(xiàn)的,因此同樣需要考慮。 這個(gè)問(wèn)題是如果2個(gè)以上的線程同時(shí)在libusb的文件描述符上調(diào)用poll() or select(),這些線程中只有一個(gè)會(huì)在事件到來(lái)時(shí)被喚醒,其他的會(huì)完全被忽略任何發(fā)生的事件。 思考下面的偽代碼,提交異步傳輸然后等待完成。這是一種在異步接口之上實(shí)現(xiàn)同步接口的方法 (盡管libusb的方式比這個(gè)頁(yè)面上的更高級(jí),但libusb也是用類似的方式)。 void cb(struct libusb_transfer *transfer) { int *completed = transfer->user_data; *completed = 1; }
void myfunc() { struct libusb_transfer *transfer; unsigned char buffer[LIBUSB_CONTROL_SETUP_SIZE]; int completed = 0;
transfer = libusb_alloc_transfer(0); libusb_fill_control_setup(buffer, LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT, 0x04, 0x01, 0, 0); libusb_fill_control_transfer(transfer, dev, buffer, cb, &completed, 1000); libusb_submit_transfer(transfer);
while (!completed) { poll(libusb file descriptors, 120*1000); if (poll indicates activity) libusb_handle_events_timeout(ctx, 0); } printf("completed!"); // other code here } 此處是針對(duì)一種條件的異步事件的序列化完成,這種條件是特定的傳輸完成。poll()循環(huán)設(shè)置了一個(gè)很長(zhǎng)的超時(shí)以便于在沒(méi)有任何事情發(fā)生的情況下(在某些原因下不被限制)最小化CPU利用率。 如果這是唯一的使用libusb的文件描述符的線程,這沒(méi)有問(wèn)題: 另一個(gè)線程將吞沒(méi)我們感興趣的事件是沒(méi)有危險(xiǎn)的。另一方面,如果另一個(gè)線程使用相同等我文件描述符,這將有可能它將接收到我們感興趣的事件。在這種情況下, myfunc()在循環(huán)下次檢測(cè)的時(shí)候,直到120秒之后將只能發(fā)現(xiàn)傳輸已經(jīng)完成。顯然2分鐘的延時(shí)是讓人無(wú)法忍受的,更不要想通過(guò)使用很短延時(shí)來(lái)解決這個(gè)問(wèn)題。 解決方案是去報(bào)沒(méi)有2個(gè)線程同時(shí)使用一個(gè)文件描述符。一個(gè)幼稚的實(shí)現(xiàn)將影響庫(kù)的性能, 所以libusb提供了下面的文檔化的方案來(lái)保證沒(méi)有性能方面的損失。 在我們繼續(xù)深入之前,值得提一下的是所有的libusb封裝的事件處理程序都完全支持下面文檔的方案,這包括libusb_handle_events()所有的同步I/O 函數(shù)——libusb為你把這個(gè)頭疼的事情隱藏了起來(lái)。如果你堅(jiān)持那種級(jí)別,你不需要擔(dān)心任何問(wèn)題。 問(wèn)題是當(dāng)我們面對(duì)libusb 暴露出文件描述符來(lái)允許你將異步USB I/O融入主循環(huán)的事實(shí), 有效地允許你在libusb的后面做些工作。如果你自己用libusb的文件描述符,并將它們傳遞給poll()/select()時(shí) ,你需要注意相關(guān)問(wèn)題。 事件鎖 第一個(gè)被介紹的概念是事件鎖。事件鎖用于序列化想要處理事件的線程,以便于在任意時(shí)間只有唯一的一個(gè)線程正在處理事件。 你必須在使用libusb文件描述符之前使用事件鎖函數(shù) libusb_lock_events()。一旦你離開poll()/select()循環(huán),你必須立即使用函數(shù) libusb_unlock_events()釋放事件鎖。 讓其他線程為你工作 雖然事件鎖是解決方案的一個(gè)重要部分,但僅用它本身是不夠的,如果下面的情況發(fā)生你也許會(huì)驚訝... libusb_lock_events(ctx); while (!completed) { poll(libusb file descriptors, 120*1000); if (poll indicates activity) libusb_handle_events_timeout(ctx, 0); } libusb_unlock_events(ctx); ...答案是這個(gè)是不對(duì)的。這是因?yàn)榇a中顯示的傳輸也許會(huì)話更長(zhǎng)的時(shí)間(比如說(shuō)30秒)完成。 直到傳出完成之前鎖都不會(huì)被釋放。 另一個(gè)與之類似想要處理事件的代碼的線程也許正在處理一個(gè)傳輸,并且將在幾毫秒之后完成。由于鎖的獨(dú)占,盡管有著如此之快的完成時(shí)間,另一個(gè)線程不能檢測(cè)到它的這種傳輸狀態(tài),直到上面的代碼完成(30秒之后) 。 為了解決這個(gè)問(wèn)題, libusb為你提供了一種方法決定什么時(shí)候另一個(gè)線程正在處理事件。 它也提供了一種方法鎖住你的線程直到事件處理線程完成一個(gè)事件(并且這種機(jī)制并不需要使用文件描述符)。 在確定另一個(gè)線程正在處理事件,你通過(guò)libusb_lock_event_waiters()使用獲得一個(gè)事件等待鎖,然后你重新檢測(cè)是否其他線程仍在處理事件。如果是這樣,你可以調(diào)用libusb_wait_for_event()。 libusb_wait_for_event() 將你的程序轉(zhuǎn)到休眠狀態(tài)直到事件發(fā)生。或者知道一個(gè)線程釋放這個(gè)事件鎖。當(dāng)這些事件發(fā)生或者你的線程被喚醒,你需要重新檢測(cè)它正在等待的條件。同樣需要重新檢測(cè)是否另一個(gè)線程在處理事件,如果沒(méi)有,它應(yīng)該自己開始處理事件。這應(yīng)該看起來(lái)像下面的偽代碼: retry: if (libusb_try_lock_events(ctx) == 0) { // we obtained the event lock: do our own event handling while (!completed) { if (!libusb_event_handling_ok(ctx)) { libusb_unlock_events(ctx); goto retry; } poll(libusb file descriptors, 120*1000); if (poll indicates activity) libusb_handle_events_locked(ctx, 0); } libusb_unlock_events(ctx); } else { // another thread is doing event handling. wait for it to signal us that // an event has completed libusb_lock_event_waiters(ctx);
while (!completed) { // now that we have the event waiters lock, double check that another // thread is still handling events for us. (it may have ceased handling // events in the time it took us to reach this point) if (!libusb_event_handler_active(ctx)) { // whoever was handling events is no longer doing so, try again libusb_unlock_event_waiters(ctx); goto retry; }
libusb_wait_for_event(ctx); } libusb_unlock_event_waiters(ctx); } printf("completed!\n"); 一個(gè)天真的人看到上面的代碼也許會(huì)建議這個(gè)只能支持一個(gè)事件等待者 (因此總工2個(gè)競(jìng)爭(zhēng)的線程,另一個(gè)處理事件),因?yàn)楫?dāng)?shù)却录臅r(shí)候,事件等待者看起來(lái)已經(jīng)事件等待鎖。但是,系統(tǒng)支持多個(gè)事件等待者,因?yàn)?a title="Wait for another thread to signal completion of an event." data="/link?url=http://libusb./api-1.0/group__poll.html#gae22755d523560be2867be7d09034ca50" rel="nofollow" target="_blank">libusb_wait_for_event() 在等待時(shí)確實(shí)放棄了鎖,在繼續(xù)之前請(qǐng)求鎖。 我們已經(jīng)實(shí)現(xiàn)了動(dòng)態(tài)處理沒(méi)有現(xiàn)成正在處理事件情況代碼(所以我們應(yīng)該自己做),并且它也能夠處理另一個(gè)線程正在處理事件的情況。(所以我們能承載他們)。它相當(dāng)于處理2者得結(jié)合。 舉例來(lái)說(shuō),另一個(gè)線程正在處理事件,但是某種原因,在我們的條件符合的情況出現(xiàn)之前,它停了下來(lái),所以我們接替處理事件。 下面介紹4個(gè)在上面?zhèn)未a中出現(xiàn)的函數(shù)。它們的重要性在上面的偽代碼中是顯而易見的 。 1. libusb_try_lock_events() 是一個(gè)嘗試獲取事件鎖非阻塞的函數(shù),如果已經(jīng)被占用它會(huì)返回失敗代碼。 2. libusb_event_handling_ok() 檢測(cè)libusb是否可以為你的線程執(zhí)行事件處理。有時(shí),libusb需要中斷事件處理器,這就是你如何能在你已經(jīng)被中斷的情況下檢測(cè)的原因。如果這個(gè)函數(shù)返回0,正確的行為是放棄事件鎖,然后重復(fù)循環(huán)。接下來(lái)的libusb_try_lock_events()將會(huì)失敗,所以你將變成一個(gè)事件等待者。想獲取更多信息,請(qǐng)閱讀下面的完整流程。 3. libusb_handle_events_locked() 是一個(gè)libusb_handle_events_timeout() 的變體,你可以在持有事件鎖的的情況下調(diào)用。 libusb_handle_events_timeout() 它本身的實(shí)現(xiàn)邏輯類似上面的,所以確保當(dāng)你正在libusb后面工作的時(shí)候不要調(diào)用它,就如這里的原因一樣。 4. libusb_event_handler_active() 判斷是否有線程占用事件鎖。 你也許會(huì)驚訝為什么沒(méi)有一個(gè)函數(shù)能喚醒所有調(diào)用 libusb_wait_for_event()的線程。這是因?yàn)閘ibusb可以在其內(nèi)部完成這項(xiàng)操作:當(dāng)有人調(diào)用libusb_unlock_events()或者傳輸結(jié)束的時(shí)候,它將喚醒所有這樣的線程 (在回調(diào)函數(shù)返回的時(shí)候)。 完整流程 以上的解釋應(yīng)該足夠你繼續(xù)下去,但是如果你真的仔細(xì)思考這個(gè)問(wèn)題,你可能會(huì)對(duì)libusb的內(nèi)部有一些更多的疑問(wèn)。如果你很好奇,繼續(xù)讀下去,如果不是,請(qǐng)?zhí)^(guò)下面的章節(jié)以避免使你困惑。 首先從你腦海中跳出來(lái)的問(wèn)題是:當(dāng)另一個(gè)線程正在處理事件的時(shí)候,如果一個(gè)線程修改了需要被使用的文件描述符集合會(huì)怎么樣? 可能會(huì)發(fā)生以下2情況。 1. libusb_open() 將會(huì)增加另一個(gè)文件描述符到使用集合中,因此中斷事件處理器是合理的,以至于它接管新的描述符后重新啟動(dòng)。 2. libusb_close() 將會(huì)從使用集合中移除一個(gè)文件描述符。有很多的競(jìng)爭(zhēng)條件在這里發(fā)生,所以在這時(shí)沒(méi)有正在處理事件是很重要的。 Libusb在內(nèi)部處理這些問(wèn)題,所以應(yīng)用程序開發(fā)者在開啟或者關(guān)閉設(shè)備的時(shí)候不需要停止他們的事件處理器。下面是它如何工作的,先來(lái)看 libusb_close()的情況: 1. 在初始化的時(shí)候,libusb打開一個(gè)內(nèi)部管道,然后增加管道的讀端到要使用的文件描述符集合中。 2. 在調(diào)用libusb_close()的時(shí)候,libusb在這個(gè)控制管道上寫一些復(fù)制下來(lái)的數(shù)據(jù)。這回立即中斷事件處理程序。libusb也會(huì)在內(nèi)部為這個(gè)高級(jí)別事件記錄下來(lái)它正在嘗試中斷事件處理程序。 3. 在這時(shí),上面的一些函數(shù)開始不同的行為: o libusb_event_handling_ok() 開始返回1,表示事件處理是不能繼續(xù)的。 o libusb_try_lock_events() 開始返回1,表示另一個(gè)線程持有事件處理鎖,即使鎖沒(méi)有被占用。 o libusb_event_handler_active()開始返回1,表示另一個(gè)線程正在處理事件,即使并有處理。 4. 上面的在事件處理停止的處理結(jié)果中發(fā)生改變。并迅速的放棄鎖,給高級(jí)別的libusb_close() 操作一個(gè)"便利"去獲取事件鎖。所有爭(zhēng)奪處理事件的線程都變成事件等待者。 5. 在 libusb_close()持有事件鎖,libusb可以從輪詢集合中安全的刪除文件描述符,in the safety of knowledge that 沒(méi)有任何線程正在輪詢那些描述符或者正嘗試訪問(wèn)輪詢集合。 6. 在獲取事件鎖后,關(guān)閉操作快速完成(通常幾毫秒) 然后直接釋放事件鎖。 7. 同時(shí),libusb_event_handling_ok() 執(zhí)行,并且其他的都返回都原始狀態(tài),文檔描述的行為。 8. 事件鎖的釋放會(huì)喚醒所有正在等待事件的線程,然后開始競(jìng)爭(zhēng)再次成為事件處理程序。他們中的一個(gè)將會(huì)成功;它將重新獲得輪詢描述符的列表,然后USB I/O將會(huì)正常執(zhí)行下去。 libusb_open()是相似的,而且是一個(gè)更相似的例子,當(dāng)調(diào)用libusb_open()的時(shí)候: 1. 設(shè)備是被打開的,并且文件描述符是被加入到輪詢集合的。 2. libusb發(fā)送一些復(fù)制數(shù)據(jù)到控制管道,然后記錄它正在嘗試修改輪詢描述符集合。 3. 事件處理程序被中斷,就如同libusb_close()的效果一樣,發(fā)生相同的行為改變,引起所有的事件處理線程編程事件等待者。 4. libusb_open() 實(shí)現(xiàn)使用它的免費(fèi)權(quán)利獲得事件鎖。 5. 很順利的它暫停了事件處理程序,libusb_open()釋放事件鎖。 6. 事件等待者線程被喚醒,然后再次爭(zhēng)奪成為事件處理程序。其中一個(gè)再次成功獲取包含新增設(shè)備的輪詢描述符列表。 結(jié)束語(yǔ) 上面的內(nèi)容也許看起來(lái)有點(diǎn)復(fù)雜,但是我希望我已經(jīng)講明白為什么這么復(fù)雜是必要的。同樣,不要忘記這只應(yīng)用于那些將使用libusb的文件描述符和集成它們到自己的輪詢循環(huán)中的應(yīng)用程序。 你也許認(rèn)為在你的多線程應(yīng)用程序中忽略一些上面詳細(xì)說(shuō)到的規(guī)則和鎖是沒(méi)有問(wèn)題的,因?yàn)槟悴徽J(rèn)為2個(gè)線程會(huì)在同一時(shí)刻輪詢一個(gè)描述符。如果是這種原因,那么這對(duì)你是個(gè)好消息,因?yàn)槟悴恍枰獡?dān)心。但是請(qǐng)注意這里,記住同步I/O函數(shù)在內(nèi)部處理事件。如果你有一個(gè)線程在循環(huán)中處理事件 (沒(méi)有實(shí)現(xiàn)上述文檔所說(shuō)的規(guī)則和鎖的概念) ,并且另一個(gè)線程正在嘗試發(fā)送一個(gè)同步USB傳輸,將發(fā)生2個(gè)線程監(jiān)視同一描述符的結(jié)果,并且會(huì)發(fā)生上述不希望發(fā)生的問(wèn)題。解決法案就是讓你的輪詢線程遵循規(guī)則,同步I/O函數(shù)也是這么做的,并且這將使線程之間處理的更協(xié)調(diào)。 如果你已經(jīng)有一個(gè)用于處理事件的線程,它占有事件鎖很長(zhǎng)時(shí)間是完全合理的。任何你從其他線程調(diào)用的同步I/O函數(shù)都會(huì)顯而易見的退回到上面詳細(xì)說(shuō)到的“事件等待者”的機(jī)制狀態(tài)下。你的事件處理線程唯一需要考慮做的就是與libusb_event_handling_ok()有關(guān)的事情:你必須在每個(gè)poll()之前調(diào)用它,如果已經(jīng)被使用你必須放棄事件鎖。 |
|
|