zoukankan      html  css  js  c++  java
  • Linux 系统编程学习笔记

    线程的概念

    线程是操作系统能够进行运算调度的最小单位,包含在进程中,是进程实际运作单位。

    线程共享资源:

    • 文件描述符
    • 每种信号的处理方式(SIG_IGN/SIG_DFL or 自定义信号 处理函数)
    • 当前工作目录
    • 用户id和组id

    每个线程各有一份的资源:

    • 线程id
    • 上下文,包括各种寄存器的值/PC(程序计数器)和栈指针
    • 栈空间
    • errno变量
    • 信号屏蔽字Signal Mask
    • 调度优先级

    线程 vs 进程
    线程是CPU最小的调度的那位,进程是最小的资源分配单位;
    一个进程可以包含多个线程,一个线程只能属于一个进程;
    同一进程下的多个线程共享同一地址空间,不同进程无法共享直接数据;

    线程库由POSIX标准定义,称为POSIX thread或pthread。
    Linux上线程函数位于libpthread共享库,编译时要加上-lpthread选项。

    线程控制

    线程创建

    #include <pthread.h>
    
    // 创建新线程,当前线程返回后继续执行
    // 成功返回0,失败返回-1,错误保存在errno中
    int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg);
    

    当前线程返回后继续执行,新线程执行由函数指针start_routine决定。

    • 线程参数
      start_routine函数接收一个参数,通过arg传递,类型是void* , 含义由调用者自己定义。

    • 线程返回值
      start_routine返回时,新建线程退出。其他线程可以通过调用pthread_join得到start_routine返回值,类似于父进程调用wait得到子进程退出状态。

    • 线程id
      新建线程id被填写到thread参数所指向的内存单元。
      进程id类型pid_t,每个进程id在系统中是唯一的,调用getpid可以得到进程id,是一个正整数。
      线程id类型是thread_t,只在当前进程中是唯一的,不同系统中thread_t有不同实现,可能是一个整数,可能是一个结构体,也可能是一个地址,因此不能简单调用printf打印,而要调用pthread_self获得当前线程id。

    注:调用创建线程的线程,通过thread参数得到的线程id,与新建线程内调用pthread_self得到的线程id是意义的,因为同一进程中线程id是唯一的。

    • 线程属性
      arr表示线程属性,这里的例子都用NULL传给arr参数,表示取缺省值。

    例,使用线程简单示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <pthread.h>
    #include unistd.h>
    
    pthread_t ntid;
    
    void printids(const char *s) {
      pid_t pid;
      pthread_t tid;
    
      pid = getpid(); // 获取当前进程id
      tid = pthread_self(); // 获取当前线程id
      printf("%s pid %u tid %u (0x%x)
    ", s, (unsigned int)pid, (unsigned int )tid, (unsigned int)tid);
    }
    
    void *thr_fn(void *arg) {
      printids(arg);
      return NULL;
    }
    
    int main() {
      int err;
      err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
      if (err != 0) {
        fprintf(stderr, "can't create thread: %s
    ", strerror(err)); 
        exit(1);
      }
    
      printids("main thread:");
      sleep(1);
      return 0;
    }
    

    编译运行结果:

    $ gcc main.c -lpthread
    $ ./a.out
    main thread: pid 7398 tid 3084450496 (0xb7d8fac0)
    new thread:  pid 7398 tid 3084446608 (0xb7d8eb90)
    

    结果分析:main所在线程,和新建线程同属于一个进程,进程id一样,而线程id不一样。由于pthread_create错误码不保存在errno中,因此不能直接用perror打印错误信息,而需要调用strerror把错误码转换成错误信息再打印。
    如果任意一个线程调用exit或_exit,整个进程的所有线程都终止。从main函数return也相当于exit,为防止新建线程还没执行就终止,所以在main return之前延时1秒。

    终止线程

    上面提到终止进程,线程也会终止。有没有什么办法可以终止线程,但不终止进程?
    有三种方法:

    1. 从线程函数return。该方法对主线程不适用,因为从main return相当于调用exit;
    2. 一个线程可以调用pthread_cancel终止同一进程中的另一个进程;
    3. 线程可以调用pthread_exit终止自己;

    用pthread_cancel终止一个线程分同步和异步两种情况。

    pthread_exit和pthread_join

    #include <pthread.h>
    void pthread_exit(void *value_ptr);
    

    pthread_exit或return返回的指针所指向的内存单元必须是全局或者malloc分配的,不能在线程函数的栈上分配。

    #include <pthread.h>
    
    int pthread_join(pthread_ thread, void**value_ptr);
    

    调用pthread_join 的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:

    • 如果thread线程通过return 返回,value_ptr所指向的单元里存放的是thread线程函数的返回值;
    • 如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED(值为-1);
    • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是pthread_exit退出码;
      如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。

    例,用三种方式终止线程,并获取返回值

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void *thr_fn1(void *arg) {
      printf("thread 1 returning
    ");
      return (void *)1;
    }
    
    void *thr_fn2(void *arg) {
      printf("thread 2 exiting
    ");
      pthread_exit((void *)2);
    }
    
    void *thr_fn3(void *arg) {
      while(1) {
        printf("thread 3 writing
    ");
        sleep(1);
      }  
    }
    
    int main() {
      pthread_t tid;
      void *tret;
    
      pthread_create(&tid, NULL, thr_fn1, NULL);
      pthread_join(tid, &tret); // 挂起main线程,等待线程tid结束,tret包含了线程返回信息
      printf("thread 1 exit code %d
    ", (int)tret);
    
      pthread_create(&tid, NULL, thr_fn2, NULL);
      pthread_join(tid, &tret);
      printf("thread 2 exit code %d
    ", (int)tret);
    
      pthread_create(&tid, NULL, thr_fn3, NULL);
      sleep(3);
      pthread_cancel(tid);
      pthread_join(tid, &tret);
      printf("thread 3 exit code %d
    ", (int)tret);
    
      return 0;
    }
    
    

    运行结果:

    thread 1 is returning
    thread 1 exit code 1
    thread 2 is existing
    thread 2 exit code 2
    thread 3 is writing
    thread 3 is writing
    thread 3 exit code -1
    

    最后返回-1,其实是PTHREAD_CANCELED的值。别的线程调用pthread_cancel终止当前线程,被终止线程会返回PTHREAD_CANCELED

    #include <pthread.h>
    
    #define PTHREAD_CANCELED ((void *) -1)
    

    线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止。
    线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。
    不能对一个已经处于detach状态的线程调用pthread_join,调用将返回EINVAL(errno.h);对尚未detach的线程调用pthread_join或phtread_detach可以把该线程置为detach状态。

    注意:不能对同一线程调用2次pthread_join,也不能对同一线程同时调用pthread_detach和pthread_join。

    #include <pthread.h>
    int pthread_detach(pthread_t tid);
    

    线程间同步

    mutex 互斥量

    多个线程同时访问共享数据可能产生冲突。
    比如,一个变量自增1,需要3条指令:

    1. 从内存读取变量值到寄存器;
    2. 寄存器值+1;
    3. 将寄存器值写回内存;

    假如2个线程在多处理器平台上同时执行这三条指令,可能导致下图结果,最后变量只加了1次而非2次。

    思考:单处理器平台执行,会出现这样的问题吗?
    解析:也可能会。在第一个线程从内存取值到寄存器并+1后,写回内存之前,另外一个线程中断了当前线程的执行,将值+1后写回内存,然后回到第一个线程将值写回。这样就还是只加了1次而非2次。

    例子,创建2个线程各自对counter +1进行5000次

    #include <pthread.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    #define NLOOP 5000
    
    int counter ; // 全局变量默认值0
    
    void *doit(void *arg) {
        int val;
    
        for (int i = 0; i < NLOOP; ++i) {
            val = counter;
            printf("%x: %d
    ", (unsigned int)pthread_self(), val + 1);
            counter = val + 1;
        }
    
        return NULL;
    }
    
    // 多线程访问冲突问题
    int main() {
        pthread_t thrA, thrB;
    
        pthread_create(&thrA, NULL, &doit, NULL);
        pthread_create(&thrB, NULL, &doit, NULL);
    
        // 等待线程结束
        pthread_join(thrA, NULL);
        pthread_join(thrB, NULL);
    //    sleep(10);
    
        return 0;
    }
    

    正常情况下,counter应该等于10000,但实际运行5000,也有可能不等于5000,可以尝试运行多条线程。这是因为多线程程序,存在访问冲突的问题。解决办法就是使用互斥锁(Mutex,Mutual Exclusive Lock)。获得锁的线程可以完成“读/写/修改”操作,然后释放给其他线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读/写/修改”就是原子操作,无法被打断。

    Mutex
    Mutex用pthread_mutex_t类型变量表示,初始化和销毁方式

    #include <pthread.h>
    
    // 销毁pthread_mutex_t
    // 成功返回0,出错返回错误号
    // 适用于销毁phtread_mutex初始化的mutex
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    // 初始化Mutex
    // attr 设定Mutex属性,NULL表示使用缺省属性
    // 适用于在代码块内对mutex进行初始化
    int pthread_mutex_init(pthread_mutext_t *restrict mutex, const pthread_mutexattr_t *attr);
    // 初始化Mutex
    // 适用于全局变量或者static变量
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // <=> pthread_mutex_init(&mutex, NULL);
    

    Mutex的加锁/解锁操作

    #include <pthread.h>
    // 挂起等待锁资源Mutex,直到另一个线程unlock锁资源
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    // 尝试获得锁资源Mutex,当前线程不会挂起,如果锁已被占用,返回EBUSY
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    // 释放Mutex资源
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

    线程如果通过lock获得锁资源,就会执行锁后面的代码;如果锁资源已经被获取,线程就会挂起等待另一个线程调用unlock释放资源。
    要确保原子性的代码运行结束后,通过unlock释放锁资源。

    死锁的两种典型情况:

    1. 如果一个线程连续调用2次lock,第1个lock已经获得了锁资源,第2个lock由于锁被占用会挂起等待别的线程unlock,而占用该锁资源的正是自己,这样就形成死锁(Deadlock),线程就永远挂起等待了。
    2. 如果线程A获得锁1,等待锁2,线程B获得锁2,等待锁1,就形成死锁;

    写程序应尽量避免同时使用多个锁,如果要这么做,有一个原则:
    所有线程都按相同的现后顺序获得锁,如一个程序用到锁1、锁2、锁3,那么所有线程需要获得2个或3个锁时,都应该按锁1、锁2、锁3的顺序获得;
    如果要为锁确定顺序很困难,应尽量使用pthread_mutex_trylock代替pthread_mutex_lock,以避免死锁。

    将上面的例子,用Mutex进行改造

    #include <pthread.h>
    #include <stdlib.h>
    #include <stdio.h>
    #define NLOOP 5000
    
    int counter ;
    pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化全局锁
    
    void *doit(void *arg) {
        int val;
    
        for (int i = 0; i < NLOOP; ++i) {
            pthread_mutex_lock(&counter_mutex);     // 挂起等待锁资源
            val = counter;
            printf("%x: %d
    ", (unsigned int)pthread_self(), val + 1);
            counter = val + 1;
            pthread_mutex_unlock(&counter_mutex);   // 释放锁资源
        }
    
        return NULL;
    }
    
    // 多线程访问冲突问题
    int main() {
        pthread_t thrA, thrB, thrC;
    
        pthread_create(&thrA, NULL, &doit, NULL);
        pthread_create(&thrB, NULL, &doit, NULL);
    
        // 等待线程结束
        pthread_join(thrA, NULL);
        pthread_join(thrB, NULL);
    //    sleep(10);
    
        return 0;
    }
    

    Condition Variable 条件变量

    线程间同步有这样一种情况:线程A需要等待某个条件成立,才能继续往下执行,这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。
    pthread库使用条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待条件的线程。
    条件变量是pthread_cond_t类型的,初始化和销毁方式(类似于Mutex的初始化和销毁):

    #include <pthread.h>
    
    int pthread_cond_destroy(pthread_cond_t *cond);
    int pthread_cond_init(pthread_cond_t *restrict cond,  const pthread_condattr_t *restrict attr);
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  // <=> pthread_cond_init(&cond, NULL);
    

    Condition Variable操作方式

    #include <pthread.h>
    // 阻塞等待条件满足,可以设置超时时间。超时时,会自动退出阻塞等待状态
    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
    // 阻塞等待条件满足
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *resctrict mutex);
    // 唤醒在cond指向条件变量上等待的所有线程,让他们重新竞争锁资源
    int pthread_cond_broadcast(pthread_cond_t *cond);
    // 唤醒在cond指向的条件变量上等待的一个线程
    int pthread_cond_signal(pthread_cond_t *cond);
    

    一个Condition Variable总是和一个Mutex搭配使用:一个线程可以调用pthread_cond_wait阻塞等待某个条件,该函数主要完成三件事:

    1. 释放Mutex,这也是为什么条件变量需要传入Mutex(互斥锁);
    2. 阻塞当前线程,等待条件满足;
    3. 被唤醒时,重新获得Mutex并返回,需要别的线程来唤醒;
    #include <pthread.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    
    struct msg {
        struct msg *next;
        int num;
    };
    
    struct msg *head;
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
    
    void *producer(void *arg) {
        struct msg *mp;
        sleep(2);
        // 新建的链表节点,插入头部,用head指向
        for ( ; ; ) {
            mp = malloc(sizeof (struct msg));
            // rand() 生成0~RAND_MAX之间的伪随机数
            mp->num = rand() % 1000 + 1; // 生成随机数1~1000之间的随机数
            printf("Produce %d
    ", mp->num);
    
            pthread_mutex_lock(&lock);
            mp->next = head;
            head = mp;
            pthread_mutex_unlock(&lock);
    
            pthread_cond_signal(&has_product);
    
            sleep(rand() % 5); // 随机挂起当前线程 0~4秒
        }
    
    }
    
    void *consumer(void *arg) {
        struct msg *mp;
    
        for ( ; ; ) {
            pthread_mutex_lock(&lock);
    
            if (head == NULL)
                pthread_cond_wait(&has_product, &lock); // 线程阻塞等待,主动放弃mutex资源,等到唤醒时再次获取mutex
    
            mp = head;
            head = mp->next;
            pthread_mutex_unlock(&lock);
    
            printf("Consume %d
    ", mp->num);
            free(mp);
            sleep(rand() % 5); // 随机挂起当前线程 0~4秒
        }
    }
    
    int main() {
        pthread_t thrA, thrB;
        srand(time(NULL));
    
        pthread_create(&thrA, NULL, producer, NULL);
        pthread_create(&thrB, NULL, consumer, NULL);
    
        pthread_join(thrA, NULL);
        pthread_join(thrB, NULL);
    
        return 0;
    }
    

    Semaphore 信号量

    Mutex变量非0即1,看看作资源数为1的可用数量。初始时,资源数为1;加锁时,资源数减为0;释放锁时,资源数增加为1。
    信号量Semaphore 和Mutex类似,表示资源可用数量,不同的是该数量可 > 1。

    POSIX semaphore库函数(见sem_overview),可用于同一进程不同线程间同步,而且还可以用于不同进程间同步。

    #include <semaphore.h>
    
    // 初始化一个semaphore变量,value表示资源可用数量,pshared参数为0表示信号量用于同一进程的线程间同步
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    // 阻塞等待,可使资源可用数目-1
    int sem_wait(sem_t *sem);
    // 阻塞等待,但可设置超时时间
    int sem_trywait(sem_t *sem);
    // 释放资源,使资源可用数目+1
    int sem_post(sem_t *sem);
    // 释放与semaphore相关资源
    int sem_destroy(sem_t *sem);
    

    semaphore变量类型为sem_t;

    将上面的生产者-消费者示例,由Mutex锁 + 条件变量 + 链表实现,改成 信号量Semaphore + 环形队列实现:

    #include <semaphore.h>
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdbool.h>
    
    #define NUM    5
    
    sem_t blank_num, product_num;
    int queue[NUM];
    
    void *produce(void *arg) {
        int p = 0;
        while (true) {
            sem_wait(&blank_num);
            queue[p] = rand() % 1000 + 1;
            printf("Produce %d
    ", queue[p]);
            sem_post(&product_num);
            p = (p + 1) % NUM;
            sleep(rand() % 5);
        }
    }
    
    void *consume(void *arg) {
        int c = 0;
        int temp;
    
        while (true) {
            sem_wait(&product_num);
            printf("Consume %d
    ", queue[c]);
            queue[c] = 0;
            sem_post(&blank_num);
            c = (c + 1) % NUM;
            sleep(rand() % 5);
        }
    }
    
    int main() {
        pthread_t thrA, thrB;
    
        sem_init(&blank_num, 0, NUM);
        sem_init(&product_num, 0, 0);
        pthread_create(&thrA, NULL, produce, NULL);
        pthread_create(&thrB, NULL, consume, NULL);
    
        pthread_join(thrA, NULL);
        pthread_join(thrB, NULL);
    
        sem_destroy(&blank_num);
        sem_destroy(&product_num);
    
        return 0;
    }
    

    可以看到,条件变量需要搭配互斥锁使用,而信号里不一定。条件不满足的时候,即使获取了mutex锁资源,也会自动放弃,等到条件满足时再自动获取。

    互斥量与信号量的关系

    互斥量Mutex,信号量Semaphore都能用于线程同步/互斥,那么它们有什么区别呢?
    信号量可以表示资源可用数目,用于资源的保护。二值信号量(值只能为0或1)时,相当于互斥量。

    其他线程间同步机制

    如果数据是共享的,那么各线程读到的数据应该总是一致的,不会出现访问冲突。只要有一个线程可以修改数据,就要考虑线程同步问题。由此引出读写锁(Reader-Writer Lock)的概念。
    Reader之间并不互斥,Writer是独占的(exclusive),Writer修改数据时,其他Reader或Writer不能访问数据。因此,Reader-Writer Lock比Mutex具有更好的并发性。
    用挂起等待的方式解决访问冲突不见得是最好的办法,因为这样会影响系统的并发性,在某些情况下,解决访问冲突的问题可以尽量避免挂起某个进程,如Linux内核Seqlock、RCU(read-copy-update)等机制。

    详参加APUE2e

    自旋锁

    自旋锁类似互斥量, 不过并不通过休眠而阻塞线程, 而是在获取锁之前一直处于忙等(自旋)阻塞状态.
    自旋锁适用场景: 锁被持有时间短, 线程不希望在重新调度上花费太多成本.
    优点: 在非抢占式内核中时常很有用, 除了提供互斥机制外, 还会阻塞中断, 这样中断处理程序就不会让系统陷入死锁状态;
    缺点: 当线程自旋等待锁变为可用时, CPU不能做其他事情, 会浪费大量CPU时间poll;

    自旋锁原理类似于下面的代码:

    s = 1;
    
    某个线程:
    while (s <= 0) { ; }
    s--; // P操作
    ...
    s++; // V操作
    

    自旋锁操作方式 (用法类似于互斥量)

    #include <pthread.h>
    
    int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // 初始化自旋锁
    int pthread_spin_destroy(pthread_spinlock_t *lock); // 销毁自旋锁
    
    /* 成功返回0; 失败返回错误编号*/
    int pthread_spin_lock(pthread_spinlock_t *lock);
    int pthread_spin_trylock(pthread_spinlock_t *lock);
    int pthread_spin_unlock(pthread_spinlock_t *lock);
    

    编程练习

    哲学家问题:5个哲学家共有5跟筷子,哲学家坐成一圈,两人中间放一根筷子。哲学家吃饭的时候必须同时得到左右两根筷子。如果身边的任何一位正在使用筷子,那他只有等着。
    假设筷子编号:1,2,3,4,5,哲学家编号:A,B,C,D,E,哲学家和筷子围城一圈如下图所示:

    编程模拟哲学家就餐场景:

    Philosopher A fetches chopstick 5
    Philosopher B fetches chopstick 1
    Philosopher B fetches chopstick 2
    Philosopher D fetches chopstick 3
    Philosopher B releases chopsticks 1 2
    Philosopher A fetches chopstick 1
    Philosopher C fetches chopstick 2
    Philosopher A releases chopsticks 5 1
    ......
    

    用5个互斥锁Mutex表示5根筷子,5个独立线程代表5个哲学家就餐过程,要求每个哲学家都先拿左边的筷子,再拿右边的筷子,有任何一边那不到就等着,全拿到就吃饭rand()%10秒,然后放下筷子。

    分析:
    如果5个线程中哲学家都先取走左边的筷子,然后等待右边的筷子,就容易形成死锁。

    解决办法:
    参考 哲学家进餐问题-3种解决方案 | 博客园

    思路一
    通过一个额外的mutex,确保取走左边筷子和右边筷子是一个原子操作,即要么都取走,要么都不能取走。
    核心伪代码

    void *philosopher(void *arg) {
      int id = *(int *)arg; // id 是哲学家数组的索引
      while (true) {
        lock(&mutex); // 通过mutex确保同时取左边筷子和右边筷子是原子操作
        take_forks(id); // 取走左边和右边筷子
        eating();
        unlock(&mutex);
        putdown_forks(id);
      }
    }
    

    完整源码

    #include <pthread.h>
    #include <stdio.h>
    #include <stdbool.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    #define N     5 // 5个哲学家
    
    static char names[N] = {'A', 'B', 'C', 'D', 'E'}; // 哲学家编号
    static pthread_mutex_t cho[N]; // 5个筷子对应5个互斥锁
    static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    
    void take_forks(int id);
    void putdown_forks(int id);
    
    /**
     * names_index  phi_id  cho_id  cho_index
     *      0          A     5,1       4,0
     *      1          B     1,2       0,1
     *      2          C     2,3       1,2
     *      3          D     3,4       2,3
     *      4          E     4,5       3,4
    */
    static inline int left(int index) {
        return (index + N - 1) % N;
    }
    
    static inline int right(int index) {
        return index;
    }
    
    static void eating() {
        sleep(rand() % 2);
    }
    
    static void *philosopher(void *arg) {
        const int id = *(int *)arg;
        printf("philosopher : id = %d
    ", id);
    
        while (true) {
            pthread_mutex_lock(&lock);
            take_forks(id);
            pthread_mutex_unlock(&lock);
    
            eating();
            putdown_forks(id);
        }
    
        return NULL;
    }
    
    void putdown_forks(int id) {
        pthread_mutex_unlock(&cho[left(id)]);
        pthread_mutex_unlock(&cho[right(id)]);
    
        printf("Philosopher %c release chopstick %d %d
    ", names[id], left(id) + 1, right(id) + 1);
    }
    
    void take_forks(int id) {
    //    int l = left(id);
    //    int r = right(id);
    //    printf("left index: %d, right index: %d
    ", left(id), right(id));
    
        printf("Philosopher %c fetches chopstick %d
    ", names[id], left(id) + 1);
        printf("Philosopher %c fetches chopstick %d
    ", names[id], right(id) + 1);
        pthread_mutex_lock(&cho[left(id)]);
        pthread_mutex_lock(&cho[right(id)]);
    }
    
    // 通过一个mutex lock, 确保同时取得左右的筷子是原子操作
    void solution1() {
        for (int i = 0; i < N; ++i) {
            pthread_mutex_init(&cho[i], NULL);
        }
    
        pthread_t thrs[N];
    
        int ids[N];
    
        for (int i = 0; i < N; ++i) {
            ids[i] = i;
            pthread_create(&thrs[i], NULL, philosopher, &ids[i]);
        }
    
        void *tret;
        for (int i = 0; i < N; ++i) {
            pthread_join(thrs[i], &tret);
            printf("thread %d exit with code %d
    ", (int)tret);
        }
    
        for (int i = 0; i < N; ++i) {
            pthread_mutex_destroy(&cho[i]);
        }
    }
    
    int main() {
      solution1();
      return 0;
    }
    

    思路2
    最多只有4个哲学家才能先取走左边筷子,这样只是有一个哲学家可能成功就餐,不会形成死锁。
    这样就需要设置个信号量room 初值 4,代表最多有4个可先取走左边筷子的机会;每根筷子将互斥锁mutex换成信号量semaphore

    #include <semaphore.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    static sem_t room;
    static sem_t chop[N];
    
    void take_forks2(int id) {
        sem_wait(&chop[left(id)]);
        sem_wait(&chop[right(id)]);
        printf("Philosopher %c fetches chopstick %d
    ", names[id], left(id) + 1);
        printf("Philosopher %c fetches chopstick %d
    ", names[id], right(id) + 1);
    }
    
    void putdown_forks2(int id) {
        sem_post(&chop[left(id)]);
        sem_post(&chop[right(id)]);
    
        printf("Philosopher %c release chopstick %d %d
    ", names[id], left(id) + 1, right(id) + 1);
    }
    
    void *philosopher2(void *arg) {
        int id = *(int *)arg;
    
        while (true) {
            sem_wait(&room);
            take_forks2(id);
            sem_post(&room);
            eating();
            putdown_forks2(id);
        }
    }
    
    // 确保最多只有4个人能同时取得左边的筷子
    void solution2() {
        const int room_num = N - 1;
        pthread_t thrs[N];
        int ids[N];
        int err;
    
        sem_init(&room, 0, room_num);
    
        for (int i = 0; i < N; ++i) {
            sem_init(&chop[i], 0, 1);
        }
    
        for (int i = 0; i < N; ++i) {
            ids[i] = i;
            int err = pthread_create(&thrs[i], NULL, philosopher2, &ids[i]);
            if (err != 0) {
                perror("can't create thread
    ");
            }
        }
    
        while (1) {}
    
        sem_destroy(&room);
        for (int i = 0; i < N; ++i) {
            sem_destroy(&chop[i]);
        }
    }
    

    线程与信号

    每个线程都有自己的信号屏蔽字(signal mask), 但是信号的处理是进程中所有线程共享的. 也就是说, 单个线程可以阻止某些信号, 但是当线程修改了某个信号处理行为后, 所有线程共享这个改动.
    简而言之, 线程有权选择是否屏蔽信号, 但是信号捕获方式(SIG_DFL(默认)/SIG_IGN(忽略)/捕获), 以及捕获函数都是共享的.

    信号的递送

    如果一个信号与硬件故障相关, 那么信号一般会被发送到引起该事件的线程中去, 其他信号则被发送到任意一个线程. 哪个线程取决于系统具体实现.

    pthread_sigmask

    sigprocmask 修改进程的signal mask(信号屏蔽字)阻止信号发送, 而pthread_sigmask修改线程的signal mask阻止信号发送给线程.pthread_sigmask也可以用于获取线程的signal mask.

    #include <signal.h>
    int pthread_sigmask(int how, const sigset *restrict set, sigset_t *restrict oset);
    

    pthread_sigmask与sigprocmask类似, 不过失败时返回错误码, 而不是-1.
    参数
    how 取值: SIG_BLOCK 把信号添加到线程信号屏蔽字; SIG_SETMASK 用信号集替换线程的信号屏蔽字; SIG_UNBLOCK 从线程信号屏蔽字中移除信号集
    set 用于修改线程的信号屏蔽字的信号集. 当set为NULL时, oset可用于获取线程当前的信号屏蔽字
    oset 如果oset不为NULL, 线程之前的信号屏蔽字就存在它指向的sigset_t结构中

    sigwait

    线程可以调用sigwait等待一个或多个信号的出现. 线程调用sigwait等待信号的时候, 是处于阻塞状态的.

    #include <signal.h>
    
    int sigwait(const sigset_t *restrict set, int *restrict signop);
    

    参数
    set 指定线程等待的信号集
    sigop 指向的整数将包含发送信号的数量. 注意不是信号的编号.

    如果信号集中的某个信号在sigwait调用时, 处于pending状态, 那么sigwait将无阻塞返回, 而且返回前sigwait将从进程中清除那些pending的信号. 如果实现支持排队信号, sigwait也最多只会移除一个实例, 其他实例还要排队.
    sigwait会原子地取消信号集的阻塞状态, 直到有新的信号被递送; 返回前, sigwait将恢复线程的信号屏蔽字.

    如果多个线程在sigwait调用中等待同一个信号而阻塞, 只有一个线程可以从sigwait返回; 一个线程捕获信号, 另外一个线程sigwait等待信号, 具体是由哪个线程处理信号(第一个捕获, or 第二个从sigwait返回), 取决于系统实现.

    pthread_kill

    kill可以向一个进程发送信号, 而向一个线程发送信号使用pthread_kill.

    #include <signal.h>
    
    int pthread_kill(pthread_t thread, int signo);
    

    成功返回0, 失败返回错误编号.
    类似于kill, 可以传signo = 0, 检查线程是否存在.

    线程与fork

    注意: 不建议同时使用多线程和多进程.

    当线程调用fork时, 会为子进程创建整个进程地址空间的副本(正文段, init段, bss段, 堆段, 栈段, 命令行参数和环境变量段).
    除了地址空间, 子进程还从父进程继承了互斥量, 读写锁, 条件变量的状态. 如果不是马上调用exec, 就需要清理锁状态, 因为这些锁状态是父进程的运行状态, 在子进程没有意义. 然而, 子进程并知道自己占有了哪些锁, 哪些锁需要释放.
    如果没有exec, 子进程只能调用异步信号安全的函数(也就是没有使用锁), 不过这样限制了子进程的功能.

    pthread_atfork

    要清除锁状态, 可以调用pthread_atfork函数建立fork处理程序.

    #include <pthread.h>
    int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
    

    成功返回0 ; 失败返回错编号.
    pthread_atfork可安装3个帮助清理函数:
    prepare fork处理程序由父进程在fork创建子进程前调用, 该处理程序任务是获取父进程定义的所有锁;
    parent fork处理程序是在fork创建子进程后、返回之前, 在父进程上下文中调用的, 任务是对prepare fork处理程序获取的所有锁进行解锁;
    child fork处理程序是在fork创建子进程后、返回之前, 在子进程上下文中调用的, 任务是对prepare fork处理程序获取的所有锁进行解锁;

    这样做的目的是, 避免加锁一次, 解锁两次的情况.

    线程与I/O

    因为进程中所有线程共享文件描述符, 而且一个打开的文件只有一个偏移, 因此两个线程同时分别对同一个文件描述符进行lseek, read等操作, 会导致不安全行为.

    线程A                               线程B
    lseek(fd, 300, SEEK_SET);           lseek(fd, 700, SEEK_SET);
    read(fd, buf1, 100);                read(fd, buf2, 100);
    

    文件读锁(共享锁)并不能避免并发线程对同一文件读问题, 因为2个线程都是进行读操作. 而解决这个问题, 可以使用pread, pwrite. pread使偏移量的设定和数据读取写操作是一个原子操作. pwrite类似, 确保设置偏移量和数据写操作是一个原子操作.

    pread/pwrite 原子偏移+读/写

    pread, pwrite, 以给定偏移从文件描述符读/写数据

    #include <unistd.h>
    
    ssize_t pread(int fd, void *buf, size_t count, off_t offset);
    
    ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
    

    成功返回0; 失败-1, errno设置.

    pread, pwrite 与普通read, write函数相比, 多了lseek偏移操作, 并且跟读/写绑定到一起成为原子操作.

  • 相关阅读:
    关于Idea中右边的maven projects窗口找不到了如何调出来
    如何解决IntelliJ idea的maven工程提示的Cannot Resolve Symbol
    代码战争
    golang学习笔记----并发
    golang --学习笔记 运行时间计算
    golang ---timeb
    go ---MQTT client
    golang --strings
    Windows ----tasklist/taskkill
    转Tasklist(windows)
  • 原文地址:https://www.cnblogs.com/fortunely/p/14612019.html
Copyright © 2011-2022 走看看