C语言多线程教程

Posted Dontla

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言多线程教程相关的知识,希望对你有一定的参考价值。

1、Linux多线程,基本概念

2、Linux多线程,线程的分离与结合

3、Linux多线程,线程同步

4、Linux多线程,线程同步(2)

文章目录

一、基本概念

1、线程是计算机中独立运行的最小单位。进程是分配资源的单位。

2、为什么使用多线程?

(1)启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右。

(2)使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

此外,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  1. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

  2. 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

  3. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

3、线程不像进程那样,不是按照严格的父子层次来组织的。和一个进程相关的线程组成一个对等线程池(a pool of peers)。对等(线程)池概念的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止;进一步来说,每个对等线程都能读写相同的共享数据。

4、相关函数

1)创建线程

#include <pthread.h>

int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);

pthread_t pthread_self(void);

int pthread_equal(pthread_t thread1,pthread_t thread2);

int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));

linux系统支持POSIX多线程接口,称为pthread。编写linux下的多线程程序,需要包含头文件pthread.h,链接时需要使用库libpthread.a。

如果在主线程里面创建线程,程序就会在创建线程的地方产生分支,变成两个部分执行。线程的创建通过函数pthread_create来完成。成功返回0。

参数:

  • thread: 参数是一个指针,当线程成功创建时,返回创建线程ID。

  • attr: 用于指定线程的属性

  • start_routine: 该参数是一个函数指针,指向线程创建后要调用的函数。

  • arg: 传递给线程函数的参数。

2)线程终止

两种方式终止线程。

第一通过return从线程函数返回,

第二种通过调用pthread_exit()函数使线程退出。

需要注意的地方:

  • 一是,主线程中如果从main函数返回或是调用了exit函数退出主线程,则整个进程终止,此时所有的其他线程也将终止。
  • 另一种是,如果主线程调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,其他线程也不会结束,直到所有的线程都结束时,进程才结束。

3)线程属性

/* man pthread_attr_init */

typedef struct



  int                  detachstate;    //是否与其他线程脱离同步

  int                  schedpolicy;    //新线程的调度策略

  struct sched_param    schedparam;        //运行优先级等

  int                  inheritsched;    //是否继承调用者线程的值

  int             scope;       //线程竞争CPU的范围(优先级的范围)

  size_t             guardsize;        //警戒堆栈的大小

  int                  stackaddr_set;    //堆栈地址集

  void *              stackaddr;        //堆栈地址

  size_t             stacksize;        //堆栈大小

 pthread_attr_t;

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。

5、关于线程绑定 pthread_attr_setscope()

(1)关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。

#include <pthread.h>

pthread_attr_t attr;

pthread_t tid;


/*初始化属性值,均设为默认值*/

pthread_attr_init(&attr);

pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);

二、Linux多线程,线程的分离与结合(游离态线程结束后,资源直接就被操作系统回收了,所以游离态线程是为了避免内存泄露的?)

(2)线程的分离与结合 pthread_attr_setdetachstate() pthread_cond_timewait()

在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放(独立了?)。

线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态(即可结合的,joinable,需要回收),这种情况下,原有的线程等待创建的线程结束;只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。

设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了(还能这样啊?20220719,我貌似遇到过,海康h7创建线程立即返回,报错了),它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数(条件睡眠函数,能保证线程及时唤醒,sleep函数不能及时唤醒),让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

(3)另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。

(4)线程等待——正确处理线程终止(pthread_join()的目的为释放资源)

#include <pthread.h>

void pthread_exit(void *retval);

void pthread_join(pthread_t th,void *thread_return);//挂起等待th结束,*thread_return=retval;

int pthread_detach(pthread_t th);

如果线程处于joinable状态,则只能只能被创建他的线程等待终止。

在Linux平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。(说明:线程处于joinable状态下)

调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。(总结:不能对游离状态的线程join

(如何决定线程是否用游离态?)
如果不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。

创建 detach 线程:

pthread_t tid;

pthread_attr_t attr;

pthread_attr_init(&attr);

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

总之为了在使用 pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否则就需要调用 pthread_join() 函数来对其进行资源回收。

总结为什么要用游离态?比如我们主线程需要分配子线程处理任务,如果不用游离态,那么就需要用主线程join子线程,主线程被阻塞,子线程结束时,返回join,然后在主线程释放子线程资源;如果用了游离态,主线程就不用join了,自己想干啥就干啥去,可以继续分配子线程干活

先缓一缓,取干其他的。。。。

5)线程私有数据(Thread-specific Date)TSD

