Linux c++ 线程

<p class="toc-title">目录</p>
<div class="toc-list">
    <ul>
    <li><a href="#线程与进程">1. 线程与进程</a><ul>
    <li><a href="#线程的概念">线程的概念</a></li>
    <li><a href="#深入理解进程和线程">深入理解进程和线程</a></li>
    </ul></li>
    <li><a href="#多线程">2. 多线程</a><ul>
    <li><a href="#什么是多线程">什么是多线程</a></li>
    <li><a href="#多线程模型的好处">多线程模型的好处</a></li>
    </ul></li>
    <li><a href="#线程标识">3. 线程标识</a></li>
    <li><a href="#线程创建">4. 线程创建</a><ul>
    <li><a href="#函数原型">函数原型</a></li>
    <li><a href="#参数说明">参数说明</a></li>
    <li><a href="#使用示例-打印线程id">使用示例-打印线程ID</a></li>
    </ul></li>
    <li><a href="#线程终止">5. 线程终止</a></li>
    <li><a href="#线程等待">6. 线程等待</a><ul>
    <li><a href="#函数原型-1">函数原型</a></li>
    <li><a href="#参数说明-1">参数说明</a></li>
    <li><a href="#使用示例-获得线程返回值">使用示例-获得线程返回值</a></li>
    </ul></li>
    <li><a href="#线程分离">7. 线程分离</a><ul>
    <li><a href="#pthread_detach">pthread_detach</a></li>
    <li><a href="#以分离状态创建线程">以分离状态创建线程</a></li>
    </ul></li>
    <li><a href="#线程取消">8. 线程取消</a><ul>
    <li><a href="#pthread_cancel">pthread_cancel</a></li>
    <li><a href="#线程取消属性">线程取消属性</a></li>
    <li><a href="#取消点">取消点</a></li>
    <li><a href="#自定义取消点">自定义取消点</a></li>
    <li><a href="#使用线程取消的风险">使用线程取消的风险</a></li>
    <li><a href="#线程清理程序">线程清理程序</a></li>
    </ul></li>
    </ul>
</div>

1. 线程与进程

线程的概念

线程是进程内相对独立的一个执行流,是进程内的一个执行单元,是操作系统中一个可调度的实体。

深入理解进程和线程

  • 在现代操作系统中,资源分配的基本单位是进程,而CPU调度执行的基本单位是线程
  • 进程不是调度单元,线程是进程使用CPU资源的基本单位
  • 进程有独立的地址空间,进程中可以存在多个线程共享进程资源
  • 线程不能脱离进程单独存在,只能依附于进程运行
  • 线程可以在不影响进程的情况下终止,但反之则不然

2. 多线程

什么是多线程

多线程,是指从软件或硬件层面上实现多个线程并发执行的技术。

  • 从软件层面看,一个进程中可以有多个线程,该程序也可以称之为多线程程序;
  • 从硬件层面看,多核处理器能够支持在同一时间执行多个线程。

实际上,对于单核处理器,即使软件编写为多线程模型,同一时间也只能执行一个线程,但这并不代表此时多线程就没有意义,因为处理器的数量不会影响程序结构,那么多线程编程模型在程序结构上到底有哪些好处呢?

多线程模型的好处

  • 通过为每种事件类型分配单独的处理线程,可以简化异步事件处理代码
  • 可以直接共享进程的数据资源
  • 将复杂问题分解为相互独立的任务,可以交叉进行,提高程序吞吐量
  • 通过把处理用户输入输出的部分和其他部分分开,可以改善交互式程序响应时间

3. 线程标识

  • 线程ID(Thread ID)是线程的唯一标识
  • 线程ID只有在它所属的进程上下文中才有意义
  • 线程ID类型为pthread_t,可能实现为unsigned long或结构体,依系统而定
  • 线程可以调用pthread_self获得自身线程ID
  • 可移植的程序应该调用pthread_equal来比较两个线程的ID
#include <pthread.h>

pthread_t pthread_self(); //返回调用线程的线程ID
int pthread_equal(pthread_t tid1, pthread_t tid2); //相等返回非0数值,否则返回0

4. 线程创建

函数原型

任意线程可以通过调用pthread_create创建新线程,start_routine为新线程的启动例程,创建成功后,新线程和调用线程谁先运行是不确定的。

//成功返回0,失败返回错误编号
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 

参数说明

  • pthread_create成功返回后,tid指向内存即为新线程ID
  • attr用于定制线程属性,若使用默认属性则传NULL
  • start_routine是线程启动例程
  • arg是start_routine的参数,若参数不止一个,就把这些参数放到一个结构中,再把该结构的地址作为arg传入

使用示例-打印线程ID

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

pthread_t tid;

