09-线程(线程管理、信号量、互斥量)

线程

新线程拥有自己的栈,因此有自己的局部变量,如:线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量、线程私有数据。但与创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。Linux线程与POSIX标准线程存在一定细微区别,最明显的是信号处理部分。差别大都受底层Linux内核限制。

创建线程

大多数函数名都以pthread_开头,使用他们必须定义宏_REENTRANT,包含头文件pthread.h,并使用-lpthread链接线程库。

最初设计库例程时假设只有一个线程执行。errno 就是一个例子,默认只有一个线程访问。在一个线程准备获取刚刚的错误代码时,该变量很容易被另一个线程中函数调用所改变。类似的问题还出现在fputs之类的函数中,它们通常使用一个全局性的区域来缓存输出数据。

为解决问题使用可重入的例程,可重入代码可被不同的线程多次调用而仍正常工作,也可以是某种形式的嵌套调用。因此代码中可重用部分通常只使用局部变量,这使得每次对改代码的调用都获得它自己的唯一一份数据副本。

编写多线程时要在所有的 #include 前定义宏 _REENTRANT 告诉编译器我们需要可重入的功能。它将做三件事:

  1. 它为部分函数重新定义可安全重入版本,这些函数名后加上-r字符串。如:gethostbyname->gethostbyname_r
  2. stdio.h 中原来以宏的形式实现的一些函数将变为可安全重入的函数
  3. errno.h中定义的变量errno将变为一个函数调用,能以一种多线程安全方式获取真正的errno值

使用pthread.h头文件,链接pthread函数库。pthread_系列函数成功时返回0,失败时返回错误代码

#include <pthread.h>

typedef unsigned long int pthread_t;

// 线程相等返回0
int pthread_equal (pthread_t __thread1, pthread_t __thread2);

// 获得自身线程id
pthread_t pthread_self (void);

// 进程调用线程的浮点环境和信号屏蔽字,返回错误码不设置errno
int pthread_create (pthread_t *__restrict __newthread,       // 存放线程标识符
                    const pthread_attr_t *__restrict __attr, // 线程属性,可为NULL
                    void *(*__start_routine) (void *),       // 线程将要启动的函数
                    void *__restrict __arg);                 // 启动函数的参数
/*
单个线程3种方式退出:
1. 函数执行结束,返回值为退出码
2. 线程可以被同一进程种其他线程取消
3. 线程调用 pthread_exit
终止调用它的线程,返回指向某个对象的指针。
注:不能返回栈中的局部变量,因线程调用此此函数后就销毁
*/
void pthread_exit (void *__retval); // 参数可以通过 pthread_join 接收


/*
线程正常结束,返回值包含返回码
线程被取消,返回值指定的内存单元设置为 PTHREAD_CANCELED
通过pthread_join 可以将线程置于分离状态,这样资源就可以恢复。如果线程已处于分离状态,pthread_join 调用就会失败,返回 EINVAL。
*/
int pthread_join (pthread_t __th,          // 要等待的线程
                  void **__thread_return); // 指向指针的指针->指针->线程返回值

/*
**请求**取消同一进程中其他线程
目标线程可以选中忽略或控制如何被取消;取消时表现如同调用参数为 PTHREAD_CANCELED 得 pthread_exit 函数
*/
int pthread_cancel (pthread_t __th);

/*
线程清理处理程序,与进程中atexit函数类似
记录在栈中,执行顺序与注册顺序相反
线程执行以下动作时,清理函数rtn由pthread_cleanup_push函数调用:
1. 调用 pthread_exit 时
2. 响应取消请求时
3. 用非零execute参数调用 pthread_cleanup_pop 时
* 如果是从启动例程中正常返回,则不会调用pthread_cleanup_push
如果execute参数为0,清理函数将不被调用。无论那种情况,pthread_cleanup_pop 都将删除上次pthread_cleanup_push 调用建立得清理处理程序。
*/
void pthread_cleanup_push(void(*rtn)(void*), void* arg);
void pthread_cleanup_pop(int execute);

// ubuntu中函数实现如下
#  define pthread_cleanup_push(routine, arg) \
  do {									      \
    __pthread_cleanup_class __clframe (routine, arg)
#  define pthread_cleanup_pop(execute) \
    __clframe.__setdoit (execute);					      \
  } while (0)