关于线程私有数据,这篇文章感觉讲得不错,还有源码:线程私有数据

进程内的所有线程共享进程的数据空间,因此全局变量为所有线程所共有。但有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Date)TSD来解决。
在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。
例如我们常见的变量errno,它返回标准的出错信息。
它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。
要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。

线程私有数据采用了一键多值的技术,即一个键对应多个数值,访问数据时好像是对同一个变量进行访问,但其实是在访问不同的数据。

创建私有数据的函数有4个:

  • pthread_key_create(创建)
  • pthread_setspecific(设置)
  • pthread_getspecific(获取)
  • pthread_key_delete(删除)
#include <pthread.h>

int pthread_key_creadte(pthread_key_t *key,void (*destr_fuction) (void *));

int pthread_setspecific(pthread_key_t key,const void * pointer));

void * pthread_getspecific(pthread_key_t key);

int pthread_key_delete(ptherad_key_t key);

三、Linux多线程,线程同步

20220721,我又回来了

线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。
linux下提供了多种方式来处理线程同步,最常用的是互斥锁条件变量异步信号

1)互斥锁(mutex)

通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);

int pthread_mutex_lock(pthread_mutex *mutex);

int pthread_mutex_destroy(pthread_mutex *mutex);

int pthread_mutex_unlock(pthread_mutex *);

(1)先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER

attr_t有:

PTHREAD_MUTEX_TIMED_NP:其余线程等待队列

PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许线程多次加锁,不同线程,解锁后重新竞争

PTHREAD_MUTEX_ERRORCHECK_NP:检错,与一同,线程请求已用锁,返回EDEADLK;

PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,解锁后重新竞争

(2)加锁,lock,trylock,lock阻塞等待锁,trylock立即返回EBUSY

(3)解锁,unlock需满足是加锁状态,且由加锁线程解锁

(4)清除锁,destroy(此时锁必需unlock,否则返回EBUSY,//Linux下互斥锁不占用内存资源

示例代码

(testthread.cpp)

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
#include "iostream"
 
using namespace std;
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tmp;
 
void* thread(void *arg)

    cout << "thread id is " << pthread_self() << endl;
    pthread_mutex_lock(&mutex);
    tmp = 12;
    cout << "Now a is " << tmp << endl;
    pthread_mutex_unlock(&mutex);
    return NULL;    

 
int main()

    pthread_t id;
    cout << "main thread id is " << pthread_self() << endl;
    tmp = 3;
    cout << "In main func tmp = " << tmp << endl;
    if (!pthread_create(&id, NULL, thread, NULL))
    
        cout << "Create thread success!" << endl;
    
    else
    
        cout << "Create thread failed!" << endl;
    
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;

编译: g++ -o thread testthread.cpp -lpthread

运行结果:

[root@ubuntu /arnold_test/20220721_test_thread_lock]4# ./a.out 
main thread id is 139705560844096
In main func tmp = 3
Create thread success!
thread id is 139705543493376
Now a is 12

说明:pthread库不是Linux系统默认的库,连接时需要使用静态库libpthread.a,所以在使用pthread_create()创建线程,以及调用pthread_atfork()函数建立fork处理程序时,需要链接该库。在编译中要加 -lpthread参数。

2)条件变量(cond)

参考文章:Linux C语言 pthread_cond_wait()、pthread_cond_timedwait()函数(不允许cond被唤醒时产生竞争,所以需要和互斥锁搭配)

利用线程间共享的全局变量进行同步的一种机制。条件变量上的基本操作有:

  • 触发条件(当条件变为 true 时);
  • 等待条件,挂起线程直到其他线程触发条件。
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);   

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有线程的阻塞

(1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER(前者为动态初始化,后者为静态初始化);属性置为NULL

(2)等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真,timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)

(3)激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)

(4)清除条件变量:destroy;无线程等待,否则返回EBUSY

对于

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

一定要在mutex的锁定区域内使用。

如果要正确的使用pthread_mutex_lockpthread_mutex_unlock,请参考:

pthread_cleanup_pushpthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!

另外,posix1标准说,pthread_cond_signalpthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。

说明:

(1)pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。

(2)互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。

(3)pthread_cond_timedwaitpthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在abstime指定的时间内cond未触发,互斥量mutex被重新加锁,且pthread_cond_timedwait返回错误 ETIMEDOUT。abstime 参数指定一个绝对时间,时间原点与 time 和 gettimeofday 相同:abstime = 0 表示 1970年1月1日00:00:00 GMT。

(4)pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。

(5)条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。

示例程序1:两个线程等待不同的互斥量(不同代码)中的同一条件变量

test_pthread_cond_example1.cpp
#include <stdio.h>
#include <pthread.h>
#include "stdlib.h"
#include "unistd.h"

pthread_mutex_t mutex;
pthread_cond_t cond;

void hander(void *arg)

    free(arg);
    (void)pthread_mutex_unlock(&mutex);


void *thread1(void *arg)

     pthread_cleanup_push(hander, &mutex);
     while(1)
     
         printf("thread1 is running\\n");
         pthread_mutex_lock(&mutex);
         pthread_cond_wait(&cond,&mutex);	//第一个进入的线程将上锁,并进入cond_wait状态
         printf("thread1 applied the condition\\n");
         pthread_mutex_unlock(&mutex);
         sleep(4);
     
     pthread_cleanup_pop(0);
 
 
void *thread2(void *arg)

     while(1)
     
         printf("thread2 is running\\n");
         pthread_mutex_lock(&mutex);
         pthread_cond_wait(&cond,&mutex);
         printf("thread2 applied the condition\\n");
         pthread_mutex_unlock(&mutex);
         sleep(1);
     

int main()

     pthread_t thid1,thid2;
     printf("condition variable study!\\n");
     pthread_mutex_init(&mutex,NULL);
     pthread_cond_init(&cond,NULL);
     pthread_create(&thid1,NULL,thread1,NULL);
     pthread_create(&thid2,NULL,thread2,NULL);
     sleep(1);
     do
     
         pthread_cond_signal(&cond);
     while(1);
     sleep(20);
     pthread_exit(0);
    return 0;

编译:

g++ -g test_pthread_cond_example1.cpp -lpthread -o test_pthread_cond_example1

运行结果:(中间我为了区分,不断敲击enter,合在一起的表示是同一时间执行的;可以假设空行时间间隔为cond触发间隔时间,1秒)

[root@ubuntu /arnold_test/20220721_test_thread_lock]25# ./test_pthread_cond_example1 
condition variable study!
thread1 is running
thread2 is running

thread1 applied the condition
thread2 applied the condition


thread2 is running
thread2 applied the condition



thread2 is running
thread2 applied the condition


thread2 is running
thread2 applied the condition

thread1 is running
thread1 applied the condition
thread2 is running
thread2 applied the condition


thread2 is running
thread2 applied the condition


thread2 is running
thread2 applied the condition


thread2 is running
thread2 applied the condition

thread1 is running
thread1 applied the condition
thread2 is running
thread2 applied the condition
^C

示例程序1扩展

test_pthread_cond_example1.cpp

#include <stdio.h>
#include <pthread.h>
#include "stdlib.h"
#include "unistd.h"
#include <string.h>
#include <sys/time.h>

#define printf(format, ...)                                                          \\
    do                                                                                 \\
          /*2.generate a time string and connect it to the log string tail.*/           \\
            char str[1024] = 0;                                                       \\
            struct timeval tv;                                                          \\
            struct tm* t;                                                               \\
            gettimeofday(&tv, NULL);                                                    \\
            t = localtime(&tv.tv_sec);                                                  \\
            sprintf(str,"[%04d-%02d-%02d %02d:%02d:%02d.%03ld] ",                       \\
                            1900 + t->tm_year, 1 + t->tm_mon, t->tm_mday,               \\
                            t->tm_hour, t->tm_min, t->tm_sec, tv.tv_usec / 1000);       \\
            printf("%s", str);                                                          \\
            printf("#%d "format, __LINE__, ##__VA_ARGS__);                              \\
        while (0以上是关于C语言多线程教程的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程程序设计初步入门

C语言多线程教程(pthread)(线程创建pthread_t,指定线程run方法pthread_create,加mutex锁,解锁,伪共享 false sharing假共享)

图文多线程入门教程

c语言实现多线程

Win32多线程

c语言多线程线程不执行的原因