Contoh mutex / tutorial? [Tutup]

176

Saya baru mengenal multithreading, dan berusaha memahami cara kerja mutex. Memang banyak Googling tetapi masih meninggalkan beberapa keraguan tentang cara kerjanya karena saya membuat program sendiri di mana penguncian tidak berfungsi.

Salah satu sintaks yang benar-benar non-intuitif dari mutex adalah pthread_mutex_lock( &mutex1 );, di mana sepertinya mutex sedang dikunci, ketika apa yang ingin saya kunci adalah variabel lain. Apakah sintaksis ini berarti bahwa mengunci suatu mutex mengunci suatu wilayah kode sampai mutex tidak dikunci? Lalu bagaimana cara mengetahui bahwa wilayah tersebut dikunci? [ UPDATE: Utas tahu bahwa wilayah ini dikunci, oleh Memory Fencing ]. Dan bukankah fenomena seperti itu seharusnya disebut bagian kritis? [ UPDATE: Objek bagian kritis hanya tersedia di Windows, di mana objek lebih cepat daripada mutex dan hanya dapat dilihat oleh utas yang mengimplementasikannya. Jika tidak, bagian kritis hanya mengacu pada area kode yang dilindungi oleh mutex ]

Singkatnya, dapatkah Anda membantu dengan program contoh mutex yang paling sederhana dan penjelasan yang paling sederhana tentang logika cara kerjanya? Saya yakin ini akan membantu banyak pemula lainnya.

Nav
sumber
2
Terus menekankan pada perlunya tutorial sederhana (baik itu meningkatkan thread, tbb atau pthreads): Contoh-contoh kebingungan: 1. stackoverflow.com/questions/3528877/… 2. stackoverflow.com/questions/2979525/… 3. stackoverflow.com/questions/2095977/to-mutex-atau-not-to-mutex 4. stackoverflow.com/questions/3931026/… 5. stackoverflow.com/questions/1525189/…
Nav
1
Saya tidak bermaksud menyinggung ini, tetapi komentar terakhir Anda menyarankan kepada saya adalah bahwa kita membutuhkan lebih sedikit analogi dan penjelasan teknis yang lebih baik tentang bagaimana sebuah mutex bekerja dan mengapa kita membutuhkannya.
San Jacinto
@San: Jangan tersinggung :) Komentar saya hanya dimaksudkan untuk menyarankan bahwa seorang pemula bisa mendapatkan penjelasan paling singkat, paling jelas tentang mutex. Banyak analogi bisa membingungkan bagi pemula, sehingga analogi yang berbeda harus disimpan secara terpisah. Seluruh alasan bagi saya memposting ques dan ans adalah karena sebagai pemula, saya merasa sulit untuk membaca penjelasan panjang dan contoh kode. Saya tidak ingin orang lain melewati rasa sakit.
Nav
2
@Cory: Jika jawaban ini dapat ditingkatkan, saya akan dengan senang hati menerima saran Anda. Saya senang bahwa banyak orang lain yang merasa terbantu. Jika tidak membantu Anda, ada jawaban dari orang lain juga yang telah menunjuk ke tutorial mutex lainnya. Mengapa bersikap begitu negatif?
Nav

Jawaban:

278

Ini dia usaha sederhana saya untuk menjelaskan konsep ini kepada pemula di seluruh dunia: ( versi kode warna di blog saya juga)

Banyak orang berlari ke bilik telepon yang terpisah (mereka tidak memiliki ponsel) untuk berbicara dengan orang yang mereka cintai. Orang pertama yang menangkap gagang pintu stan, adalah orang yang diizinkan menggunakan telepon. Dia harus tetap memegang gagang pintu selama dia menggunakan telepon, kalau tidak orang lain akan memegang gagangnya, mengusirnya dan berbicara dengan istrinya :) Tidak ada sistem antrian seperti itu. Ketika orang itu selesai menelepon, keluar dari bilik dan meninggalkan pegangan pintu, orang berikutnya yang memegang gagang pintu akan diizinkan menggunakan telepon.

Sebuah benang adalah: Setiap orang
yang mutex adalah: Pintu menangani
The kunci adalah: Tangan seseorang
yang sumber daya adalah: Ponsel

Setiap utas yang harus menjalankan beberapa baris kode yang tidak boleh dimodifikasi oleh utas lain secara bersamaan (menggunakan telepon untuk berbicara dengan istrinya), harus terlebih dahulu mendapatkan kunci pada sebuah mutex (memegangi pegangan pintu bilik) ). Hanya dengan demikian sebuah utas dapat menjalankan jalur kode tersebut (melakukan panggilan telepon).