/*
默认情况下线程得终止状态会保存到对该线程调用 pthread_join,就像父线程要对子线程调用 wait。
如果线程被分离,则线程底层存储资源在线程终止时立即被收回。已经被分离的线程无法使用join操作
*/
int pthread_detach (pthread_t __th);

编译命令:

gcc -D_REENTRANT main.c -lpthreadgcc -D_REENTRANT main.c -lpthread -L/usr/lib/nptl

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char message[] = "Hello World!";

void *thread_func(void *arg)
{
    printf("thread_func is running\n");
    sleep(3);
    strncpy(message, "Bye!", strlen(message));
    pthread_exit("This is from other thread.");
}

int main(int argc, char const *argv[])
{
    int res;
    pthread_t a_thread;
    void *thread_result;

    res = pthread_create(&a_thread, NULL, thread_func, (void *)message);
    if (res != 0) {
        perror("Thread create failed");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for thread to finish\n");
    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed\n");
        exit(EXIT_FAILURE);
    }

    printf("Thread joined, it returned: [%s]\n", (char *)thread_result);
    printf("message is \"%s\"\n", message);
    return 0;
}

同步

信号量

两种接口,一组是对POSIX的拓展,一组称为系统V信号量。

信号量是一种特殊变量,可以i被增加或减少,对其关键访问保证原子性。此处介绍二进制变量,只有0与1两种取值;还有一种更通用的信号量计数信号量。信号量函数都以sem_开头

#include <semaphore.h>

int sem_init (sem_t *__sem,     // 需要初始化的sem变量
              int __pshared,    // 控制信号量类型,为0表示这个信号量是当前进程局部信号量,否则可在多进程间共享。Linux暂不支持非零值。
              unsigned int __value);

int sem_post (sem_t *__sem);     // 以原子方式为sem加一
int sem_wait (sem_t *__sem);     // 以原子方式减一

int sem_destroy (sem_t *__sem);  // 清理sem拥有的所有资源,若企图清理的信号量正被一些线程等待,会收到一个错误

例子,一个线程读取输入,一个线程统计字符数。但此处没有对工作区的互斥访问

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define WORK_SIZE 1024

sem_t bin_sem;
char work_area[WORK_SIZE];

void *thread_fun(void *arg)
{
    sem_wait(&bin_sem);
    while (strncmp("end", work_area, 3) != 0) {
        printf("You input %ld characters\n", strlen(work_area) - 1);
        sem_wait(&bin_sem);
    }
    pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
    int res;
    pthread_t a_thread;
    void *thread_result;

    res = sem_init(&bin_sem, 0, 0);
    if (res != 0) {
        perror("Semaphor initalization failed");
        exit(EXIT_FAILURE);
    }

    res = pthread_create(&a_thread, NULL, thread_fun, NULL);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }

    printf("Input some text. Enter 'end' to finish\n");
    while (strncmp("end", work_area, 3) != 0) {
        fgets(work_area, WORK_SIZE, stdin);
        sem_post(&bin_sem);
    }
    printf("\nWaiting for thread to finish...\n");

    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed");
        exit(EXIT_FAILURE);
    }
    sem_destroy(&bin_sem);
    exit(EXIT_SUCCESS);

    return 0;
}

互斥量

成功返回0,失败返回错误代码,但并不设置errno

#include <pthread.h>

PTHREAD_MUTEX_INITIALIZER // 宏定义的默认属性互斥锁

/*
默认类型fast,这种类型对已加锁的互斥量加锁会导致阻塞,又因为互斥量被这个线程所拥有,所以死锁
可以改变属性,使其检查到就返回一个错误,或允许递归、可重入锁
*/
int pthread_mutex_init (pthread_mutex_t *__mutex,
                        const pthread_mutexattr_t *__mutexattr);//attr可为NULL
int pthread_mutex_destroy (pthread_mutex_t *__mutex);

int pthread_mutex_lock (pthread_mutex_t *__mutex);
int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex,
                             const struct timespec *__restrict __abstime);
int pthread_mutex_trylock (pthread_mutex_t *__mutex)
int pthread_mutex_unlock (pthread_mutex_t *__mutex);

读写锁

PTHREAD_RWLOCK_INITIALIZER  // 宏定义常量,默认属性读写锁
int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,
                         const pthread_rwlockattr_t *__restrict __attr);
