Linux多线程编程详解(1)

1.简介

线程是应用程序并发执行多个任务的机制,一个进程可以包含多个线程,且共享同一个全局内存区域,包括(未)初始化数据段、堆内存。多核处理器机器是可以支持多个线程真正意义上的多个线程并发执行。线程支持错误码errno,并有以下优点:

  线程间共享数据方便

  创建消耗系统资源少,耗时短。

  进程间很多属性是进程中所有线程共享的。例如进程ID、打开的文件描述符、栈、CPU时间消耗、信号等。

多线程编程时需要注意线程安全问题,信号处理问题,并且每个线程都在争用且可以使用宿主进程的虚拟内存空间(32位机器为3GB)。

线程相关的数据类型:

2.线程基础API

(1)创建线程

函数原型:
#include <pthread.h>
int pthread_create(pthread_t * thread_id,const pthread_attr_t*attr,void*(*start)(void),void*arg)
参数解释 thread_id:保存本次创建的线程的唯一标识id attr: 线程属性(通常为NULL,表示使用默认属性) start:线程要执行的函数 arg:start函数的参数,可为NULL
返回值:返回0成功,或错误时返回大于零的error码 注:因为pthread的库不是linux系统的库,所以在进行编译的时候要加上
-lpthread,例如gcc filename.c -lpthread

(2)主动退出线程

函数原型:
#include <pthread.h>
void pthread_exit(void * rval_ptr)
参数说明:
rval_ptr:线程返回值,由调用pthread_join()的线程获取。
注:调用在线程内调用此函数,相当于直接调用了return,但不同的是在线程中的任意函数调用了pthread_exit()都具有退出线程的同样效果。

(3)获取当前线程ID

函数原型:
pthread_t pthread_self(void);
参数说明:
返回值:返回当前线程的ID

(4)判断两个线程ID是否相同

函数原型:
int pthread_equal(pthread_t t1,pthread_t t2);
参数说明:
t1:线程1的ID
t2:线程2的ID
返回值:0表示不相等,不为0表示相等
注:pthread_t是unsigned long类型 ,但不能通过值是否相等来简单的判断t1、t2是否相等,需要通过此函数判断。

(5)连接已终止线程(joining)

等待thread_id标识的线程终止,并获取return或调用pthread_exit()返回值。
函数原型: 
int pthread_join( pthread_t thread_id, void **retvalue );
参数说明:
thread_id:要连接的线程的ID
retvalue:该值非空时,将保存线程终止前调用return或pthread_exit()时的返回值。
注: 若线程未分离,则必须要调用pthread_join()进行连接,否则在线程终止时将产生僵尸线程。僵尸线程过多将无法创建新的线程
  若传入之间已经连接过的线程ID,将导致无法预知行为。

 (6)分离线程

默认情况下,线程是可连接的,线程终止时是可以其他线程可以获取它的返回值。但当不关心线程的返回值,并且线程退出时让系统自动清理回收资源,就可以使用该函数。

函数原型:
#include<pthread.h>
int pthread_detach(pthread_t thread_id);
参数说明:
thread_id:要分离的线程ID
返回值:0表示成功,或返回大于零的错误码
注:一旦分离,无法恢复可连接状态,它是只影响终止线程以后的事情。
   可以这样使用pthread_detach(pthread_self());

3.线程取消API

(1)请求取消一个线程

函数原型:
#include<pthread.h>
int pthread_cancel(pthread_t pthread_id);
参数说明:
pthread_id:要取消的线程ID
返回值:返回0成功,或返回大于0的错误啊。
注:调用函数后何时取消线程与取消状态及类型、取消点有关。

(2)设置取消状态函数

