如何在单独的线程中更新GTK + GUI的一部分,该线程不断从另一个进程读取数据?

时间:2019-05-14 11:07:10

标签: c linux multithreading gtk gtk3

在Linux下编写GTK + GUI来管理其他进程时遇到问题。我绝对不是GTK +的专家,而且我似乎无法解决这个问题。

我正在尝试编写一个GTK +应用程序,该应用程序应运行其他进程(特别是iPerf-网络测量程序-客户端和iPerf服务器,它们使用system()popen()进行管理/ pclose(),具体取决于用户单击的按钮。

有一些与启动客户端有关的按钮和两个用于启动和停止服务器的按钮,它们分别调用各自的回调。

尤其是服务器启动按钮,它调用一个回调,该回调负责启动线程,该线程应从服务器读取数据(异步退出)并相应地更新GUI的一部分,而GUI应响应执行其他操作(例如启动客户端)。

特别是,iPerf设置为每1秒输出一次新数据,并且每个数据每秒都位于iPerf返回的每一行上。

我尝试使用popen()从服务器读取数据。

如果我使用serverParserIdle()从GTK +回调中启动gdk_threads_add_idle()函数(如下所示),则它可以工作,但是有两个大问题阻止了程序正常工作:

1)iPerf输出由popen()缓冲,并且不像程序那样实时解析数据

2)serverParserIdle()线程锁定了GUI,并且我无法同时执行其他操作,例如运行客户端,这是我需要做的事情

尝试解决(2),我尝试使用gdk_threads_add_idle()gdk_threads_add_timeout(1000,...)进行更改。在这种情况下,GUI不再处于锁定状态,但是popen返回0并且服务器没有启动。你知道为什么吗?

该如何解决上面列出的所有问题?

这是前面提到的serverParserIdle()函数:

static gboolean serverParserIdle(gpointer data) {
    FILE *iperfFp;
    char linebuf[STRSIZE_LINEBUF];
    double goodput, final_goodput;
    char unit_letter;
    int total_datagrams, prev_total_datagrams=-1;
    struct parser_data *parser_data_struct=data;
    gchar *gput_label_str=NULL, *final_gput_label_str=NULL;
    char first_char;


    iperfFp=popen(parser_data_struct->cmd,"r"); //parser_data_struct->cmd contains a string containing the command to launch the iperf server "iperf -s -u -i 1 ..."

    if(!iperfFp) {
        // We enter here if gdk_threads_add_timeout(1000,...) is used to call serverParserIdle()
        return FALSE;
    }

    while(fgets(linebuf,sizeof(linebuf),iperfFp)!=NULL) {
        sscanf(linebuf,"%c %*s %*s %*f %*s %*f %*s %lf %c%*s %*f %*s %*s %d %*s",&first_char,&goodput,&unit_letter,&total_datagrams); // Parse useful data on this line

        if(first_char!='[' || (unit_letter!='K' && unit_letter!='M')) {
            // This is just to discrimate the useful lines
            continue;
        }

        if(unit_letter=='K') {
            goodput=goodput/1000;
        }

        // This is again a way to distinguish the last line of a client-server session from all the other lines
        if(prev_total_datagrams!=-1 && total_datagrams>prev_total_datagrams*2) {
            if(final_gput_label_str) {
                g_free(final_gput_label_str);
            }
            // Update final goodput value in the GUI
            final_goodput=goodput;
            prev_total_datagrams=-1;
            final_gput_label_str=g_strdup_printf("<b><span font=\"70\" foreground=\"blue\">%.2f</span></b>",goodput);
            gtk_label_set_text(GTK_LABEL(parser_data_struct->gput_labels.final_gput_info_label),final_gput_label_str);
        } else {
            if(gput_label_str) {
                g_free(gput_label_str);
            }
            prev_total_datagrams=total_datagrams;

            // Update current goodput value in the GUI (every 1s only when a client is being connected to the server)
            gput_label_str=g_strdup_printf("<b><span font=\"70\" foreground=\"#018729\">%.2f</span></b>",goodput);
            gtk_label_set_text(GTK_LABEL(parser_data_struct->gput_labels.gput_info_label),gput_label_str);
        }

        //fflush(iperfFp); <- tried flushing, but it does not work
    }

    pclose(iperfFp);

    g_free(gput_label_str);
    g_free(final_gput_label_str);


    return FALSE;
}

gdk_threads_add_idle()gdk_threads_add_timeout()实际上是从回调(start_server())中调用的,该回调使用以下命令分配给main()中的按钮:

g_signal_connect(button,"clicked",G_CALLBACK(start_server),&(data));

非常感谢您。

2 个答案:

答案 0 :(得分:1)

以下是Perl中的一个例子,以防有人感兴趣。这只是说明了如何在GTK事件循环中执行异步操作的基本原理:

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

use Glib 'TRUE', 'FALSE';
use Gtk3 -init;
use AnyEvent;  # Important: load AnyEvent after Glib!
use AnyEvent::Subprocess;

use constant {
    GTK_STYLE_PROVIDER_PRIORITY_USER => 800,
};