Setelah utas mengeksekusi kode itu, ia harus melepaskan kunci pada mutex sehingga utas lain dapat memperoleh kunci pada mutex (orang lain dapat mengakses bilik telepon).

[ Konsep memiliki mutex agak tidak masuk akal ketika mempertimbangkan akses eksklusif dunia nyata, tetapi di dunia pemrograman saya kira tidak ada cara lain untuk membiarkan utas lain 'melihat' bahwa utas sudah mengeksekusi beberapa baris kode. Ada konsep mutex rekursif dll, tetapi contoh ini hanya dimaksudkan untuk menunjukkan kepada Anda konsep dasar. Semoga contoh memberi Anda gambaran yang jelas tentang konsep tersebut. ]

Dengan C ++ 11 threading:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Kompilasi dan jalankan menggunakan g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Alih-alih menggunakan lockdan unlock, Anda dapat menggunakan tanda kurung seperti yang ditunjukkan di sini , jika Anda menggunakan kunci scoped untuk keuntungan yang diberikannya . Namun, scoped locks memiliki sedikit overhead kinerja.

Nav
sumber
2
@San: Saya akan jujur; Ya, saya suka fakta bahwa Anda sudah mencoba yang terbaik untuk menjelaskan detail (dengan aliran) kepada seorang pemula yang lengkap. TETAPI, (tolong jangan salah paham saya) maksud dari posting ini adalah untuk menempatkan konsep dalam penjelasan singkat (karena jawaban lain menunjuk ke tutorial panjang). Saya harap Anda tidak keberatan jika saya meminta Anda untuk menyalin seluruh jawaban Anda dan mempostingnya sebagai jawaban yang terpisah? Sehingga saya dapat mengembalikan dan mengedit jawaban saya untuk menunjukkan jawaban Anda.
Nav
2
@ Tom Dalam hal ini, Anda seharusnya tidak mengakses mutex itu. Operasi di atasnya harus dienkapsulasi sehingga apa pun yang dijaga dilindungi dari tindakan bodoh tersebut. Jika saat Anda menggunakan API yang terpapar perpustakaan, perpustakaan dijamin aman-utas, maka Anda aman untuk menyertakan mutex yang jelas berbeda untuk melindungi item yang Anda bagikan sendiri. Jika tidak, Anda memang menambahkan pegangan pintu baru, seperti yang Anda sarankan.
San Jacinto
2
Untuk memperluas poin saya, apa yang ingin Anda lakukan adalah menambahkan ruangan lain yang lebih besar di sekitar stan. Kamar juga dapat berisi toilet dan shower. Katakanlah hanya 1 orang yang diizinkan masuk ke kamar sekaligus. Anda harus mendesain ruangan sehingga ruangan ini harus memiliki pintu dengan pegangan yang melindungi pintu masuk seperti halnya bilik telepon. Jadi sekarang, meskipun Anda memiliki mutex tambahan, Anda dapat menggunakan kembali bilik telepon dalam proyek apa pun. Pilihan lain adalah mengekspos mekanisme penguncian untuk setiap perangkat di ruangan dan mengelola kunci di kelas kamar. Bagaimanapun, Anda tidak akan menambahkan kunci baru ke objek yang sama.
San Jacinto
8
Contoh threading C ++ 11 Anda salah . Begitu juga dengan TBB, petunjuknya ada dalam nama scoped lock .
Jonathan Wakely
3
Saya sangat menyadari keduanya, @Jonathan. Anda sepertinya telah melewatkan kalimat yang saya tulis (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Sedangkan untuk menggunakan penguncian ruang lingkup, tergantung pada pengembang, tergantung pada jenis aplikasi yang mereka bangun. Jawaban ini dimaksudkan untuk mengatasi pemahaman dasar tentang konsep mutex dan tidak untuk masuk ke semua kompleksitasnya, jadi komentar dan tautan Anda disambut baik tetapi sedikit di luar cakupan tutorial ini.
Nav
41

Sementara mutex dapat digunakan untuk memecahkan masalah lain, alasan utama mereka ada adalah untuk memberikan pengecualian bersama dan dengan demikian memecahkan apa yang dikenal sebagai kondisi ras. Ketika dua (atau lebih) utas atau proses berusaha mengakses variabel yang sama secara bersamaan, kami memiliki potensi untuk kondisi balapan. Pertimbangkan kode berikut

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

Bagian dalam dari fungsi ini terlihat sangat sederhana. Itu hanya satu pernyataan. Namun, bahasa padanan pseudo-assembly yang khas mungkin:

load i from memory into a register
add 1 to i
store i back into memory

Karena semua instruksi bahasa assembly yang setara diperlukan untuk melakukan operasi kenaikan pada i, kami mengatakan bahwa penambahan i adalah operasi non-atmoik. Operasi atom adalah operasi yang dapat diselesaikan pada perangkat keras dengan jaminan tidak akan terganggu begitu eksekusi instruksi telah dimulai. Bertambah i terdiri dari rantai 3 instruksi atom. Dalam sistem bersamaan di mana beberapa utas memanggil fungsi, masalah muncul ketika utas membaca atau menulis pada waktu yang salah. Bayangkan kita memiliki dua utas yang berjalan secara simultan dan satu memanggil fungsi segera setelah yang lainnya. Katakan juga bahwa kita telah menginisialisasi ke 0. Juga berasumsi bahwa kita memiliki banyak register dan bahwa dua utas menggunakan register yang sama sekali berbeda, sehingga tidak akan ada tabrakan. Waktu sebenarnya dari peristiwa ini mungkin:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

Apa yang terjadi adalah kita memiliki dua utas yang bertambah secara bersamaan, fungsi kita dipanggil dua kali, tetapi hasilnya tidak konsisten dengan fakta itu. Sepertinya fungsinya hanya dipanggil sekali. Ini karena atomisitasnya "rusak" pada tingkat mesin, artinya benang dapat saling mengganggu atau bekerja sama pada waktu yang salah.

Kami membutuhkan mekanisme untuk menyelesaikan ini. Kita perlu memaksakan pemesanan untuk instruksi di atas. Satu mekanisme umum adalah memblokir semua utas kecuali satu. Mutth pthread menggunakan mekanisme ini.

Utas apa pun yang harus menjalankan beberapa baris kode yang dapat secara tidak aman mengubah nilai bersama oleh utas lainnya pada saat yang sama (menggunakan telepon untuk berbicara dengan istrinya), pertama-tama harus dibuat mendapatkan kunci pada mutex. Dengan cara ini, utas apa pun yang memerlukan akses ke data bersama harus melewati kunci mutex. Hanya dengan demikian utas dapat mengeksekusi kode. Bagian kode ini disebut bagian kritis.

Setelah utas mengeksekusi bagian kritis, itu harus melepaskan kunci pada mutex sehingga utas lain dapat memperoleh kunci pada mutex.

Konsep memiliki mutex tampak agak aneh ketika mempertimbangkan manusia mencari akses eksklusif ke objek fisik nyata, tetapi ketika pemrograman, kita harus disengaja. Utas dan proses bersamaan tidak memiliki asuhan sosial dan budaya yang kita lakukan, jadi kita harus memaksa mereka untuk berbagi data dengan baik.

Jadi secara teknis, bagaimana cara kerja mutex? Bukankah itu menderita dari kondisi ras yang sama yang kami sebutkan sebelumnya? Bukankah pthread_mutex_lock () sedikit lebih rumit dari peningkatan variabel?

Secara teknis, kami membutuhkan dukungan perangkat keras untuk membantu kami. Perancang perangkat keras memberi kita instruksi mesin yang melakukan lebih dari satu hal tetapi dijamin atomik. Contoh klasik dari instruksi semacam itu adalah test-and-set (TAS). Ketika mencoba mendapatkan kunci pada sumber daya, kita mungkin menggunakan TAS mungkin memeriksa untuk melihat apakah nilai dalam memori adalah 0. Jika ya, itu akan menjadi sinyal kita bahwa sumber daya sedang digunakan dan kita tidak melakukan apa-apa (atau lebih akurat) , kita menunggu dengan beberapa mekanisme. Mutex pthreads akan menempatkan kita ke dalam antrian khusus dalam sistem operasi dan akan memberi tahu kita ketika sumber daya tersedia. Sistem Dumber mungkin mengharuskan kita untuk melakukan putaran putaran yang ketat, menguji kondisi berulang-ulang) . Jika nilai dalam memori bukan 0, TAS mengatur lokasi ke sesuatu selain 0 tanpa menggunakan instruksi lain. Itu' itu seperti menggabungkan dua instruksi perakitan menjadi 1 untuk memberi kita atomisitas. Dengan demikian, pengujian dan perubahan nilai (jika perubahan itu sesuai) tidak dapat terganggu setelah dimulai. Kita dapat membangun mutex di atas instruksi semacam itu.