int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);

// 存在错误返回,检查返回值
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);
int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);

// 可以获取时返回0。否则,返回错误 EBUSY
int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock);
int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock);

int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock,
                                const struct timespec *__restrict __abstime);
int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock,
                                const struct timespec *__restrict __abstime);

条件变量

注意:当先signal在wait会导致信号丢失,然后无限阻塞。

// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
//成功则返回0, 出错则返回错误编号.

// 申请资源,P操作
// **注意**:在wait函数阻塞时会释放其绑定的互斥体,所以在wait函数前lock这个mutex
//         在wait函数返回时会对互斥体加锁,所以在wait函数后unlock这个mutex
/*
注意虚假唤醒:
while (list.empty()) {
    pthread_cond_wait(&cond_, &mutex_);
}
*/
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
//成功则返回0, 出错则返回错误编号.

// 释放资源,V操作
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//成功则返回0, 出错则返回错误编号.

屏障

屏障时用户协调多个线程并行工作的同步机制。它允许每个线程等待,直到所有合作线程都到达某一点,然后从该点继续执行。pthread_join 就是一种屏障。

int pthread_barrier_init (pthread_barrier_t *__restrict __barrier,
                          const pthread_barrierattr_t *__restrict __attr,
                          unsigned int __count);// 指定继续运行前必须到达屏障的线程数
int pthread_barrier_destroy (pthread_barrier_t *__barrier);//成功返回0,失败返错误编号
/*
建立屏障,等待其他线程
调用此函数的线程在屏障计数未满足条件时进入休眠状态。第count个线程调用此函数时满足了屏障计数,所有线程被唤醒。
对于一个任意线程,函数返回PTHREAD_BARRIER_SERIAL_THREAD,其余的返回0。这个线程可以看为主线程,它可以工作在其他所有线程完成的工作结果上。
*/
int pthread_barrier_wait (pthread_barrier_t *__barrier);

自旋锁

用于锁被持有时间短,而且线程并不希望再重新调度上花费太多成本。一般用于内核,用户层编程不要使用。

extern int pthread_spin_init (pthread_spinlock_t *__lock, int __pshared);
extern int pthread_spin_destroy (pthread_spinlock_t *__lock);

extern int pthread_spin_lock (pthread_spinlock_t *__lock);
extern int pthread_spin_trylock (pthread_spinlock_t *__lock);
extern int pthread_spin_unlock (pthread_spinlock_t *__lock);

线程属性

守护线程又称分离线程(detached thread),可通过pthread_detach或修改线程属性创建。

#include <pthread.h>

int pthread_attr_init (pthread_attr_t *__attr); // 成功返回0,错误返回错误代码
int pthread_attr_destroy (pthread_attr_t *__attr);


int pthread_attr_getdetachstate (const pthread_attr_t *__attr, int *__detachstate);
int pthread_attr_setdetachstate (pthread_attr_t *__attr, int __detachstate);


int pthread_attr_getschedpolicy (const pthread_attr_t *__restrict __attr, int *__restrict __policy);
int pthread_attr_setschedpolicy (pthread_attr_t *__attr, int __policy);

/*
类似detachstate和schedplicy的还有很多
detachedstate
    PTHREAD_CREATE_DETACHED    不能调用 pthread_join
    PTHREAD_CREATE_JOINABLE
    
schedpolicy     控制属性调度方式
	SCHED_OTHER
	SCHED_RP     循环调度 只许root使用,有实时调度功能
	SCHED_FIFO   先进先出 只许root使用,有实时调度功能
	
schedparam      与前者联合使用,对 SCHED_OTHER 策略运行的线程调度进行控制

inheritsched
	PTHREAD_EXPLICIT_SCHED    默认,调度由属性明确设置
	PTHREAD_INHERIT_SCHED     新线程将沿用创建者的参数
	
scope  控制线程调度的计算方式,目前只支持 PTHREAD_SCOPE_SYSTEM
*/


取消线程

取消操作允许线程请求终止其所在进程总的任何其他线程。

  1. 通过pthread_testcancel 调用已编程方式建立线程取消点
  2. 线程等待pthread_cond_wait或pthread_cond_timewait中的特定条件
  3. 被sigwait(2)阻塞的函数
  4. 一些标准的库调用。通常这些调用包括线程可基于阻塞的函数