函数原型:
int pthread_setcancelstate(int state,   int *oldstate)  
参数说明:
state:新的状态
oldstate:保存上一次设置的状态。可以为NULL,表示不关心之前状态,但不保证所有系统的兼容性。
返回值:0表示成功,或返回大于0的错误码
state类型说明如下:
       PTHREAD_CANCEL_ENABLE
       线程可取消。新建线程的默认状态,
       PTHREAD_CANCEL_DISABLE
      线程不可取消。收到取消线程请求时,将请求挂起,直到取消状态职位置为启用。
函数原型:
int pthread_setcanceltype(int type, int *oldtype);
参数说明:
type:新的类型
oldtype:保存上一次设置的类型。可以为NULL,表示不关心之前状态,
       PTHREAD_CANCEL_DEFERRED
       取消请求保持挂起状态,直至到达取消点(也就是延时状态)。新建线程的默认状态
       PTHREAD_CANCEL_ASYNCHRONOUS
       异步取消线程,可能在任何时间点(可能立即取消,但不一定)取消线程,比较少用。可异步取消的线程不应该分配资源(堆),不能获得锁、互斥量,一般是用于计算密集型的线程。

(3)取消点说明

若将线程的状态和类型置为启用和延时,当线程到达取消点,取消请求才生效。取消点可以是以下函数:

 其中还包括了stdio函数、openAPI、syslogAPI、nftw()、popen()、semop()、unlink()等函数,SUSv4移除了sifpause()、usleep()函数为取消点,增加了openat()函数。

增加一个取消点函数

如果是计算密集的线程,没有上述取消点,当请求取消该线程,将永远无法响应该取消请求。可以使用取消点函数。

函数原型:
#include<pthread.h>
void pthread_testcancel(void);
注:可以在线程中周期性调用该函数,确保响应其他线程请求退出。

(4)清理函数

 当线程直接被其他线程取消时,可能有资源没释放或未回收,将产生异常,如死锁、程序崩溃等。每个线程都有自己的清理函数栈,当线程被取消时,将依次沿该栈自顶向下执行清理函数,执行所有清理函数,线程终止。

清理函数入栈
函数原型:
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
参数说明:
routine:函数指针,将函数地址添加到清理函数的栈顶。
arg:传入routine所指向的清理函数参数,void*类型

清理函数出栈
函数原型:
void pthread_clean_pop(int execute);
参数说明:为0时,栈顶的清理函数出栈。当线程调用pthread_exit函数或者其它线程对本线程调用pthread_cancel函数时(return是不会执行的,直接出栈的),执行栈顶清理函数后再出栈。
              非0时,栈顶的清理函数会被执行,并出栈。
注:
  这两个函数必须成对使用,且要同属于一块语法块,因为有些系统是把这两个函数定义为宏。
  错误示例:
    pthread_cleanuo_push(p_fun,NULL);
      ...
    if(k>0)
    {
      pthread_clean_pop(0);
    }
  在下面三种情况下,pthread_cleanup_push()压栈的
"清理函数"会被调用:
     1.线程调用pthread_exit()函数,而不是直接return.     2.响应取消请求时,也就是有其它的线程对该线程调用pthread_cancel()函数。     3.本线程调用pthread_cleanup_pop()函数,并且其参数非0

 4.线程同步API

线程同步有两种工具:互斥量(mutex)和条件变量(condition).

4.1互斥量

防止多个线程同时访问同一共享变量。

4.1.1互斥量初始化

静态初始化
pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
动态初始化 函数原型:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 参数说明: mutex:要初始化的互斥量 mutexattr:互斥量的属性,若为NULL则使用缺省(默认)的属性。更多参考https://www.cnblogs.com/jest549/articles/14111802.html
返回值:0表示成功,或返回大于0的错误码 注: 有自动或动态分配的互斥量要使用pthread_mutex_destroy()销毁,静态分配则不需要。 以下情况必须使用动态初始化 (
1)动态分配在堆中的互斥量 (2)互斥量是在栈中分配的自动变量 (3)静态分配的互斥量但不使用缺省属性

4.1.2互斥量销毁

