[Linux] spin lock | read-write lock

In the previous thread learning, the locks used are all pending and waiting locks. If the lock cannot be applied for, it will wait in the lock;

Spinlocks are not so similar

Article directory

  • 1. Spin lock
    • 1.1 Concept
    • 1.2 Interface
      • 1.2.1 pthread_spin_init/destroy
      • 1.2.2 pthread_spin_lock
      • 1.2.3 pthread_spin_unlock
  • 2. Read-write lock
    • 2.1 The relationship between readers and writers
    • 2.2 Interface
      • 2.2.1 init/destroy
      • 2.2.2 Reader locking
      • 2.2.3 Writer lock
      • 2.2.4 Set the properties of the lock
    • 2.3 Code

1. Spinlock

1.1 Concept

The spin lock is a polling detection lock. Its detection mechanism is not to hang and wait, but to continuously ask whether the lock is free; similar to a trylock( )

Because it requires constant polling and detection, it will occupy a certain amount of CPU resources; if there are many threads, it will easily cause a load on the CPU.

However, the spin lock does not need to wake up the thread in the suspended waiting state, and its consumption is relatively small.

in conclusion:

  • Spin locks are suitable for situations where the competition is not fierce and the critical section is small (short stay time)
  • Spin locks are not suitable for a large number of threads and long critical sections

The advantages and disadvantages of spin locks, in turn, are the advantages and disadvantages of hanging and waiting for locks. We need to choose the correct type of lock according to different scenarios

1.2 interface

The related interface is very similar to mutex, so the effect of using it will not be demonstrated here

1.2.1 pthread_spin_init/destroy

#include <pthread.h>

int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

1.2.2 pthread_spin_lock

The spin lock also has a trylock interface, which is used to determine whether the lock is ready

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);

1.2.3 pthread_spin_unlock

#include <pthread.h>
int pthread_spin_unlock(pthread_spinlock_t *lock);

2. Read-write lock

Sometimes, we will have a config configuration file, which will be read by many threads, but rarely modified and written.

At this time, if we lock the reading of the configuration file, it will easily lead to efficiency problems, and many threads will be blocked continuously, resulting in performance loss.

At this point, you can use a dedicated read-write lock to add different locks to readers and writers, so as to improve read performance while ensuring that writes do not conflict

2.1 The relationship between readers and writers

Needless to say, there must be a mutually exclusive relationship between writing and writing;

Readers and writers also have a mutually exclusive relationship. When writing, they cannot be read, otherwise ambiguity problems will easily occur;

  • The writer writes a, thread A reads it, and the result is a
  • The writer continues to write b, and thread B reads it, and the result is ab

This is because the writing of the writer has not been completed, so readers A and B will get completely different results, which is wrong;

There is no relationship between readers and readers, because readers do not modify data, nor take data away, and their existence has no impact on critical resources.

2.2 Interface

2.2.1 init/destroy

Read-write locks only need to initialize one lock, and there is no need to initialize two different locks for readers and writers

#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);

2.2.2 Reader lock

The reader lock/writer lock of the read-write lock is separate, and we need to call different locks for different threads

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

But the unlocked interface is the same

#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

2.2.3 Writer lock

#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

2.2.4 Set the properties of the lock

See PTHREAD_RWLOCKATTR_SETKIND_NP – Linux man page

Read-write locks allow us to set whether readers are preferred or writers are preferred. If the default attributes are used, there may be a situation where the reader has been reading and the writer has no way to write (printing misalignment is a normal situation)

[muxue@bt-7274:~/git/linux/code/23-01-20 rwlock]$ ./test
reader [140484801697536]
reader [140484801697536] 0
reader [140484793304832]
reader [reader [140484784912128140484793304832]
reader [140484784912128] 0
] 0
reader [140484759734016]
reader [140484759734016] 0
reader [140484776519424]

At this time, the writer starvation problem occurs. The writer cannot access critical resources and starves to death