POSIX标准中,pthread_join(),pthread_testcancel().pthread_cond_wait(),pthread_cond_timewait(),sem_wait(),sigwait()等函数以及read,write等会引起阻塞的系统调用。

#include <pthread.h>

// 向目标线程发请求取消它
int pthread_cancel (pthread_t __th);

// 设置自己的取消状态,第一个参数可取 PTHREAD_CANCEL_ENABLE 允许线程接收取消请求;或 PTHREAD_CANCEL_DISABLE 忽略取消请求
// 接收取消请求后,用 pthread_setcanceltype 设置取消类型
int pthread_setcancelstate (int __state, int *__oldstate);

/*
type 有两个取值 
PTHREAD_CANCEL_ASYNCHRONOUS 立即取消
PTHREAD_CANCEL_DEFERRED     等到下述函数之一再采取行动
	pthread_join pthread_cond_timedwait pthread_cond_wait pthread_testcancel sem_wait sigwait
*/
int pthread_setcanceltype (int __type, int *__oldtype);

线程特殊数据

系统支持特定数量的线程特定数据,POSIX要求不小于128个。每个进程维护一个数组,数组元素是此索引位是否被使用的标识,还有线程退出时释放对应空间的函数
通过 pthread_key_create 获取到当前没有被使用的索引位,然后通过 pthread_getspecific pthread_setspecific 读写数据。



#include <pthread.h>

int pthread_once(pthread_once_t *once_control,  // 指向值为 PTHREAD_ONCE_INIT 变量的指针
                 void (*init_routine)(void));   // 指向的函数只会在第一次时被调用

int pthread_key_create(pthread_key_t *key,         // 返回值,当前没有被使用的标志位
                       void (*destructor)(void *));// 析构函数指针

void *pthread_getspecific(pthread_key_t key);      // 获取线程特殊数据

int pthread_setspecific(pthread_key_t key, const void *value); // 设置线程特殊数据
多线程版readline
#include "unpthread.h"

static pthread_key_t rl_key;
static pthread_once_t rl_once = PTHREAD_ONCE_INIT;

// 释放线程特定数据
static void readline_destructor(void *ptr) { free(ptr); }

static void readline_once(void) {
  Pthread_key_create(&rl_key, readline_destructor);
}

// readline 的线程特定数据结构:缓冲区、缓冲区中的字符数、缓冲区中的下一个字符
typedef struct {
  int rl_cnt;
  char *rl_bufptr;
  char rl_buf[MAXLINE];
} Rline;

static ssize_t my_read(Rline *tsd, int fd, char *ptr) {
  if (tsd->rl_cnt <= 0) {
  again:
    if ((tsd->rl_cnt = read(fd, tsd->rl_buf, MAXLINE)) < 0) {
      if (errno == EINTR) {
        goto again;
      }
      return (-1);
    } else if (tsd->rl_cnt == 0)
      return (0);
    tsd->rl_bufptr = tsd->rl_buf;
  }

  tsd->rl_cnt--;
  *ptr = *tsd->rl_bufptr++;
  return (1);
}

ssize_t readline(int fd, void *vptr, size_t maxlen) {
  size_t n, rc;
  char c, *ptr;
  Rline *tsd;

  // 每个线程都有一个独有的缓冲区
  Pthread_once(&rl_once, readline_once);
  if ((tsd = pthread_getspecific(rl_key)) == NULL) {
    tsd = Calloc(1, sizeof(Rline));
    Pthread_setspecific(rl_key, tsd);
  }

  ptr = vptr;
  for (n = 1; n < maxlen; n++) {
    if ((rc = my_read(tsd, fd, &c)) == 1) {
      *ptr++ = c;
      if (c == '\n') break;
    } else if (rc == 0) {
      *ptr = 0;
      return (n - 1);
    } else
      return (-1);
  }

  *ptr = 0;
  return (n);
}

// 对外暴露的函数,用于从文件描述符中读取一行
ssize_t Readline(int fd, void *ptr, size_t maxlen) {
  ssize_t n;

  if ((n = readline(fd, ptr, maxlen)) < 0) err_sys("readline error");
  return (n);
}
posted @ 2022-06-16 09:42  某某人8265  阅读(47)  评论(0编辑  收藏  举报