函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
mutex:要销毁的互斥量
返回值:0表示成功,或返回大于0的错误码
注:
当互斥量未锁定,且后续无任何线程企图再锁定它时,销毁才安全
若互斥量驻留在动态分配的内存,先销毁再free此内存区域
自动分配的互斥量,在宿主函数返回前销毁
销毁的互斥量可以使用pthread_mutex_init()函数重新初始化,再次使用

4.1.3互斥量的加解锁

加锁函数
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
mutex:需要加锁的互斥量
返回值:0表示成功,或返回其他大于0的错误码
注:若互斥量未锁定,将锁定并立即返回。若其他线程锁定,调用将阻塞线程直到互斥量被解锁,然后锁定互斥量再返回。

加锁变形函数(ubuntu需要 sudo apt-get install manpages-posix manpages-posix-dev 才能man 查的)
尝试加锁,若互斥量已被加锁则立即返回  
函数原型:
  int pthread_mutex_trylock(pthread_mutex_t *mutex);
  参数pthread_mutex_lock()函数一样,但不同的是若互斥量锁定会失败返回EBUSY错误。当轮询调用此函数,若存在其他很多线程使用pthread_mutex_lock等待同一互斥量,可能将永远不能获得上锁权。
尝试加锁,若互斥量已被加锁等待指定时间再返回
函数原型:
  int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
  参数:
  mutex:互斥量
  abstime:超时指定愿意等待的绝对时间(与相对时间对比而言,指定在时间X之前可以阻塞等待,而不是说愿意阻塞Y秒)。这个超时时间是用timespec结构来表示,它用秒和纳秒来描述时间。
  成功加锁返回0,失败返回大于0错误码。
  使用示例:
  
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <pthread.h>
int main(void)
{
    int err;
    struct timespec tout;  //纳秒级别
    struct tm *tmp;
    char buf[64];
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 
    pthread_mutex_lock(&lock);
    printf("mutex is locked.\n");
    clock_gettime(CLOCK_REALTIME, &tout);
    tmp = localtime(&tout.tv_sec);
    strftime(buf, sizeof(buf), "%r", tmp);  
   //strftime(char *str, size_t maxsize, const char *fmt, struct tm *time) 
  //按照参数fmt所设定格式将time类型的参数格式化为日期时间信息,然后存储在字符串str中(至多maxsize 个字符) printf("Current time is %s.\n", buf); tout.tv_sec += 10; //延迟10s err = pthread_mutex_timedlock(&lock, &tout); clock_gettime(CLOCK_REALTIME, &tout); tmp = localtime(&tout.tv_sec); strftime(buf, sizeof(buf), "%r", tmp); printf("The time is now %s\n", buf); if(err == 0) printf("mutex locked again!\n"); else printf("Can't lock mutex again: %s\n", strerror(err)); return 0; }

解锁:
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明,同加锁
注:不应该解锁未锁定的互斥量
   不应该解锁其他线程锁定的互斥量
   若不止一个线程等待某互斥量解锁,不确定哪个线程上锁该互斥量。  

 4.1.4互斥量加锁性能与避免死锁

  

 

 

   对于这样一段代码,使用单一线程执行1000万次,加上互斥锁耗时3.1s,不加则耗时0.35s,性能差距看起来为10倍,但一般而言,线程会花费更多时间做其他工作,加锁解锁操作对大部分程序性能影响不大。

  避免死锁有两种方法:

  1.分层加锁。即当多个线程对一组互斥量加锁时,总是以相同的顺序加锁。

  2.尝试加锁,若失败,则释放所有互斥量。线程先使用pthread_mutex_lock加锁第一个互斥量,然后使用pthread_mutex_trylock来锁定其余互斥量,若其中一个失败,则释放所有互斥量,以一定间隔重试。这样方法效率比较低,比较少用。

4.2条件变量

条件变量允许一个线程改变某个共享变量时通知其他线程,并让其他线程一直阻塞等待(休眠等待)这一变化状态。条件变量总是与互斥量结合使用。

4.2.1条件变量的初始化与销毁

静态分配条件变量:
pthread_cond_t=PTHREAD_COND_INITIALIZER;

动态分配条件变量:
函数原型:
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *attr);
参数说明:
cond:要初始化的条件变量
attr:初始化的条件变量属性,若为NULL,则使用缺省属性。
返回值:0表示成功,或返回大于0的错误码 注:对已初始化的条件变量,再次初始化,结果行为未定义。