my $window = Gtk3::Window->new( 'toplevel' );
my $grid1 = Gtk3::Grid->new();
$window->add( $grid1 );
my $frame1 = Gtk3::Frame->new('Output');
$frame1->set_size_request(800,600);
$grid1->attach($frame1, 0,0,1,1);
my $scrolled_window = Gtk3::ScrolledWindow->new();
$scrolled_window->set_border_width(5);
$scrolled_window->set_policy('automatic','automatic');
my $textview = Gtk3::TextView->new();
my $buffer = $textview->get_buffer();
$buffer->set_text ("Hello, this is some text\nHello world\n");
$textview->set_wrap_mode('none');
$textview->set_editable(FALSE);
$textview->set_cursor_visible(FALSE);
set_widget_property( $textview, 'font-size', '18px' );
my $bg_color = Gtk3::Gdk::RGBA::parse( "#411934" );
$textview->override_background_color('normal', $bg_color);
my $color = Gtk3::Gdk::RGBA::parse( "#e9e5e8" );
$textview->override_color('normal', $color);
$textview->set_monospace(TRUE);

$scrolled_window->add($textview);
$frame1->add($scrolled_window);
$window->set_border_width(5);
$window->set_default_size( 600, 400 );
$window->set_position('center_always');
$window->show_all();
setup_background_command( $buffer );  # start background command
my $condvar = AnyEvent->condvar;
$window->signal_connect( destroy  => sub { $condvar->send } );
my $done = $condvar->recv;  # enter main loop...

sub setup_background_command {
    my ( $buffer ) = @_;

    my $job = AnyEvent::Subprocess->new(
        delegates     => [ 'StandardHandles', 'CompletionCondvar' ],
        code          => sub { exec 'unbuffer', 'myscript.pl' }
    );
    my $run = $job->run;
    $run->delegate('stdout')->handle->on_read(
        sub {
            my ( $handle ) = @_;
            my $line = $handle->rbuf;
            chomp $line;
            my $iter = $buffer->get_end_iter();
            $buffer->insert( $iter, $line . "\n" );
            $handle->rbuf = ""; # clear buffer
        }
    );
}

sub set_widget_property {
    my ( $widget, $prop, $value ) = @_;

    my $context = $widget->get_style_context();
    my $cls_name = $prop . '_class';
    $context->add_class( $cls_name );
    my $provider = Gtk3::CssProvider->new();
    my $css = sprintf ".%s {%s: %s;}", $cls_name, $prop, $value;
    $provider->load_from_data( $css );
    $context->add_provider($provider, GTK_STYLE_PROVIDER_PRIORITY_USER);
}

在这里,要在GTK事件循环中异步运行的命令是脚本myscript.pl

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

#STDOUT->autoflush(1);
sleep 1;
say "data 1";
sleep 1;
say "data 2";
sleep 1;
say "data 3";

请注意,可以通过用autoflush(1)取消注释行来使脚本无缓冲。但是总的来说,我们必须假设我们无法修改命令的内部,因此我使用unbuffer来运行脚本。

答案 1 :(得分:1)

我终于按照 pan-mroku 的建议(即使用IO Channels on a pipe opened with popen)解决了我的问题。

这是相关的代码,最终可以从服务器读取信息,因为新行以及新数据将由服务器本身打印到 stdout 并更新GTK + GUI。

#include <gtk/gtk.h>
#include <errno.h>
// ...


static gboolean serverParser(GIOChannel *source, GIOCondition condition, gpointer data) {
    gchar *linebuf; gsize strsize_linebuf;
    GIOStatus opstatus;
    int scan_retval=0;
    // ...

    opstatus=g_io_channel_read_line(source,&linebuf,&strsize_linebuf,NULL,NULL);
    if(opstatus==G_IO_STATUS_NORMAL && strsize_linebuf!=0) {
        scan_retval=sscanf(linebuf,"%c %*s %f%*[- *]%f %*s %*f %*s %lf %c%*s %*f %*s %*f%*[/ *]%d %*s",&field_1,&field_2,&field_3,&field_4,&field_5,&field_6);

        if(scan_retval==6) {
            // Work with the parsed server data, line by line
        }
    }

    // ...

    g_free(linebuf);
    return TRUE;
}



static void start_server(GtkWidget *widget, gpointer data) {
    // ...
    FILE *iperfFp;
    int iperfFd;
    GIOChannel *iperfIOchannel;

    // ...
    // Start server using stdbuf to get a line buffered output
    iperfFp=popen("stdbuf -o L iperf -s -u","r");

    if(!iperfFp) {
        g_print("Error in launching the server. errno = %d\n",errno);
        return;
    }

    iperfFd=fileno(iperfFp);

    iperfIOchannel=g_io_channel_unix_new(iperfFd);
    g_io_channel_set_flags(iperfIOchannel,G_IO_FLAG_NONBLOCK,NULL);
    g_io_channel_set_line_term(iperfIOchannel,NULL,-1);
    g_io_add_watch(iperfIOchannel,G_IO_IN,serverParser,&(data_struct->parser_pointers));

    // ...
}

// ...

单击“开始”按钮时,将调用start_server回调,这将使用popen启动iPerf服务器(但其他外部过程也可以执行此操作)并配置新的IO通道。然后,每次服务器本身生成新行时,都会调用serverParser来解析所有相关数据。

为了获得行缓冲的输出并为生成的每一行调用stdbuf,我必须首先通过调用-o L(使用参数serverParser)来启动外部iPerf进程。通过该过程。