Linux内核系列:多线程和线程同步

线程间通信

  1. 不同进程的线程之间通信,等同于进程间通信。
  2. 相同进程中不同线程之间通信,主要是线程同步,避免对临界区资源的破坏。

线程同步

  1. 互斥锁

互斥量是最简单的同步机制,即互斥锁。多个进程(线程)均可以访问到一个互斥量,通过对互斥量加锁,从而来保护一个临界区,防止其它进程(线程)同时进入临界区,保护临界资源互斥访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <fcntl.h>  
#include <unistd.h>
#include <error.h>
#include <iostream>
#include <cstring>
using namespace std;
pthread_mutex_t m;

static int resource = 0;

void* func(void* args){
while(true){
pthread_mutex_lock(&m);
pthread_t res = pthread_self();
cout << "current thread : " << res << endl;
cout << "resource: " << resource << endl;
++ resource;
pthread_mutex_unlock(&m);
}
return 0;
}

#define handler(msg) \
perror(msg);\
exit(-1);
int main(int argc, char **argv) {
pthread_t p1, p2;

pthread_mutex_init(&m, nullptr);

if(pthread_create(&p1, nullptr, func, 0)){
handler("thread create error");
}
if(pthread_create(&p2, nullptr, func, 0)){
handler("thread create error");
}
if(pthread_join(p1, nullptr)){
handler("error");
}
if(pthread_join(p2, nullptr)){
handler("error");
}
return 0;
}
  1. 条件变量

条件变量是并发编程中的一种同步机制。条件变量使得线程能够阻塞到等待某个条件发生后,再继续执行

  • 条件变量的实现原理:
    条件变量其实就是一种等待机制,每个条件变量对应一个等待原因和等待队列,通常有两种操作:wait和signal。
    条件变量的伪代码:
    1
    2
    3
    4
    class Condition{
    int waitNum = 0;
    WaitQueue q;
    };
    wait操作分为两步,首先对waitNum + 1,然后将当前进程加入等待队列中。
    1
    2
    3
    4
    5
    6
    7
    8
    Condition::Wait(lock)
    {
    ++numWaiting;
    Add current thread to WaitQueue q;
    release(lock);
    schedule(); //调度机制,让出CPU
    acquire(lock);
    }
    对于singal操作来说,则如果等待队列中有线程的话,将它取出唤醒,waitNum减一即可。
    1
    2
    3
    4
    5
    6
    7
    8
    Condition::Singal(){
    if(waitNum > 0)
    {
    remove t from q;
    wakeup(q);
    --waitNum;
    }
    }
    条件变量的原理虽然很简单,但是使用起来却是比较费脑,还是参考陈硕大佬这篇文章正确使用,下面是一个保证不会出错的使用方法。经典例子见github
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Waiter7 : public Waiter
{
public:
void wait() override
{
pthread_mutex_lock(&mutex_);
while (!signaled_)
{
pthread_cond_wait(&cond_, &mutex_);
}
pthread_mutex_unlock(&mutex_);
}

void signal() override // Sorry, bad name in base class, poor OOP
{
broadcast();
}

void broadcast()
{
pthread_mutex_lock(&mutex_);
pthread_cond_broadcast(&cond_);
signaled_ = true;
pthread_mutex_unlock(&mutex_);
}

private:
bool signaled_ = false;
};

条件变量里面有个经典的假唤醒问题,特别是在广播的时候,一个条件满足了,但是当被唤醒线程拿到资源的时候其实已经被其他线程先行一步消耗了,因此需要在等待时使用while循环判断和等待。
这个其实和网络编程里面的惊群现象比较相似。

  1. 读写锁

读写锁的核心思想是:将线程访问共享数据时发出的请求分为两种,分别是:

  • 读请求:只读取共享数据,不做任何修改;
  • 写请求:存在修改共享数据的行为。

当有多个线程发出读请求时,这些线程可以同时执行,也就是说,共享数据的值可以同时被多个发出读请求的线程获取;当有多个线程发出写请求时,这些线程只能一个一个地执行(同步执行)。此外,当发出读请求的线程正在执行时,发出写请求的线程必须等待前者执行完后才能开始执行;当发出写请求的线程正在执行时,发出读请求的线程也必须等待前者执行完后才能开始执行。

本质上,读写锁就是一个全局变量,发出读请求和写请求的线程都可以访问它。为了区别线程发出的请求类别,当读写锁被发出读请求的线程占用时,我们称它为“读锁”;当读写锁被发出写请求的线程占用时,称它为“写锁”。

当前读写锁模式 读请求 写请求
无锁 允许占用 允许占用
读锁 允许占用 阻塞线程执行
写锁 阻塞线程执行 阻塞线程执行
  • 当读写锁未被任何线程占用时,发出读请求和写请求的线程都可以占用它。注意,由于读请求和写请求的线程不能同时执行,读写锁默认会优先分配给发出读请求的线程。

  • 当读写锁的状态为“读锁”时,表明当前执行的是发出读请求的线程(可能有多个)。此时如果又有线程发出读请求,该线程不会被阻塞,但如果有线程发出写请求,它就会被阻塞,直到读写锁状态改为“无锁”。

  • 当读写锁状态为“写锁”时,表明当前执行的是发出写请求的线程(只能有 1 个)。此时无论其它线程发出的是读请求还是写请求,都必须等待读写锁状态改为“无锁”后才能执行。

总的来说,对于进程空间中的共享资源,读写锁允许发出“读”请求的线程共享资源,发出“写”请求的线程必须独占资源,进而实现线程同步。

  1. 信号量

在linux中进程和线程相似,参考前面的进程间通信