UNIX多线程编程(1) 创建多线程

来源:互联网 发布:gta5pc男角色捏脸数据 编辑:程序博客网 时间:2024/05/22 10:22

线程概念
线程是指运行中的程序的调度单位。一个线程指的是进程中一个单一顺序的控制流,也被称为轻量级线程。它是系统独立调度和分配的基本单位。同一进程中的多个线程将共享该系统中的全部系统资源,比如文件描述符和信号处理等。一个进程可以有很多线程,每个线程并行执行不同的任务。
线程与进程比较
① 和进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统中,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护其代码段、堆栈段和数据段,这种多任务工作方式的代价非常“昂贵”。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且线程间彼此切换所需要时间也远远小于进程间切换所需要的时间。
② 线程间方便的通信机制。对不同进程来说它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行。这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,不仅方便,而且快捷。

线程的同步问题
同一进程内的所有线程共享全局变量之外还共享:

  • 进程指令
  • 大多数数据
  • 打开的文件;
  • 信号处理函数和信号处置;
  • 当前工作目录;
  • 用户ID和组ID;

    不过每个线程有各自的:

  • 线程ID;
  • 寄存器集合,包括程序计数器和栈指针;
  • 栈;
  • errno;
  • 信号掩码;
  • 优先级;

同一进程内的所有线程共享相同的全局内存。这使得线程之间易于共享消息,然而伴随这种简易性而来的却是同步问题。
使用多线程其实是非常容易的。下面这个程序的主线程会创建了一个子线程并等待其运行完毕,子线程就输出它的线程ID号然后输出一句经典名言——Hello World。整个程序的代码非常简短,只有区区几行。

#include<stdio.h>#include<stdlib.h>#include<pthread.h>void* mythread(void*){    printf("子线程的线程ID号为:%d\n子线程输出Hello World\n", pthread_self());    return 0;}int main(){    printf("最简单的创建多线程实例\n");    pthread_t id;    pthread_create(&id, NULL, mythread, NULL);    pthread_join(id, NULL);    return 0;}

下面来细讲代码中的一些函数。

所需头文件 include<pthread.h> 功能 创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数。 函数原型 int pthread_create((pthread_t thread, pthread_attr_t *attr, void start_routine)(void ), void *arg) 函数参数 thread:线程标识符;attr:线程属性设置; start_routine:线程函数的起始地址;arg:传递给start_routine的参数; 函数返回值 成功,返回0;出错,返回-1
所需头文件 include<pthread.h> 功能 pthread_join使一个线程等待另一个线程结束。 函数原型 int pthread_join(pthread_t thread, void **value_ptr); 函数参数 thread:线程标识符;value_ptr:退出线程的返回值。; 函数返回值 成功,返回0;出错,返回错误码
所需头文件 include<pthread.h> 功能 线程主动退出 函数原型 int pthread_exit(void* status); 函数参数 status:用来存储线程结束时的返回值 函数返回值 成功,返回0;出错,返回-1

现在来实现线程报数功能,即第一个子线程输出1,第二个子线程输出2,第三个子线程输出3,……。要实现这个功能似乎非常简单——每个子线程对一个全局变量进行递增并输出就可以了。代码如下:

#include<stdio.h>#include<stdlib.h>#include<pthread.h>int g_nCount;void* mythread(void*){    g_nCount++;    printf("子线程的线程ID号为:%d报数%d\n", pthread_self(), g_nCount);    return 0;}int main(){    printf(" 子线程报数\n");    const int THREAD_NUM =5000;    pthread_t id[THREAD_NUM];    for (int i = 0; i < THREAD_NUM; i++)        pthread_create(&id[i], NULL, mythread, NULL);    for (int i = 0; i < THREAD_NUM; i++)        pthread_join(id[i], NULL);    printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nCount);      return 0;}

执行结果如下:
子线程的线程ID号为:1989199616报数4996
子线程的线程ID号为:2022770432报数4993
子线程的线程ID号为:2014377728报数4994
子线程的线程ID号为:2005985024报数4995
子线程的线程ID号为:2031163136报数4992
子线程的线程ID号为:1980806912报数4997
子线程的线程ID号为:2073126656报数4998
有5000个用户登录后记录结果是4998

结果是不对的,下面来分析一下问题所在。
我们就分析下g_nCount++;操作。在qtcreator对g_nCount++;这一语句打个断点,进入调试状态,点击Qt Creator下侧工具条上的黑色图标按钮, 打开反汇编窗口。可以看到显示的是AT&T格式的汇编代码。 可以发现在C/C++语言中一条简单的自增语句其实是由三条汇编代码组成的,如下图所示。
7 g_nCount++;
0x40073a <+0x000d> mov 0x200924(%rip),%eax # 0x601064
0x400740 <+0x0013> add $0x1,%eax
0x400743 <+0x0016> mov %eax,0x20091b(%rip) # 0x601064
讲解下这三条汇编意思:
第一条汇编将g_nCount的值从内存中读取到寄存器eax中。
第二条汇编将寄存器eax中的值与1相加,计算结果仍存入寄存器eax中。
第三条汇编将寄存器eax中的值写回内存中。
这样由于线程执行的并发性,很可能线程A执行到第二句时,线程B开始执行,线程B将原来的值又写入寄存器eax中,这样线程A所主要计算的值就被线程B修改了。这样执行下来,结果是不可预知的——可能会出现5000,可能小于5000。
因此在多线程环境中对一个变量进行读写时,我们需要有一种方法能够保证对一个值的递增操作是原子操作——即不可打断性,一个线程在执行原子操作时,其它线程必须等待它完成之后才能开始执行该原子操作。
下面列出一些常用的函数:

在用gcc编译的时候要加上选项 -march=i686type __sync_fetch_and_add (type *ptr, type value);type __sync_fetch_and_sub (type *ptr, type value);type __sync_fetch_and_or (type *ptr, type value);type __sync_fetch_and_and (type *ptr, type value);type __sync_fetch_and_xor (type *ptr, type value);type __sync_fetch_and_nand (type *ptr, type value);type __sync_add_and_fetch (type *ptr, type value);type __sync_sub_and_fetch (type *ptr, type value);type __sync_or_and_fetch (type *ptr, type value);type __sync_and_and_fetch (type *ptr, type value);type __sync_xor_and_fetch (type *ptr, type value);type __sync_nand_and_fetch (type *ptr, type value);

所以把线程函数改下就好了,如下:

void* mythread(void*){     __sync_fetch_and_add( &g_nCount, 1 );    printf("子线程的线程ID号为:%d报数%d\n", pthread_self(), g_nCount);    return 0;}

再次运行, 可以发现结果是唯一的。
子线程的线程ID号为:-1514625280报数4994
子线程的线程ID号为:-1506232576报数4995
子线程的线程ID号为:-1313200384报数4960
子线程的线程ID号为:-1288022272报数4957
子线程的线程ID号为:-1304807680报数4959
子线程的线程ID号为:-1279629568报数4956
有5000个用户登录后记录结果是5000

“`
因此,在多线程环境下,我们对变量的自增自减这些简单的语句也要慎重思考,防止多个线程导致的数据访问出错。
下一篇将讲解多线程的同步互斥问题。
参考文献:
[1].http://blog.csdn.net/morewindows/article/details/7429155
[2].http://blog.csdn.net/i_am_jojo/article/details/7591743
[3].http://blog.chinaunix.net/uid-29235952-id-4223902.html

1 0
原创粉丝点击