多线程编程(Linux C)

多线程编程可以说每个程序员的基本功,同时也是开发中的难点之一,本文以Linux C为例,讲述了线程的创建及常用的几种线程同步的方式,最后对多线程编程进行了总结与思考并给出代码示例。

一、创建线程

多线程编程的第一步,创建线程。创建线程其实是增加了一个控制流程,使得同一进程中存在多个控制流程并发或者并行执行。

线程创建函数,其他函数这里不再列出,可以参考pthread.h

#include<pthread.h>int pthread_create(pthread_t *restrict thread,  /*线程id*/const pthread_attr_t *restrict attr,    /*线程属性,默认可置为NULL,表示线程属性取缺省值*/void *(*start_routine)(void*),  /*线程入口函数*/ void *restrict arg  /*线程入口函数的参数*/);

代码示例:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>char* thread_func1(void* arg) {pid_t pid = getpid();pthread_t tid = pthread_self();printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);char* msg = "thread_func1";return msg;
}void* thread_func2(void* arg) {pid_t pid = getpid();pthread_t tid = pthread_self();printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);char* msg = "thread_func2 ";while(1) {printf("%s running\n", msg);sleep(1);}return NULL;
}int main() {pthread_t tid1, tid2;if (pthread_create(&tid1, NULL, (void*)thread_func1, "new thread:") != 0) {printf("pthread_create error.");exit(EXIT_FAILURE);}if (pthread_create(&tid2, NULL, (void*)thread_func2, "new thread:") != 0) {printf("pthread_create error.");exit(EXIT_FAILURE);}pthread_detach(tid2);char* rev = NULL;pthread_join(tid1, (void *)&rev);printf("%s return.\n", rev);pthread_cancel(tid2);printf("main thread end.\n");return 0;
}

二、线程同步

有时候我们需要多个线程相互协作来执行,这时需要线程间同步。线程间同步的常用方法有:

  • 互斥
  • 信号量
  • 条件变量

我们先看一个未进行线程同步的示例:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>#define LEN 100000
int num = 0;void* thread_func(void* arg) {for (int i = 0; i< LEN; ++i) {num += 1;}return NULL;
}int main() {pthread_t tid1, tid2;pthread_create(&tid1, NULL, (void*)thread_func, NULL);pthread_create(&tid2, NULL, (void*)thread_func, NULL);char* rev = NULL;pthread_join(tid1, (void *)&rev);pthread_join(tid2, (void *)&rev);printf("correct result=%d, wrong result=%d.\n", 2*LEN, num);return 0;
}

运行结果:correct result=200000, wrong result=106860.

【1】互斥

这个是最容易理解的,在访问临界资源时,通过互斥,限制同一时刻最多只能有一个线程可以获取临界资源。

其实互斥的逻辑就是:如果访问临街资源发现没有其他线程上锁,就上锁,获取临界资源,期间如果其他线程执行到互斥锁发现已锁住,则线程挂起等待解锁,当前线程访问完临界资源后,解锁并唤醒其他被该互斥锁挂起的线程,等待再次被调度执行。

“挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。

主要函数如下:

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,     const pthread_mutexattr_t *restrict attr);       /*初始化互斥量*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);      /*销毁互斥量*/
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

用互斥解决上面计算结果错误的问题,示例如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>#define LEN 100000
int num = 0;void* thread_func(void* arg) {pthread_mutex_t* p_mutex = (pthread_mutex_t*)arg;for (int i = 0; i< LEN; ++i) {pthread_mutex_lock(p_mutex);num += 1;pthread_mutex_unlock(p_mutex);}return NULL;
}int main() {pthread_mutex_t m_mutex;pthread_mutex_init(&m_mutex, NULL);pthread_t tid1, tid2;pthread_create(&tid1, NULL, (void*)thread_func, (void*)&m_mutex);pthread_create(&tid2, NULL, (void*)thread_func, (void*)&m_mutex);pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&m_mutex);printf("correct result=%d, result=%d.\n", 2*LEN, num);return 0;
}

运行结果:correct result=200000, result=200000.

如果在互斥中还嵌套有其他互斥代码,需要注意死锁问题。

产生死锁的两种情况:

  • 一种情况是:如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,产生死锁。
  • 另一种典型的死锁情形是:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。