Catatan: beberapa bagian mungkin terlihat mirip dengan jawaban sebelumnya. Saya menerima undangannya untuk mengedit, dia lebih suka dengan cara aslinya, jadi saya menjaga apa yang saya miliki yang diinfuskan dengan sedikit kata-katanya.

San Jacinto
sumber
1
Terima kasih banyak, San. Saya telah menautkan jawaban Anda :) Sebenarnya, saya bermaksud agar Anda mengambil jawaban saya + jawaban Anda dan mempostingnya sebagai jawaban terpisah, untuk menjaga alurnya. Saya tidak keberatan jika Anda menggunakan kembali sebagian dari jawaban saya. Kami tidak melakukan ini untuk diri kami sendiri.
Nav
13

Tutorial utas terbaik yang saya tahu ada di sini:

https://computing.llnl.gov/tutorials/pthreads/

Saya suka itu ditulis tentang API, bukan tentang implementasi tertentu, dan memberikan beberapa contoh sederhana yang bagus untuk membantu Anda memahami sinkronisasi.

R .. GitHub BERHENTI MEMBANTU ICE
sumber
Saya setuju ini pasti tutorial yang bagus, tetapi banyak informasi di satu halaman dan programnya panjang. Pertanyaan yang saya posting adalah versi mutex dari pidato "I have a dream", di mana pemula akan menemukan cara sederhana untuk belajar tentang mutex dan memahami bagaimana sintaksis non-intuitif bekerja (ini adalah salah satu penjelasan yang kurang dalam semua tutorial) .
Nav
7