We can set the properties of the read-write lock according to our own needs

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
There are 3 options in pref
PTHREAD_RWLOCK_PREFER_READER_NP (default setting) Reader priority, may lead to writer starvation
PTHREAD_RWLOCK_PREFER_WRITER_NP Writer priority, currently has a BUG, resulting in performance behavior and
PTHREAD_RWLOCK_PREFER_READER_NP consistent
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP The writer has priority, but the writer cannot lock recursively to avoid deadlock
*/

Here is an example

pthread_rwlock_t rwlock;//lock
pthread_rwlockattr_t attr;//Attribute
pthread_rwlockattr_init( & amp;attr);//initialization attribute
pthread_rwlockattr_setkind_np( & amp;attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);//Set the attribute of the lock to the writer priority
pthread_rwlock_init( & amp;rwlock, & amp;attr);//Initialize and set the attributes of the read-write lock

2.3 Code

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

volatile int board = 0;//critical resource

pthread_rwlock_t rw;//global read-write lock

void *reader(void* args)
{<!-- -->
    const char *name = static_cast<const char *>(args);
    cout << "reader ["<<pthread_self() <<"]"<< endl;
    //sleep(1);
    while(true)
    {<!-- -->
        pthread_rwlock_rdlock( & rw);
        cout << "reader ["<<pthread_self() <<"] " << board << endl;
        pthread_rwlock_unlock( & rw);
        usleep(110);
    }
}

void *writer(void *args)
{<!-- -->
    const char *name = static_cast<const char *>(args);
    //sleep(1);
    while(true)
    {<!-- -->
        pthread_rwlock_wrlock( & rw);
        board + + ;
        cout << "writer [" << pthread_self() <<"]"<< endl;
        pthread_rwlock_unlock( & rw);
        usleep(100);
    }
}

int main()
{<!-- -->
    pthread_rwlock_init( &rw, nullptr);
    pthread_t r1, r2, r3, r4, r5, r6, w1, w2;
    pthread_create( & amp;r1, nullptr, reader, (void*)"reader");
    pthread_create( &r2, nullptr, reader, (void*)"reader");
    pthread_create( &r3, nullptr, reader, (void*)"reader");
    pthread_create( &r4, nullptr, reader, (void*)"reader");
    pthread_create( &r5, nullptr, reader, (void*)"reader");
    pthread_create( &r6, nullptr, reader, (void*)"reader");
    pthread_create( &w1, nullptr, writer, (void*)"writer");
    pthread_create( & amp;w2, nullptr, writer, (void*)"writer");


    pthread_join(r1, nullptr);
    pthread_join(r2, nullptr);
    pthread_join(r3, nullptr);
    pthread_join(r4, nullptr);
    pthread_join(r5, nullptr);
    pthread_join(r6, nullptr);
    pthread_join(w1, nullptr);
    pthread_join(w2, nullptr);

    pthread_rwlock_destroy( & rw);
    return 0;
}

The result of the operation is as follows

reader [140067523458816] 7086
reader [140067548636928] 7086
writer [140067515066112]
writer [140067506673408]
reader [140067540244224] 7088
reader [140067565422336] 7088
reader [140067557029632] 7088
reader [140067531851520] 7088
reader [140067523458816] 7088
writer [140067515066112]
writer [140067506673408]
reader [140067548636928] 7090
writer [140067515066112]
reader [140067523458816] 7091
writer [140067506673408]
reader [140067548636928] 7092
reader [140067557029632] 7092

You can see that the reader has many threads and can read the data correctly.

If you add sleep(10) to the reader’s while

void *reader(void* args)
{<!-- -->
    const char *name = static_cast<const char *>(args);
    cout << "reader ["<<pthread_self() <<"]"<< endl;
    while(true)
    {<!-- -->
        pthread_rwlock_rdlock( & rw);
        cout << "reader ["<<pthread_self() <<"] " << board << endl;
        sleep(10);//sleep
        pthread_rwlock_unlock( & rw);
        usleep(110);//Avoid only one thread working
    }
}

It can be seen that there is no conflict between multiple readers, and there will be no situation where reader B cannot access the critical section after reader A applies for a lock. If it is a mutual exclusion lock, reader A goes to sleep after applying, and B cannot apply for the lock.

image-20230120163852340