销毁条件变量
函数原型:
int pthread_cond_destroy(pthread_cond_t *cond)
参数说明:与pthread_cond_init()函数对应参数类似。
注: 自动或动态初始化的条件变量应有pthread_cond_destroy()销毁,静态分配的条件变量不用销毁。 应在没有任何线程等待条件变量销毁,动态分配的条件变量应在free前销毁,自动分配的应在宿主函数返回前销毁。 被pthread_cond_destroy()销毁的条件变量,可以再次调用pthread_cond_init()初始化后使用。

4.2.2通知和等待条件变量

等待某个条件变量的函数
函数原型:
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
参数说明:
cond:指定等待的条件变量
mutex:配合使用的互斥锁,防止多线程pthread_cond_wait()竞争。
返回值:0表示成功,或大于0的错误码
注:
pthread_cond_wait()必须配合pthread_mutex_lock()、pthread_mutex_unlock()使用。
此函数会阻塞等待直到pthread_cond_signal()或pthread_cond_broadcast()通知。
mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

带超时条件等待某个条件变量函数
函数原型:
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec   *abstime);
cond:指定等待的条件变量
mutex:配合使用的互斥锁,防止多线程pthread_cond_wait()竞争。
abstime:与pthread_mutex_timelock()对应参数相同
返回值:0表示成功,或大于0的错误码
注:此函数与pthread_cond_wait函数行为类似,超时则返回ETIMEOUT错误码。

唤醒一个等待该条件的线程
函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
cond:指定唤醒的条件变量
返回值:0表示成功,或大于0的错误码
注:
    存在多个等待线程时按入队顺序激活其中一个
    会存在虚假唤醒、消息遗漏问题。
虚假唤醒即多核处理器可能唤醒多个等待同一条件变量的线程,所以需要加入判断条件,例如
pthread_mutex_lock(&lock);
while (condition_is_false)
{ 
    pthread_cond_wait(&cond, &lock);
}
 pthread_mutex_unlock(&lock);
消息遗漏即如果在一个线程调用pthread_cond_wait的过程中但未进入block状态,此时有线程调用了pthread_cond_signal或者pthread_cond_broadcast,那么此次消息将被遗漏掉,因为没有任何线程在pthread_cond_wait的block状态。这类问题的解决办法是设置一个pthread_cond_signal或者pthread_cond_broadcast的计数器count,在调用pthread_cond_wait之前先对这个count进行判断,如果count != 0 则说明已经错过了消息,可以不用等待,直接往下执行即可。例如:
 if (!count)
{
    pthread_mutex_lock(&lock);
    while (condition_is_false)
   {
          pthread_cond_wait(&cond, &lock);
   }
    pthread_mutex_unlock(&lock);
}

唤醒所有(广播)等待该条件的线程
函数原型
int pthread_cond_broadcast(pthread_cond_t *cond);
参数说明:
    与pthread_cond_wait()函数相同。
注:此函数数推荐使用在所有等待线程执行的任务不同,否则线程处理结果的效率可能不如pthread_cond_signal()

 4.3一次初始化函数

在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。

函数原型:
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
参数说明:
once_control:初值为PTHREAD_ONCE_INIT的once_control变量
init_routine:要调用的初始化函数
返回值:0代表成功,或返回大于0的错误码
关于线程特有函数请看:https://www.cnblogs.com/jest549/p/14115131.html

 

posted @ 2020-12-09 16:12  jest549  阅读(394)  评论(0编辑  收藏  举报