void printf_tid(const char *s)
{
pid_t pid = getpid();
pthread_t tid = pthread_self();

<span class="hljs-built_in">printf</span>(<span class="hljs-string">"%s pid = %d, tid = %lu(0x%lx)\n"</span>, s, pid, tid, tid);

}

void *pthread_start(void *arg)
{
printf_tid("new thread: "); //新线程用pthread_self()获取自身ID,是因为新线程执行时pthread_create()可能还未返回,tid还未初始化完成
}

int main()
{
pthread_create(&tid, NULL, pthread_start, NULL);
sleep(1); //调用线程休眠1S,让新线程先执行
printf_tid("main thread: ");

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

注意:使用pthread的代码在编译时需要指定链接-lpthread

5. 线程终止

在不影响整个进程的情况下,单个线程有三种终止方式:

  • 在线程启动例程中调用return返回
  • 在线程启动例程中调用pthread_exit退出
  • 被进程中的其他线程取消
void pthread_exit(void *value_ptr);

value_ptr是一个无类型指针,进程中的其他线程可以调用pthread_join访问到这个指针。

6. 线程等待

函数原型

int pthread_join(pthread_t tid, void **value_ptr); //成功返回0,失败返回错误编号

调用线程将一直阻塞,直到等待的线程以上述三种方式终止。

参数说明

  • tid表示等待的线程ID
  • value_ptr用于保存线程的退出状态
  • 如果线程以return或pthread_exit方式终止,value_ptr指向内存就被设置为return或pthread_exit的参数
  • 如果线程被取消,value_ptr指向内存就被设置为PTHREAD_CANCELED
  • 如果不关心线程的返回值,就给value_ptr传NULL

使用示例-获得线程返回值

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

void *thread1_start(void *arg)
{
int ret = 0;
return ((void *)ret);
}

void *thread2_start(void *arg)
{
char *ret = "thread 2 exit";
pthread_exit(ret);
}

int main()
{
pthread_t tid1;
pthread_t tid2;
void *ret;

pthread_create(&amp;tid1, <span class="hljs-literal">NULL</span>, thread1_start, <span class="hljs-literal">NULL</span>);
pthread_create(&amp;tid2, <span class="hljs-literal">NULL</span>, thread2_start, <span class="hljs-literal">NULL</span>);

pthread_join(tid1, &amp;ret);
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 1: %d\n"</span>, (<span class="hljs-keyword">int</span>)ret); 

pthread_join(tid2, &amp;ret);
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 2: %s\n"</span>, (<span class="hljs-keyword">char</span> *)ret);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

7. 线程分离

在默认情况下,线程的终止状态会一直保存到对该线程调用pthread_join;但是,如果线程已经被分离,其占用的系统资源会在线程终止时被立即回收。
有两种方式可以使线程分离:

  • 调用pthread_detach,该函数不会使调用线程阻塞
  • 修改线程属性结构pthread_attr_t,以分离状态创建线程

在线程被分离后,就不能再用pthread_join等待它的终止状态了,因为对分离状态的线程调用pthread_join会产生未定义行为。

pthread_detach

int pthread_detach(pthread_t tid); //成功返回0,失败返回错误编号
#include <pthread.h>
#include <stdio.h>

void *thread_start(void *arg)
{
sleep(2);
printf("new thread exit\n");
pthread_exit(NULL);
}

int main()
{
pthread_t tid;

pthread_create(&amp;tid, <span class="hljs-literal">NULL</span>, thread_start, <span class="hljs-literal">NULL</span>);
pthread_detach(tid);

<span class="hljs-built_in">printf</span>(<span class="hljs-string">"main thread: pthread_detach() return\n"</span>);
sleep(<span class="hljs-number">5</span>);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

以分离状态创建线程

/*4个函数的返回值:成功返回0,失败返回错误编号*/

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_destroy(pthread_attr_t *attr);

可以调用pthread_attr_setdetachstate来设置线程的可分离状态:

  • detachstate = PTHREAD_CREATE_DETACHED,以分离状态启动线程
  • detachstate = PTHREAD_CREATE_JOINABLE,以正常状态启动线程
#include <pthread.h>
#include <stdio.h>

void *thread_start(void *arg)
{
sleep(2);
printf("new thread exit\n");
pthread_exit(NULL);
}

int main()
{
pthread_t tid;
pthread_attr_t attr;

pthread_attr_init(&amp;attr);
pthread_attr_setdetachstate(&amp;attr, PTHREAD_CREATE_DETACHED);   
pthread_create(&amp;tid, &amp;attr, thread_start, <span class="hljs-literal">NULL</span>);
pthread_attr_destroy(&amp;attr);

<span class="hljs-built_in">printf</span>(<span class="hljs-string">"main thread: pthread_attr_destroy() return\n"</span>);
sleep(<span class="hljs-number">5</span>);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

8. 线程取消

pthread_cancel

在编写多线程代码时,经常面临线程安全退出问题,一般情况下,最好使用将标志位置位的方式;
在其他线程中将标志位置位,然后调用pthread_join等待线程退出,回收线程占用的资源。

void *thread_start(void *arg)
{
    while (!quit)
    {
        //......
    }
}

int main()
{
quit = 1;
pthread_join(tid, NULL);
}

但是在某些应用中,线程可能正阻塞于某个函数(如pthread_cond_wait)无法被唤醒,即使设置了标志位也无法结束。
此时可以在其他线程中调用pthread_cancel请求取消线程,然后立即调用pthread_join等待线程退出。

int pthread_cancel(pthread_t tid); //成功返回0,失败返回错误编号

tid为要取消的线程ID,需要注意的是,pthread_cancel并不等待线程终止,它仅仅是发出请求。

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

void *thread1_start(void *arg)
{
sleep(10);
pthread_exit(NULL);
}

void *thread2_start(void *arg)
{
sleep(10);
pthread_exit(NULL);
}

int main()
{
pthread_t tid1;
pthread_t tid2;

pthread_create(&amp;tid1, <span class="hljs-literal">NULL</span>, thread1_start, <span class="hljs-literal">NULL</span>);
pthread_create(&amp;tid2, <span class="hljs-literal">NULL</span>, thread2_start, <span class="hljs-literal">NULL</span>);

sleep(<span class="hljs-number">1</span>);

pthread_cancel(tid1);
pthread_join(tid1, <span class="hljs-literal">NULL</span>);
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 1 exit\n"</span>);
 
sleep(<span class="hljs-number">1</span>);

pthread_cancel(tid2);
pthread_join(tid2, <span class="hljs-literal">NULL</span>);
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 2 exit\n"</span>);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

线程取消属性

线程取消有两个属性,分别是可取消状态可取消类型,这两个属性不在pthread_attr_t结构中,但它们影响着线程在响应取消请求时的行为。

/*
 * 可取消状态:PTHREAD_CANCEL_ENABLE(允许取消,默认属性),PTHREAD_CANCEL_DISABLE(不允许取消,但取消请求不会丢失,而是一直处于挂起状态)
 * 可取消类型:PTHREAD_CANCEL_DEFERRED(延迟取消,到达取消点才取消,默认属性),PTHREAD_CANCEL_ASYNCHRONOUS(异步取消,可在任意时刻取消)
*/
int pthread_setcancelstate(int state, int *oldstate);  //将线程可取消状态设为state,原有可取消状态通过oldstate返回,这两步是原子操作
int pthread_setcanceltype(int type, int *oldtype);     //将线程可取消类型设为type,原有可取消类型通过oldtype返回

取消点

默认情况下,线程的可取消类型为延迟取消,也就是说:被取消的线程在取消请求发出后还是继续运行,直到到达某个取消点。
取消点是线程检查它是否被取消的一个位置,根据《UNIX环境高级编程 第3版》P362-P363描述,POSIX.1定义的取消点和可选取消点如下。

自定义取消点

如果线程在很长一段时间内都不会调用前面两张图中的取消点函数,那么可以调用pthread_testcancel在线程中添加自己的取消点。
调用pthread_testcancel时,如果有某个取消请求处于挂起状态,且可取消状态为ENABLE,那么线程就会被取消。

void pthread_testcancel();

使用线程取消的风险

当线程响应取消请求而终止时,主要面临的两大风险:

  • 线程里面的锁可能没有unlock,有可能导致死锁
  • 线程申请的资源(如堆内存)没有释放

下面是一段由pthread_cancel引起的死锁范例代码。

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

static pthread_cond_t cond;
static pthread_mutex_t mutex;

void *thread0(void *arg)
{
pthread_mutex_lock(&mutex);
printf("thread 0 lock sucess\n");
pthread_cond_wait(&cond, &mutex); //主线程发出取消请求时,thread1阻塞于slepp(2),thread0阻塞于此取消点,导致thread0未解锁mutex就终止
printf("thread 0 pthread_cond_wait return\n");
pthread_mutex_unlock(&mutex);

pthread_exit(<span class="hljs-number">0</span>);

}

void *thread1(void *arg)
{
sleep(2);

<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 1 start lock\n"</span>);
pthread_mutex_lock(&amp;mutex);       <span class="hljs-comment">//thread0终止约1S后,thread1执行到此,由于mutex已加锁,也没有其他地方能够对其解锁,从而导致死锁</span>
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 1 lock sucess\n"</span>);      
pthread_cond_signal(&amp;cond);    
pthread_mutex_unlock(&amp;mutex); 

pthread_exit(<span class="hljs-number">0</span>);  

}

int main()
{
pthread_t tid[2];

pthread_cond_init(&amp;cond, <span class="hljs-literal">NULL</span>);
pthread_mutex_init(&amp;mutex, <span class="hljs-literal">NULL</span>);

pthread_create(&amp;tid[<span class="hljs-number">0</span>], <span class="hljs-literal">NULL</span>, thread0, <span class="hljs-literal">NULL</span>);
pthread_create(&amp;tid[<span class="hljs-number">1</span>], <span class="hljs-literal">NULL</span>, thread1, <span class="hljs-literal">NULL</span>);

sleep(<span class="hljs-number">1</span>);  
pthread_cancel(tid[<span class="hljs-number">0</span>]);
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"main thread request cancel thread 0\n"</span>);

pthread_join(tid[<span class="hljs-number">0</span>], <span class="hljs-literal">NULL</span>);
pthread_join(tid[<span class="hljs-number">1</span>], <span class="hljs-literal">NULL</span>);

pthread_cond_destroy(&amp;cond);
pthread_mutex_destroy(&amp;mutex);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

线程清理程序

可以使用线程清理程序来解决线程取消的风险问题。线程可以安排它退出时需要调用的函数,这样的函数称为线程清理程序。
一个线程可以注册多个清理程序,处理程序记录在栈中,也就是说,它们的执行顺序和注册顺序是相反的。

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理程序是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:

  • 调用pthread_exit结束线程
  • 响应pthread_cancel取消请求
  • 用非零execute参数调用pthread_cleanup_pop

注意:如果线程以return方式终止,线程清理程序不会被调用。

不管发生上述哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push登记的线程清理程序。
这两个函数有一个限制,由于它们经常实现为宏,所以必须在与线程启动例程相同的作用域中以配对的方式使用,否则,可能会产生编译错误。

回到线程取消的风险问题上来,我们只需要在线程清理程序中解锁和释放资源,并在线程启动例程的第一步就注册清理程序
这样,当线程因响应取消请求而终止时,线程清理程序就会得以执行。

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

static pthread_cond_t cond;
static pthread_mutex_t mutex;

void cleanup(void *arg)
{
pthread_mutex_unlock(&mutex);
printf("mutex unlock in cleanup\n");
}

void *thread0(void *arg)
{
pthread_cleanup_push(cleanup, NULL); //注册线程清理程序进行解锁

pthread_mutex_lock(&amp;mutex);    
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 0 lock sucess\n"</span>);
pthread_cond_wait(&amp;cond, &amp;mutex);    <span class="hljs-comment">//主线程发出取消请求时,thread1阻塞于slepp(2),thread0阻塞于此取消点</span>
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 0 pthread_cond_wait return\n"</span>); 
pthread_mutex_unlock(&amp;mutex); 

pthread_cleanup_pop(<span class="hljs-number">0</span>);
pthread_exit(<span class="hljs-number">0</span>);

}

void *thread1(void *arg)
{
sleep(2);

<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 1 start lock\n"</span>);
pthread_mutex_lock(&amp;mutex);          <span class="hljs-comment">//thread0终止约1S后,thread1执行到此</span>
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"thread 1 lock sucess\n"</span>);      
pthread_cond_signal(&amp;cond);    
pthread_mutex_unlock(&amp;mutex); 

pthread_exit(<span class="hljs-number">0</span>);  

}

int main()
{
pthread_t tid[2];

pthread_cond_init(&amp;cond, <span class="hljs-literal">NULL</span>);
pthread_mutex_init(&amp;mutex, <span class="hljs-literal">NULL</span>);

pthread_create(&amp;tid[<span class="hljs-number">0</span>], <span class="hljs-literal">NULL</span>, thread0, <span class="hljs-literal">NULL</span>);
pthread_create(&amp;tid[<span class="hljs-number">1</span>], <span class="hljs-literal">NULL</span>, thread1, <span class="hljs-literal">NULL</span>);

sleep(<span class="hljs-number">1</span>);  
pthread_cancel(tid[<span class="hljs-number">0</span>]);
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"main thread request cancel thread 0\n"</span>);

pthread_join(tid[<span class="hljs-number">0</span>], <span class="hljs-literal">NULL</span>);
pthread_join(tid[<span class="hljs-number">1</span>], <span class="hljs-literal">NULL</span>);

pthread_cond_destroy(&amp;cond);
pthread_mutex_destroy(&amp;mutex);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

最后,引用一篇由pthread_cancel引起死锁的博客https://blog.csdn.net/xsckernel/article/details/48052425,提取核心内容如下:

“通常的说法:某某函数是Cancellation Points,这种方法是容易令人混淆的。因为函数的执行是一个时间过程,而不是一个时间点。其实真正的Cancellation Points
只是在这些函数中Cancellation Type被修改为PHREAD_CANCEL_ASYNCHRONOUS和修改回PTHREAD_CANCEL_DEFERRED中间的一段时间。”

posted @ 2019-09-16 09:54  ylaoda  阅读(773)  评论(0编辑  收藏  举报