如何避免死锁:

  1. 不用互斥锁(这个很多时候很难办到)
  2. 写程序时应该尽量避免同时获得多个锁。
  3. 如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。 (比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用,以避免死锁。

【2】条件变量

条件变量概括起来就是:一个线程需要等某个条件成立(而这个条件是由其他线程决定的)才能继续往下执行,现在这个条件不成立,线程就阻塞等待,等到其他线程在执行过程中使这个条件成立了,就唤醒线程继续执行。

相关函数如下:

#include <pthread.h>int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

举个最容易理解条件变量的例子,“生产者-消费者”模式中,生产者线程向队列中发送数据,消费者线程从队列中取数据,当消费者线程的处理速度大于生产者线程时,会产生队列中没有数据了,一种处理办法是等待一段时间再次“轮询”,但这种处理方式不太好,你不知道应该等多久,这时候条件变量可以很好的解决这个问题。下面是代码:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>#define LIMIT 1000struct data {int n;struct data* next;
};pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
struct data* phead = NULL;void producer(void* arg) {printf("producer thread running.\n");int count = 0;for (;;) {int n = rand() % 100;struct data* nd = (struct data*)malloc(sizeof(struct data));nd->n = n;pthread_mutex_lock(&mlock);struct data* tmp = phead;phead = nd;nd->next = tmp;pthread_mutex_unlock(&mlock);pthread_cond_signal(&condv);count += n;if(count > LIMIT) {break;}sleep(rand()%5);}printf("producer count=%d\n", count);
}void consumer(void* arg) {printf("consumer thread running.\n");int count = 0;for(;;) {pthread_mutex_lock(&mlock);if (NULL == phead) {pthread_cond_wait(&condv, &mlock);} else {while(phead != NULL) {count += phead->n;struct data* tmp = phead;phead = phead->next;free(tmp);}}pthread_mutex_unlock(&mlock);if (count > LIMIT)break;}printf("consumer count=%d\n", count);
}int main() {pthread_t tid1, tid2;pthread_create(&tid1, NULL, (void*)producer, NULL);pthread_create(&tid2, NULL, (void*)consumer, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

条件变量中的执行逻辑:

关键是理解执行到int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
这里时发生了什么,其他的都比较容易理解。执行这条函数前需要先获取互斥锁,判断条件是否满足,如果满足执行条件,则继续向下执行后释放锁;如果判断不满足执行条件,则释放锁,线程阻塞在这里,一直等到其他线程通知执行条件满足,唤醒线程,再次加锁,向下执行后释放锁。(简而言之就是:释放锁-->阻塞等待-->唤醒后加锁返回

实现细节可看源码pthread_cond_wait.c和pthread_cond_signal.c

上面的例子可能有些繁琐,下面的这个代码示例则更为简洁:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>#define NUM 3
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER; void producer(void* arg) {int n = NUM;while(n--) {sleep(1);pthread_cond_signal(&condv);printf("producer thread send notify signal. %d\t", NUM-n);}
}void consumer(void* arg) {int n = 0;while (1) {pthread_cond_wait(&condv, &mlock);printf("recv producer thread notify signal. %d\n", ++n);if (NUM == n) {break;}}
}int main() {pthread_t tid1, tid2;pthread_create(&tid1, NULL, (void*)producer, NULL);pthread_create(&tid2, NULL, (void*)consumer, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

运行结果:

producer thread send notify signal. 1   recv producer thread notify signal. 1
producer thread send notify signal. 2   recv producer thread notify signal. 2
producer thread send notify signal. 3   recv producer thread notify signal. 3

【3】信号量

信号量适用于控制一个仅支持有限个用户的共享资源。用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待时,该计数值减一;当线程完成一次对semaphore对象的释放时,计数值加一。当计数值为0时,线程挂起等待,直到计数值超过0.

主要函数如下:

#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);

代码示例如下:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#include<semaphore.h>#define NUM 5int queue[NUM];
sem_t psem, csem; void producer(void* arg) {int pos = 0;int num, count = 0;for (int i=0; i<12; ++i) {num = rand() % 100;count += num;sem_wait(&psem);queue[pos] = num;sem_post(&csem);printf("producer: %d\n", num); pos = (pos+1) % NUM;sleep(rand()%2);}printf("producer count=%d\n", count);
}void consumer(void* arg){int pos = 0;int num, count = 0;for (int i=0; i<12; ++i) {sem_wait(&csem);num = queue[pos];sem_post(&psem);printf("consumer: %d\n", num);count += num;pos = (pos+1) % NUM;sleep(rand()%3);}printf("consumer count=%d\n", count);
} int main() {sem_init(&psem, 0, NUM);sem_init(&csem, 0, 0);pthread_t tid[2];pthread_create(&tid[0], NULL, (void*)producer, NULL);pthread_create(&tid[1], NULL, (void*)consumer, NULL);pthread_join(tid[0], NULL);pthread_join(tid[1], NULL);sem_destroy(&psem);sem_destroy(&csem);return 0;
}

信号量的执行逻辑:

当需要获取共享资源时,先检查信号量,如果值大于0,则值减1,访问共享资源,访问结束后,值加1,如果发现有被该信号量挂起的线程,则唤醒其中一个线程;如果检查到信号量为0,则挂起等待。

可参考源码sem_post.c

三、多线程编程总结与思考

最后,我们对多线程编程进行总结与思考。

  • 第一点就是在进行多线程编程时一定注意考虑同步的问题,因为多数情况下我们创建多线程的目的是让他们协同工作,如果不进行同步,可能会出现问题。
  • 第二点,死锁的问题。在多个线程访问多个临界资源时,处理不当会发生死锁。如果遇到编译通过,运行时卡住了,有可能是发生死锁了,可以先思考一下是那些线程会访问多个临界资源,这样查找问题会快一些。
  • 第三点,临界资源的处理,多线程出现问题,很大原因是多个线程访问临界资源时的问题,一种处理方式是将对临界资源的访问与处理全部放到一个线程中,用这个线程服务其他线程的请求,这样只有一个线程访问临界资源就会解决很多问题。
  • 第四点,线程池,在处理大量短任务时,我们可以先创建好一个线程池,线程池中的线程不断从任务队列中取任务执行,这样就不用大量创建线程与销毁线程,这里不再细述。

*************************************************************************************************************************************************

join

在Linux中,新建的线程并不是在原先的进程中,而是系统通过一个系统调用clone()。该系统调用clone() copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。这样,原先进程中的变量变动在copy后的进程中便能体现出来。

那么pthread_join函数有什么用呢???

pthread_join使一个线程等待另一个线程结束。

代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。

所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号。

如果你的主线程,也就是main函数执行的那个线程,在你其他线程退出之前就已经退出,那么带来的bug则不可估量。通过pthread_join函数会让主线程阻塞,直到所有线程都已经退出。

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

  2. thread:等待退出线程的线程号。

  3. value_ptr:退出线程的返回值。

在多线程编程的时候我们往往都是以for循环的形式调用pthread_join函数,既然运行prhtead_join之后主线程就阻塞了,也没法调用后面的pthread_join,那么以for循环有什么用呢?

主线程是在第一个线程处挂起。

比如有:

  1. pthread_join(1,NULL);

  2. pthread_join(2,NULL);

  3. pthread_join(3,NULL);

  4. pthread_join(4,NULL);

  5. pthread_join(5,NULL);

实际上主线程在pthread_join(1,NULL); 这里就挂起了,在等待1号线程结束后再等待2号线程。

当然会出现3,4,5比1,2先结束的情况。主线程还是在等待1,2结束后,发现3,4,5其实早已经结束了,就会回收3,4,5的资源,然后主线程再退出。

detach

  可以通过pthread_join()函数来使主线程阻塞等待其他线程退出,这样主线程可以清理其他线程的环境。但是还有一些线程,更喜欢自己来清理退出 的状态,他们也不愿意主线程调用pthread_join来等待他们。我们将这一类线程的属性称为detached(分离的)。如果我们在调用 pthread_create()函数的时候将属性设置为NULL,则表明我们希望所创建的线程采用默认的属性,也就是jionable(此时不是detached)。

如果需要将属性 设置为detached。则应该如下设定:

  1. pthread_attr_t  attr;

  2. pthread_attr_init(&attr);

  3. pthread_attr_setdetachstate(&attr,  PTHREAD_CREATE_DETACHED);

  4. pthread_create(&pthreadid,  &attr,  myprocess,  &arg);

警告:

  在线程设置为joinable后,可以调用pthread_detach()使之成为detached。但是相反的操作则不可以。还有,如果线程已经调用pthread_join()后,则再调用pthread_detach()则不会有任何效果。

线程可以通过自身执行结束来结束,也可以通过调用pthread_exit()来结束线程的执行。另外,线程甲可以被线程乙被动结束。这个通过调用pthread_cancel()来达到目的。

int pthread_cancel(pthread_t thread);

函数调用成功返回0。

当然,线程也不是被动的被别人结束。它可以通过设置自身的属性来决定如何结束

线程的被动结束分为两种,一种是异步终结,另外一种是同步终结。

异步终结就是当其他线程调用pthread_cancel的时候,线程就立刻被结束。

而同 步终结则不会立刻终结,它会继续运行,直到到达下一个结束点(cancellation point)。当一个线程被按照默认的创建方式创建,那么它的属性是同步终结。

多线程编程(Linux C)相关推荐

  1. Linux高性能server规划——多线程编程(在)

    多线程编程 Linux主题概述 线程模型 线程是程序中完毕一个独立任务的完整执行序列.即一个可调度的实体. 依据执行环境和调度者的身份.线程可分为内核线程和用户线程.内核线程,在有的系统上也称为LWP ...

  2. linux 多线程 semaphore ,Linux下多线程编程-Pthread和Semaphore使用.doc

    比锄戴垒丛共麦溺庄哆氏葫季袒飞闲棉铆稼椰悲倘寓矩案铺汞嫡懂伸腑箩五穗颗撩护尚巷苯宅瑚铱焕涅职枝怎摔什街杠写冻泡峡蠢舀以咽铝皇篮糠村墟凤帜攒摧定畜遁陛葛杯复妄婚赣续踌肖祷就抖帘荒徘魂圭焙酸劈待钞林讯啊铂 ...

  3. [转]Linux 的多线程编程的高效开发经验

    Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别.不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断.本文中我们 ...

  4. Linux下不使用qt自带sleep,Linux下Qt多线程编程

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 作者:武汉华嵌嵌入式培训中心 技术部 以下和大家分享Linux平台下Qt两种多线程编程的方式: 1.使用Linux平台下的线程函数.以下是给出的代码片段: ...

  5. Linux下的多线程编程

    1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程(process ...

  6. Linux 多线程编程

    这篇文章总结下 Linux 中多线程编程中能用到的几个函数,当然,需要同步操作的时候还需要加锁的操作,这里,没有列举的这么具体,只是把最常用的函数介绍下. 在编写多线程程序在编译的时候需要加上 -lp ...

  7. Linux环境多线程编程基础设施

    Linux环境多线程编程基础设施 来源:Yebangyu 本文介绍多线程环境下并行编程的基础设施.主要包括: Volatile __thread Memory Barrier __sync_synch ...

  8. 【linux】多线程编程(c语言编程)

    多线程编程 一.线程的基本概念         与进程相比,多线程是一种非常"节俭"的多任务操作方式.在linux操作系统下,启动一个新进程必须给     它分配独立的地址空间,建 ...

  9. Linux 的多线程编程的高效开发经验

    背景 Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别.不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断.本文 ...

  10. 详解Linux多线程编程

    前言 线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,如线程之间怎样同步.互斥,这些东西将在本文中介绍.我在某QQ群里见到这样一道面试题: 是否熟 ...

最新文章

  1. 虚拟桌面的备份恢复最佳实践 第一部分
  2. 图像和流媒体 -- 详解YUV数据格式
  3. putty的的颜色配置步骤
  4. 从零开始学keras之多分类问题
  5. 暂不升级iOS 14.2:多款iPhone续航崩溃
  6. 对比Vector、ArrayList、LinkedList有何区别(转)
  7. 社区发现(二)--GN
  8. 【转】BW的星型数据模型
  9. M1 mac外接显示器休眠后又失败
  10. 小米ui开发 android9,基于安卓10,小米9推送MIUI 10 9.8.22开发版
  11. GIS原理与技术-平时作业
  12. 苹果网页显示无法连接服务器失败怎么办啊,苹果手机自带的浏览器显示无法连接互联网是怎么回事啊...
  13. 南宁装修工长带队,价格公道不乱增加项目
  14. IDE工具(42) Alibaba Cloud Toolkit 一键部署插件使用入门
  15. 广和通l610二次开发|广和通l610 CAT.1模组opencpu开发《三》阿里云物联网平台mqtt动态注册
  16. python爬虫——大众点评——商户评论
  17. GNU radio入门学习(2)GNU radio简介
  18. python可以用于dsp吗_将Python/Matlab移植到C和定点DSP处理器上-C也应该是定点的吗?...
  19. OJ2105小泉的难题
  20. OpenCV 使用安卓手机作为摄像头

热门文章

  1. php5.3 appache phpstudy win7win8win10下 运行速度慢
  2. 电影推荐之《白鹿原》 隐私策略(Privacy policy)
  3. javascript之querySelector和querySelectorAll
  4. pytorch不加载fc_Pytorch自己加载单通道图片用作数据集训练的实例
  5. python locust mqtt_Boomer 实战压测 mqtt,2w 并发轻松实现
  6. gbdt 算法比随机森林容易_数据挖掘面试准备(1)|常见算法(logistic回归,随机森林,GBDT和xgboost)...
  7. 重构计算力 浪潮M5新一代服务器闪耀登场
  8. thinkphp-更新数据update函数
  9. Selenium如何处理类悬浮弹出菜单
  10. AS3类库资源大集合