Saya menemukan posting ini baru-baru ini dan berpikir bahwa itu membutuhkan solusi yang diperbarui untuk perpustakaan standar c ++ 11 mutex (yaitu std :: mutex).

Saya telah menempelkan beberapa kode di bawah ini (langkah pertama saya dengan mutex - saya belajar concurrency pada win32 dengan HANDLE, SetEvent, WaitForMultipleObjects dll).

Karena ini adalah upaya pertama saya dengan std :: mutex dan teman-teman, saya ingin melihat komentar, saran, dan peningkatan!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}
makanan ikan
sumber
1
Super! Terima kasih sudah memposting. Padahal seperti yang telah saya sebutkan sebelumnya, tujuan saya hanyalah menjelaskan konsep mutex. Semua tutorial lainnya membuatnya sangat sulit dengan konsep tambahan konsumen konsumen dan variabel kondisi dll, yang membuatnya sangat sulit bagi saya untuk memahami apa yang sedang terjadi.
Nav
4

Fungsi pthread_mutex_lock()dapat memperoleh mutex untuk utas panggilan atau memblokir utas sampai mutex dapat diperoleh. Yang terkait pthread_mutex_unlock()merilis mutex.

Pikirkan tentang mutex sebagai antrian; setiap utas yang berupaya mendapatkan mutex akan ditempatkan di akhir antrian. Ketika utas melepaskan mutex, utas berikutnya dalam antrian terlepas dan sekarang berjalan.

Bagian kritis mengacu pada wilayah kode di mana non-determinisme dimungkinkan. Seringkali ini karena banyak utas berusaha mengakses variabel yang dibagi. Bagian kritis tidak aman sampai sinkronisasi dilakukan. Kunci mutex adalah salah satu bentuk sinkronisasi.

chrisaycock
sumber
1
Apakah dijamin bahwa utas upaya berikutnya akan masuk?
Arsen Mkrtchyan
1
@ Arsen Tidak ada jaminan. Itu hanya analogi yang bermanfaat.
chrisaycock
3

Anda seharusnya memeriksa variabel mutex sebelum menggunakan area yang dilindungi oleh mutex. Jadi pthread_mutex_lock Anda () dapat (tergantung pada implementasi) menunggu sampai mutex1 dilepaskan atau mengembalikan nilai yang menunjukkan bahwa kunci tidak dapat diperoleh jika orang lain telah menguncinya.

Mutex sebenarnya hanya sebuah semaphore yang disederhanakan. Jika Anda membaca tentang mereka dan memahaminya, Anda memahami mutex. Ada beberapa pertanyaan tentang mutex dan semaphore di SO. Perbedaan antara semaphore biner dan mutex , Kapan kita harus menggunakan mutex dan kapan kita harus menggunakan semaphore dan sebagainya. Contoh toilet di tautan pertama adalah contoh yang baik yang bisa dipikirkan orang. Semua kode dilakukan untuk memeriksa apakah kunci tersedia dan jika ada, cadangan. Perhatikan bahwa Anda tidak benar-benar memesan toilet itu sendiri, tetapi kuncinya.

Makis
sumber
1
pthread_mutex_locktidak dapat kembali jika orang lain memegang kunci. Memblokir dalam kasus ini dan itulah intinya. pthread_mutex_trylockadalah fungsi yang akan kembali jika kunci dipegang.
R .. GitHub STOP BANTUAN ICE
1
Ya, pada awalnya saya tidak menyadari apa implementasi ini.
Makis
3

Bagi mereka yang mencari contoh mutex shortex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}
Kosong
sumber