C++ 线程学习

时间:2022-03-24 12:41:02

原文:http://blog.csdn.net/wzy_1988/article/details/46562921

线程的概念

C++中的线程的Text Segment和Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符
  • 每种信号的处理方式
  • 当前工作目录
  • 用户id和组id

但是,有些资源是每个线程各有一份的:

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

我们将要学习的线程库函数是由POSIX标准定义的,称为POSIX thread或pthread。


线程控制


创建线程

创建线程的函数原型如下:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

  

返回值:成功返回0,失败返回错误号。

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数类型为void*,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值。

pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid可以得到当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单的当成整数用printf打印,调用pthread_self可以获取当前线程的id。

我们先来写一个简单的例子:

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

  

编译执行结果如下:

g++ thread.cpp -o thread -lpthread
./thread
main thread: pid 21046 tid 3612727104 (0xd755d740)
Child Process: pid 21046 tid 3604444928 (0xd6d77700)

从结果可以知道,thread_t类型是一个地址值,属于同一进程的多个线程调用getpid可以得到相同的进程号,而调用pthread_self得到的线程号各不相同。

如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,接下来,我们学习一下比较好的解决方法。


终止线程

如果需要只终止某个线程而不是终止整个进程,可以有三种方法:

  • 从线程函数return。这种方法对主线程不适应,从main函数return相当于调用exit。
  • 一个线程可以调用pthread_cancel终止同一个进程中的另一个线程。
  • 线程可以调用pthread_exit终止自己。

这里主要介绍pthread_exit和pthread_join的用法。

#include <pthread.h>

void pthread_exit(void *value_ptr);

  

value_ptr是void*类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join获取这个指针。 
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

#include <pthread.h>

int pthread_join(pthread_t thread, void **value_ptr);

  

返回值:成功返回0,失败返回错误号。

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

  • 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
  • 如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元存放的是常数PTHREAD_CANCELED。
  • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。参考代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> void* thread_function_1(void *arg)
{
printf("thread 1 running\n");
return (void *)1;
} void* thread_function_2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *) 2);
} void* thread_function_3(void* arg)
{
while (1) {
printf("thread 3 writeing\n");
sleep(1);
}
} int main(void)
{
pthread_t tid;
void *tret; pthread_create(&tid, NULL, thread_function_1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code %d\n", *((int*) (&tret))); pthread_create(&tid, NULL, thread_function_2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code %d\n", *((int*) (&tret))); pthread_create(&tid, NULL, thread_function_3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code %d\n", *((int*) (&tret))); return 0;
}

  

运行结果是:

thread 1 running
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writeing
thread 3 writeing
thread 3 writeing
thread 3 exit code -1

可见,Linux的pthread库中常数PTHREAD_CANCELED的值是-1.可以在头文件pthread.h中找到它的定义:

#define PTHREAD_CANCELED ((void *) -1)

  


线程间同步

多个线程同时访问共享数据时可能会冲突,例如两个线程都要把某个全局变量增加1,这个操作在某平台上需要三条指令才能完成:

  1. 从内存读变量值到寄存器。
  2. 寄存器值加1.
  3. 将寄存器的值写回到内存。

这个时候很容易出现两个进程同时操作寄存器变量值的情况,导致最终结果不正确。

解决的办法是引入互斥锁(Mutex, Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样,“读-修改-写”的三步操作组成一个原子操作,要不都执行,要不都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:

#include <pthread.h>

int pthread_mutex_destory(pthread_mutex_t *mutex);
int pthread_mutex_int(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
pthread_mutex_t mutex = PTHEAD_MUTEX_INITIALIZER;

  

返回值:成功返回0,失败返回错误号。

用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下列函数:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

  

返回值:成功返回0,失败返回错误号。

一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

我们用Mutex解决上面说的两个线程同时对全局变量+1可能导致紊乱的问题:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h> #define NLOOP 5000 int counter;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; void *do_add_process(void *vptr)
{
int i, val; for (i = 0; i < NLOOP; i ++) {
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);
} return NULL;
} int main()
{
pthread_t tida, tidb; pthread_create(&tida, NULL, do_add_process, NULL);
pthread_create(&tidb, NULL, do_add_process, NULL); pthread_join(tida, NULL);
pthread_join(tidb, NULL); return 0;
}

  

这样,每次运行都能显示到10000。如果去掉锁机制,可能就会有问题。这个机制类似于Java的synchronized块机制。


Condition Variable

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

#include <pthread.h>

int pthread_cond_destory(pthread_cond_t *cond);
int pthread_cond_init(pthead_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

  

返回值:成功返回0,失败返回错误号。

和Mutex的初始化和销毁类似,pthread_cond_init函数初始化一个Condition Variable,attr参数为NULL则表示缺省属性,pthread_cond_destroy函数销毁一个Condition Variable。如果Condition Variable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并且attr参数为NULL。Condition Variable的操作可以用下列函数:

#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

  

可见,一个Condition Variable总是和一个Mutex搭配使用的。一个线程可以调用pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作:

  1. 释放Mutex。
  2. 阻塞等待。
  3. 当被唤醒时,重新获得Mutex并返回。

pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。一个线程可以调用pthread_cond_signal唤醒在某个Condition Variable上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个Condition Variable上等待的所有线程。

下面的程序演示了一个生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费者从表头取走结构体。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> struct msg {
struct msg *next;
int num;
}; struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void* consumer(void *p)
{
struct msg *mp; for(;;) {
pthread_mutex_lock(&lock);
while (head == NULL) {
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
} void* producer(void *p)
{
struct msg *mp; for(;;) {
mp = (struct msg *)malloc(sizeof(*mp));
pthread_mutex_lock(&lock);
mp->next = head;
mp->num = rand() % 1000;
head = mp;
printf("Product %d\n", mp->num);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
} int main()
{
pthread_t pid, cid;
srand(time(NULL)); pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL); pthread_join(pid, NULL);
pthread_join(cid, NULL); return 0;
}

  

 
 
 

C++ 线程学习的更多相关文章

  1. Linux进程线程学习笔记:运行新程序

    Linux进程线程学习笔记:运行新程序                                         周银辉 在上一篇中我们说到,当启动一个新进程以后,新进程会复制父进程的大部份上下 ...

  2. C&num;线程学习笔记九:async &amp&semi; await入门二

    一.异步方法返回类型 只能返回3种类型(void.Task和Task<T>). 1.1.void返回类型:调用方法执行异步方法,但又不需要做进一步的交互. class Program { ...

  3. Python-多线程学习总结

    我们在做软件开发的时候很多要用到多线程技术.例如如果做一个下载软件象flashget就要用到.象在线视频工具realplayer也要用到因为要同时下载media stream还要播放.其实例子是很多的 ...

  4. C&num;线程学习笔记四:线程同步

    本笔记摘抄自:https://www.cnblogs.com/zhili/archive/2012/07/21/ThreadsSynchronous.html,记录一下学习过程以备后续查用.     ...

  5. java线程学习

    线程概念 当我问别人什么是线程的时候,别人给我讲了一大堆线程如何创建,如何使用以及若干线程的高深问题,其实作为一个资深菜鸟,我就想问问,什么是线程而已,找了书中的一些概念总结了一下,多线程与操作系统中 ...

  6. Java多线程线程学习(一)

    一.操作系统级别的进程与线程1.进程: 一个计算机程序的运行实例.包含了需要执行的指令,有自己的独立地址空间,是互相隔离的.进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理.2.线程: 表 ...

  7. IOS线程学习(一)

    1.NSThread  官方的描述 An NSThread object controls a thread of execution. Use this class when you want to ...

  8. python 线程学习

    彩照 一.学习[1] # -*- coding: utf-8 -*- import time import thread def timer(no, interval): cnt = 0 while ...

  9. 线程学习笔记 等待句柄和线程池(摘自https&colon;&sol;&sol;blog&period;gkarch&period;com&sol;threading&sol;part2&period;html&num;manualresetevent)

    //如果你的应用有很多线程,这些线程大部分时间都在阻塞,那么可以通过调用ThreadPool.RegisterWaitForSingleObject来减少资源消耗.这个方法接受一个委托,它会在向等待句 ...

随机推荐

  1. springboot 学习资源推荐

    springboot 是什么?对于构建生产就绪的Spring应用程序有一个看法. Spring Boot优先于配置的惯例,旨在让您尽快启动和运行.(这是springboot的官方介绍) 我们为什么要学 ...

  2. &lbrack;转载&rsqb;C&num;中as和is关键字的用法

    这篇文章主要介绍了C#中as和is关键字的用法的相关资料,需要的朋友可以参考下. 原文链接:http://www.jb51.net/article/80944.htm#comments  在程序中,进 ...

  3. 【转】EXCEL不显示科学计数法

    源地址:http://jingyan.baidu.com/article/e4d08ffdcc304e0fd3f60d69.html 2法无效,不知道为何

  4. java数据库连接池性能对比

    这个测试的目的是验证当前常用数据库连接池的性能. testcase Connection conn = dataSource.getConnection(); PreparedStatement st ...

  5. 大S《美容大王》内容80&percnt;都是没用的东西

    揭20位明星真实人品 我在她大陆的企划公司工作过,见过她N次了,对人态度蛮不错的,而且很爱开玩笑,但感觉很假很抠门.只要一回公司她就黏着请她吃饭.她很主动很霸道,她和周渝民交往,原因之一就是周什么都听 ...

  6. Linux基本命令(5)管理使用者和设立权限的命令

    管理使用者和设立权限的命令 命令 说明 命令 说明 chmod 用来改变权限 useradd 用来增加用户 su 用来修改用户     5.1 chmod命令 chmod命令用来改变许可权限.读取.写 ...

  7. easyui源码翻译1&period;32--ValidateBox(验证框)

    前言 使用$.fn.validatebox.defaults重写默认值对象.下载该插件翻译源码 validatebox(验证框)的设计目的是为了验证输入的表单字段是否有效.如果用户输入了无效的值,它将 ...

  8. select组件

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/ ...

  9. windows下安装python的C扩展编译环境(解决&OpenCurlyDoubleQuote;Unable to find vcvarsall&period;bat”)

    个人文章除注明转载外,均为个人原创或者翻译. 个人文章欢迎各种形式的转载,但请18岁以上的转载者注明文章出处,尊重我的劳动,也尊重你的智商: 本文链接:http://www.cnblogs.com/f ...

  10. MySQL(10):实体、实体表和外键(foreign key)

    1.实体        数据库管理系统中的各种用于数据管理方便而设定的各种数据管理对象,如:数据库表.视图.存储过程等都是数据库实体.广义上讲,这些对象中所存储的数据也是数据库实体.因为它们也是确切存 ...